diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index 1910d6153..738b67d9d 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; } @@ -207,8 +207,34 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", [self registerForDraggedTypes:[CPArray arrayWithObjects:CPRuleEditorItemPBoardType,nil]]; [_boundArrayOwner addObserver:self forKeyPath:_boundArrayKeyPath options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:boundArrayContext]; + + [[CPNotificationCenter defaultCenter] addObserver:self + selector:@selector(_ruleEditorLocalizerDidLoad:) + name:@"_CPRuleEditorLocalizerDidLoadNotification" + object:nil]; } + +- (void)_ruleEditorLocalizerDidLoad:(CPNotification)aNotification +{ + if ([aNotification object] === [self standardLocalizer]) + { + // Defer execution to the next run loop cycle so that any active slice + // insertions have fully completed and are present in the `_slices` array. + [[CPRunLoop mainRunLoop] performBlock:function() { + var count = [_slices count]; + for (var i = 0; i < count; i++) + { + var slice = [_slices objectAtIndex:i]; + [slice _reconfigureSubviews]; + [slice _updateButtonVisibilities]; // Force updates on row button tooltips + } + + [self _updatePredicate]; + [self _sendRuleAction]; + } argument:nil order:0 modes:[CPDefaultRunLoopMode]]; + } +} /*! @endcond */ /*! @@ -384,7 +410,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", */ - (CPDictionary)formattingDictionary { - return [_standardLocalizer dictionary]; + return [[self standardLocalizer] dictionary]; } /*! @@ -396,6 +422,9 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", */ - (void)setFormattingDictionary:(CPDictionary)dictionary { + if (_standardLocalizer == nil) + _standardLocalizer = [_CPRuleEditorLocalizer new]; + [_standardLocalizer setDictionary:dictionary]; _stringsFilename = nil; } @@ -440,6 +469,19 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", } } +- (_CPRuleEditorLocalizer)standardLocalizer +{ + if (_standardLocalizer == nil) + _standardLocalizer = [_CPRuleEditorLocalizer new]; + + return _standardLocalizer; +} + +- (void)setStandardLocalizer:(_CPRuleEditorLocalizer)aLocalizer +{ + _standardLocalizer = aLocalizer; +} + /*! @name Providing Data */ @@ -538,7 +580,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", if ([self rowTypeForRow:current_index] === CPRuleEditorRowTypeCompound) { var candidate = [[self _rowCacheForIndex:current_index] rowObject], - subObjects = [[self _subrowObjectsOfObject:candidate] _representedObject]; + subObjects = [self _subrowObjectsOfObject:candidate]; // Standard direct array query if ([subObjects indexOfObjectIdenticalTo:targetObject] !== CPNotFound) return current_index; @@ -605,7 +647,7 @@ TODO: implement for (var i = rowIndex + 1; i < count; i++) { var candidate = [[self _rowCacheForIndex:i] rowObject], - indexInSubrows = [[subobjects _representedObject] indexOfObjectIdenticalTo:candidate]; + indexInSubrows = [subobjects indexOfObjectIdenticalTo:candidate]; // Standard direct array query if (indexInSubrows !== CPNotFound) { @@ -774,7 +816,7 @@ TODO: implement while (current_index !== CPNotFound) { var rowObject = [[self _rowCacheForIndex:current_index] rowObject], - relativeChildIndex = [[subrows _representedObject] indexOfObjectIdenticalTo:rowObject]; + relativeChildIndex = [subrows indexOfObjectIdenticalTo:rowObject]; // Standard direct array query if (relativeChildIndex !== CPNotFound) [childsIndexes addIndex:relativeChildIndex]; @@ -831,7 +873,6 @@ TODO: implement for (i = 0; i < count; i++) { var item = [items objectAtIndex:i], - //var displayValue = [self _queryValueForItem:item inRow:aRow]; Ask the delegate or get cached value ?. displayValue = [[self displayValuesForRow:aRow] objectAtIndex:i], predpart = [self _sendDelegateRuleEditorPredicatePartsForCriterion:item withDisplayValue:displayValue inRow:aRow]; @@ -849,6 +890,7 @@ TODO: implement return nil; var current_index = [subrowsIndexes firstIndex]; + while (current_index !== CPNotFound) { var subpredicate = [self predicateForRow:current_index]; @@ -1278,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 @@ -1356,8 +1403,25 @@ TODO: implement if ([self rowTypeForRow:row] === type && itemIndex < [aCriteria count]) { - var crit = [aCriteria objectAtIndex:itemIndex]; - [current_criterions addObject:crit]; + // Verify that this row's parent path matches the path currently being built + var pathMatches = true; + for (var p = 0; p < itemIndex; p++) + { + var criterionA = [aCriteria objectAtIndex:p], + criterionB = [items objectAtIndex:p]; + + if (criterionA !== criterionB && (typeof criterionA.isEqual !== "function" || ![criterionA isEqual:criterionB])) + { + pathMatches = false; + break; + } + } + + if (pathMatches) + { + var crit = [aCriteria objectAtIndex:itemIndex]; + [current_criterions addObject:crit]; + } } } @@ -1924,17 +1988,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/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index 12bbbe514..14404e0ed 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 @@ -94,4 +97,231 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) return aString; } +#pragma mark - Formatting & Reordering Helpers + +- (CPString)_englishRepresentationForView:(id)aView +{ + if ([aView isKindOfClass:[CPPopUpButton class]]) + { + var selectedItem = [aView selectedItem]; + if (selectedItem) + { + var originalTitle = selectedItem._originalTitle; + + // Fallback: If not cached directly, inspect representedObject payload dictionary + if (!originalTitle) + { + var rep = [selectedItem representedObject]; + if (rep && typeof rep === "object" && [rep respondsToSelector:@selector(objectForKey:)]) + { + originalTitle = [rep objectForKey:@"value"]; + } + else if (rep && typeof rep === "string") + { + originalTitle = rep; + } + } + if (!originalTitle) + { + originalTitle = [selectedItem title]; + } + return "%[" + originalTitle + "]@"; + } + return "%[]@"; + } + else if ([aView isKindOfClass:[CPTextField class]] && ![aView isEditable]) + { + return aView._originalText || [aView stringValue]; + } + else + { + return "%@"; + } +} + +- (CPString)formattingKeyForViews:(CPArray)views +{ + var keyParts = []; + var count = [views count]; + for (var i = 0; i < count; i++) + { + var view = [views objectAtIndex:i]; + [keyParts addObject:[self _englishRepresentationForView:view]]; + } + return [keyParts componentsJoinedByString:@" "]; +} + +- (void)localizeMenuItemsForViews:(CPArray)views +{ + var count = [views count]; + for (var i = 0; i < count; i++) + { + var view = [views objectAtIndex:i]; + if ([view isKindOfClass:[CPPopUpButton class]]) + { + var menuItems = [view itemArray]; + var menuItemsCount = [menuItems count]; + var selectedItem = [view selectedItem]; + + for (var j = 0; j < menuItemsCount; j++) + { + var item = [menuItems objectAtIndex:j]; + + if (!item._originalTitle) + { + var rep = [item representedObject]; + if (rep && typeof rep === "object" && [rep respondsToSelector:@selector(objectForKey:)]) + { + item._originalTitle = [rep objectForKey:@"value"]; + } + else + { + item._originalTitle = [item title]; + } + } + + // Temporarily select item to generate formatting key context + [view selectItem:item]; + + var tempKey = [self formattingKeyForViews:views]; + var tempPattern = [self localizedStringForString:tempKey]; + + if (tempPattern !== tempKey) + { + var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g; + var match; + while ((match = regex.exec(tempPattern)) !== null) + { + var position = parseInt(match[1], 10) - 1; + var translatedValue = match[2]; + + if (position === i && translatedValue) + { + [item setTitle:translatedValue]; + break; + } + } + } + else + { + [item setTitle:item._originalTitle]; + } + } + + if (selectedItem) + { + [view selectItem:selectedItem]; + } + } + } +} + +- (CPArray)localizeAndReorderViews:(CPArray)views +{ + var key = [self formattingKeyForViews:views]; + var localizedPattern = [self localizedStringForString:key]; + + if (localizedPattern === key) + { + var count = [views count]; + + for (var i = 0; i < count; i++) + { + var originalView = [views objectAtIndex:i]; + if ([originalView isKindOfClass:[CPPopUpButton class]]) + { + var selectedItem = [originalView selectedItem]; + if (selectedItem && selectedItem._originalTitle) + { + [selectedItem setTitle:selectedItem._originalTitle]; + } + } + else if ([originalView respondsToSelector:@selector(setStringValue:)] && originalView._originalText) + { + [originalView setStringValue:originalView._originalText]; + + if ([originalView isKindOfClass:[CPTextField class]] && ![originalView isEditable]) + { + var font = [originalView font] || [CPFont systemFontOfSize:[CPFont systemFontSize]], + size = [originalView._originalText sizeWithFont:font]; + [originalView setFrameSize:CGSizeMake(size.width + 4, CGRectGetHeight([originalView frame]))]; + } + } + } + + return views; + } + + var newViews = [CPMutableArray array]; + var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g; + var lastIndex = 0; + var match; + + while ((match = regex.exec(localizedPattern)) !== null) + { + var literalText = localizedPattern.substring(lastIndex, match.index); + + // Only add a label if there are actual non-whitespace characters (like 'y') + if (literalText.length > 0 && /\S/.test(literalText)) + { + var label = [CPTextField labelWithTitle:literalText]; + [newViews addObject:label]; + } + + var position = parseInt(match[1], 10) - 1; + var translatedValue = match[2]; + + if (position >= 0 && position < [views count]) + { + var originalView = [views objectAtIndex:position]; + + if (translatedValue !== undefined && translatedValue !== null) + { + if ([originalView isKindOfClass:[CPPopUpButton class]]) + { + var selectedItem = [originalView selectedItem]; + if (selectedItem) + { + if (!selectedItem._originalTitle) + { + selectedItem._originalTitle = [selectedItem title]; + } + [selectedItem setTitle:translatedValue]; + } + } + else if ([originalView respondsToSelector:@selector(setStringValue:)]) + { + [originalView setStringValue:translatedValue]; + + // Recalculate frame size if it is a static CPTextField to avoid visual clipping + if ([originalView isKindOfClass:[CPTextField class]] && ![originalView isEditable]) + { + var font = [originalView font] || [CPFont systemFontOfSize:[CPFont systemFontSize]], + size = [translatedValue sizeWithFont:font]; + [originalView setFrameSize:CGSizeMake(size.width + 4, CGRectGetHeight([originalView frame]))]; + } + } + } + + [newViews addObject:originalView]; + } + + lastIndex = regex.lastIndex; + } + + if (lastIndex < localizedPattern.length) + { + var literalText = localizedPattern.substring(lastIndex); + + // Only add a label if there are actual non-whitespace characters + if (literalText.length > 0 && /\S/.test(literalText)) + { + var label = [CPTextField labelWithTitle:literalText]; + [newViews addObject:label]; + } + } + + return newViews; +} + @end diff --git a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j index 8866095d4..ee21be432 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) @@ -334,6 +342,28 @@ [_correspondingRuleItems setArray:ruleItems]; + // Localize drop-down options in context and reorder/insert intermediate labels natively + var localizer = [_ruleEditor standardLocalizer]; + if (localizer) + { + [localizer localizeMenuItemsForViews:_ruleOptionViews]; + _ruleOptionViews = [localizer localizeAndReorderViews:_ruleOptionViews]; + } + + // Rebuild frame configurations to match the new localized layout order + [_ruleOptionFrames removeAllObjects]; + [_ruleOptionInitialViewFrames removeAllObjects]; + + var newCount = [_ruleOptionViews count]; + for (var i = 0; i < newCount; i++) + { + var view = [_ruleOptionViews objectAtIndex:i], + frame = [view frame]; + + [_ruleOptionFrames addObject:frame]; + [_ruleOptionInitialViewFrames addObject:frame]; + } + if (!_editable) [self _updateEnabledStateForSubviews]; @@ -410,6 +440,9 @@ { [_addButton setHidden:[_ruleEditor _shouldHideAddButtonForSlice:self]]; [_subtractButton setHidden:[_ruleEditor _shouldHideSubtractButtonForSlice:self]]; + + [_addButton setToolTip:[_ruleEditor _toolTipForAddSimpleRowButton]]; + [_subtractButton setToolTip:[_ruleEditor _toolTipForDeleteRowButton]]; } - (void)_configurePlusButtonByRowType:(CPRuleEditorRowType)type diff --git a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j new file mode 100644 index 000000000..20c22eeea --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j @@ -0,0 +1,173 @@ +@import +@import +@import +@import +@import +@import +@import "RuleDelegate.j" + +@implementation AppController : CPObject +{ + CPWindow theWindow; + CPRuleEditor ruleEditor; + CPTextField predicateField; + RuleDelegate ruleDelegate; + CPPopUpButton langPopUp; + + CPDictionary englishDict; + CPDictionary spanishDict; + CPDictionary germanDict; +} + +- (void)applicationDidFinishLaunching:(CPNotification)aNotification +{ + theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(50, 50, 800, 520) + styleMask:CPTitledWindowMask | CPClosableWindowMask | CPResizableWindowMask]; + [theWindow setTitle:@"CPRuleEditor Multi-Language Localization Demo"]; + [theWindow setFullPlatformWindow:YES]; + + var contentView = [theWindow contentView]; + [contentView setBackgroundColor:[CPColor colorWithHexString:@"f3f4f5"]]; + + var label = [CPTextField labelWithTitle:@"CPRuleEditor Sentence Localization & Positional Reordering:"]; + [label setFrame:CGRectMake(20, 20, 500, 24)]; + [label setFont:[CPFont boldSystemFontOfSize:14]]; + [contentView addSubview:label]; + + // Language Selector Label + var langLabel = [CPTextField labelWithTitle:@"Language:"]; + [langLabel setFrame:CGRectMake(530, 20, 80, 24)]; + [langLabel setFont:[CPFont boldSystemFontOfSize:12]]; + [langLabel setAlignment:CPRightTextAlignment]; + [contentView addSubview:langLabel]; + + // Language Selector PopUpButton + langPopUp = [[CPPopUpButton alloc] initWithFrame:CGRectMake(620, 16, 160, 24)]; + [langPopUp addItemWithTitle:@"Spanish"]; + [langPopUp addItemWithTitle:@"German"]; + [langPopUp addItemWithTitle:@"English"]; + [langPopUp setTarget:self]; + [langPopUp setAction:@selector(changeLanguage:)]; + [contentView addSubview:langPopUp]; + + ruleDelegate = [[RuleDelegate alloc] init]; + + // Create Rule Editor + ruleEditor = [[CPRuleEditor alloc] initWithFrame:CGRectMake(20, 55, 760, 250)]; + [ruleEditor setAutoresizingMask:CPViewWidthSizable]; + [ruleEditor setDelegate:ruleDelegate]; + [ruleEditor setEditable:YES]; + [ruleEditor setNestingMode:CPRuleEditorNestingModeList]; + [ruleEditor setRowHeight:28]; + [ruleEditor setTarget:self]; + [ruleEditor setAction:@selector(ruleEditorAction:)]; + + // Programmatic dictionaries setup + englishDict = [CPDictionary dictionary]; // Identity fallback + + spanishDict = [CPDictionary dictionaryWithDictionary:@{ + @"%[firstName]@ %[is equal to]@ %@" : @"%1$[Nombre]@ y %3$@ %2$[son iguales]@", + @"%[firstName]@ %[contains]@ %@" : @"%1$[Nombre]@ %2$[contiene]@ %3$@", + @"%[lastName]@ %[is equal to]@ %@" : @"%1$[Apellido]@ y %3$@ %2$[son iguales]@", + @"%[lastName]@ %[contains]@ %@" : @"%1$[Apellido]@ %2$[contiene]@ %3$@", + @"%[age]@ %[is equal to]@ %@" : @"%1$[Edad]@ y %3$@ %2$[son iguales]@", + @"%[age]@ is equal to %@" : @"%1$[Edad]@ y %3$@ %2$[son iguales]@", + @"Add row" : @"Añadir regla", + @"Delete row" : @"Eliminar regla", + @"Add compound row" : @"Añadir grupo de reglas" + }]; + + germanDict = [CPDictionary dictionaryWithDictionary:@{ + @"%[firstName]@ %[is equal to]@ %@" : @"%1$[Vorname]@ und %3$@ %2$[sind gleich]@", + @"%[firstName]@ %[contains]@ %@" : @"%1$[Vorname]@ %2$[enthält]@ %3$@", + @"%[lastName]@ %[is equal to]@ %@" : @"%1$[Nachname]@ und %3$@ %2$[sind gleich]@", + @"%[lastName]@ %[contains]@ %@" : @"%1$[Nachname]@ %2$[enthält]@ %3$@", + @"%[age]@ %[is equal to]@ %@" : @"%1$[Alter]@ und %3$@ %2$[sind gleich]@", + @"%[age]@ is equal to %@" : @"%1$[Alter]@ und %3$@ %2$[sind gleich]@", + @"Add row" : @"Regel hinzufügen", + @"Delete row" : @"Regel löschen", + @"Add compound row" : @"Regelgruppe hinzufügen" + }]; + + // Initialize with Spanish by default + [[ruleEditor standardLocalizer] setDictionary:spanishDict]; + + [contentView addSubview:ruleEditor]; + + // Populate initial rows + [ruleEditor addRow:self]; + [ruleEditor addRow:self]; + [ruleEditor addRow:self]; + + // Display Output Label + var predLabel = [CPTextField labelWithTitle:@"Evaluated Predicate:"]; + [predLabel setFrame:CGRectMake(20, 320, 760, 20)]; + [predLabel setFont:[CPFont boldSystemFontOfSize:12]]; + [contentView addSubview:predLabel]; + + // Predicate string value display + predicateField = [[CPTextField alloc] initWithFrame:CGRectMake(20, 345, 760, 36)]; + [predicateField setAutoresizingMask:CPViewWidthSizable]; + [predicateField setBezeled:YES]; + [predicateField setEditable:NO]; + [predicateField setStringValue:@""]; + [predicateField setFont:[CPFont systemFontOfSize:13]]; + [contentView addSubview:predicateField]; + + [[CPNotificationCenter defaultCenter] addObserver:self + selector:@selector(ruleEditorRowsDidChange:) + name:CPRuleEditorRowsDidChangeNotification + object:ruleEditor]; + + [theWindow orderFront:self]; + [self ruleEditorAction:nil]; +} + +- (void)changeLanguage:(id)sender +{ + var selectedTitle = [sender titleOfSelectedItem], + targetDict = englishDict; + + if ([selectedTitle isEqualToString:@"Spanish"]) + { + targetDict = spanishDict; + } + else if ([selectedTitle isEqualToString:@"German"]) + { + targetDict = germanDict; + } + + [[ruleEditor standardLocalizer] setDictionary:targetDict]; + + // Post notification to trigger localized redraw across editor slices + [[CPNotificationCenter defaultCenter] postNotificationName:@"_CPRuleEditorLocalizerDidLoadNotification" object:[ruleEditor standardLocalizer]]; +} + +- (void)ruleEditorAction:(id)sender +{ + var predicate = [ruleEditor predicate]; + if (predicate) + { + [predicateField setStringValue:[predicate predicateFormat]]; + } + else + { + [predicateField setStringValue:@"(No predicate evaluated)"]; + } +} + +- (void)ruleEditorRowsDidChange:(CPNotification)note +{ + var predicate = [ruleEditor predicate]; + + if (predicate) + { + [predicateField setStringValue:[predicate predicateFormat]]; + } + else + { + [predicateField setStringValue:@"(Incomplete Predicate)"]; + } +} + +@end 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/spinner.gif b/Tests/Manual/CPRuleEditorTestSpanish/Resources/spinner.gif new file mode 100644 index 000000000..a5e705f6c Binary files /dev/null and b/Tests/Manual/CPRuleEditorTestSpanish/Resources/spinner.gif differ diff --git a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j new file mode 100644 index 000000000..3835b5e7f --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j @@ -0,0 +1,135 @@ +@import +@import +@import + +// Ensure the standard operator constants are explicitly defined +var CPEqualToPredicateOperatorType = 4, + CPContainsPredicateOperatorType = 99; + +@implementation RuleDelegate : CPObject +{ +} + +// 1. Root criteria and children +- (id)ruleEditor:(CPRuleEditor)editor child:(CPInteger)index forCriterion:(id)criterion withRowType:(CPRuleEditorRowType)rowType +{ + if (criterion == nil) + { + return [@[@"firstName", @"lastName", @"age"] objectAtIndex:index]; + } + + if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) + { + return [@[@"is equal to", @"contains"] objectAtIndex:index]; + } + + if ([criterion isEqualToString:@"age"]) + { + return [@[@"is equal to"] objectAtIndex:index]; + } + + if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) + { + return @"value"; + } + + return nil; +} + +// 2. Number of children +- (CPInteger)ruleEditor:(CPRuleEditor)editor numberOfChildrenForCriterion:(id)criterion withRowType:(CPRuleEditorRowType)rowType +{ + if (criterion == nil) + { + return 3; + } + + if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) + { + return 2; + } + + if ([criterion isEqualToString:@"age"]) + { + return 1; + } + + if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) + { + return 1; + } + + return 0; +} + +// 3. Display values +- (id)ruleEditor:(CPRuleEditor)editor displayValueForCriterion:(id)criterion inRow:(CPInteger)row +{ + if ([criterion isEqualToString:@"firstName"]) return @"firstName"; + if ([criterion isEqualToString:@"lastName"]) return @"lastName"; + if ([criterion isEqualToString:@"age"]) return @"age"; + + if ([criterion isEqualToString:@"contains"]) return @"contains"; + if ([criterion isEqualToString:@"is equal to"]) return @"is equal to"; + + if ([criterion isEqualToString:@"value"]) + { + var textField = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 120, 24)]; + [textField setBezeled:YES]; + [textField setBezelStyle:CPTextFieldSquareBezel]; + [textField setEditable:YES]; + [textField setStringValue:@""]; + return textField; + } + + return nil; +} + +// 4. Predicate parts +- (CPDictionary)ruleEditor:(CPRuleEditor)editor predicatePartsForCriterion:(id)criterion withDisplayValue:(id)value inRow:(CPInteger)row +{ + var parts = @{}; + + if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"] || [criterion isEqualToString:@"age"]) + { + [parts setObject:[CPExpression expressionForKeyPath:criterion] forKey:CPRuleEditorPredicateLeftExpression]; + } + else if ([criterion isEqualToString:@"contains"]) + { + [parts setObject:[CPNumber numberWithUnsignedInt:CPContainsPredicateOperatorType] forKey:CPRuleEditorPredicateOperatorType]; + } + else if ([criterion isEqualToString:@"is equal to"]) + { + [parts setObject:[CPNumber numberWithUnsignedInt:CPEqualToPredicateOperatorType] forKey:CPRuleEditorPredicateOperatorType]; + } + else if ([criterion isEqualToString:@"value"]) + { + var activeValue = value; + var slices = [editor valueForKey:@"_slices"]; + + if (slices && row < [slices count]) + { + var slice = [slices objectAtIndex:row]; + var optionViews = [slice valueForKey:@"_ruleOptionViews"]; + if (optionViews) + { + var count = [optionViews count]; + for (var i = 0; i < count; i++) + { + var view = [optionViews objectAtIndex:i]; + if ([view isKindOfClass:[CPTextField class]] && [view isEditable]) + { + activeValue = view; + break; + } + } + } + } + + [parts setObject:[CPExpression expressionForConstantValue:[activeValue stringValue]] forKey:CPRuleEditorPredicateRightExpression]; + } + + return parts; +} + +@end 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); +}