diff --git a/ChangeLog b/ChangeLog index 04f73a385b..c6de946c86 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2024-11-08 Gregory John Casamento + + * Headers/AppKit/NSBrowser.h: Add dictionary to hold relationships. + * Source/NSBrowser.m: Add logic in methods to support binfings, + expose bindings in +initialize. + * Source/NSTextFieldCell.m: Minor fix to NSTextField for displaying + binding values. + 2024-10-31 Richard Frith-Macdonald * Source/GSServicesManager.m: fix -laterDate: conditionals to be true diff --git a/Headers/AppKit/NSBrowser.h b/Headers/AppKit/NSBrowser.h index 06257b2178..3a622e80f0 100644 --- a/Headers/AppKit/NSBrowser.h +++ b/Headers/AppKit/NSBrowser.h @@ -37,6 +37,7 @@ @class NSArray; @class NSIndexPath; @class NSIndexSet; +@class NSMutableDictionary; @class NSCell; @class NSEvent; @@ -100,6 +101,7 @@ APPKIT_EXPORT_CLASS NSBrowserColumnResizingType _columnResizing; BOOL _itemBasedDelegate; + NSMutableDictionary *_columnDictionary; } // diff --git a/Source/NSBrowser.m b/Source/NSBrowser.m index ccdbd5113d..cce4641afa 100644 --- a/Source/NSBrowser.m +++ b/Source/NSBrowser.m @@ -16,7 +16,7 @@ Date: September 2002 Author: Gregory Casamento Date: July 2024 - Note: Added support for 10.6+ delegate methods. + Note: Added support for 10.6+ delegate methods. Added bindings support This file is part of the GNUstep GUI Library. @@ -56,10 +56,13 @@ #import "AppKit/NSColor.h" #import "AppKit/NSFont.h" #import "AppKit/NSGraphics.h" +#import "AppKit/NSKeyValueBinding.h" #import "AppKit/NSMatrix.h" #import "AppKit/NSScroller.h" #import "AppKit/NSScrollView.h" #import "AppKit/NSTableHeaderCell.h" +#import "AppKit/NSTreeController.h" +#import "AppKit/NSTreeNode.h" #import "AppKit/NSEvent.h" #import "AppKit/NSViewController.h" #import "AppKit/NSWindow.h" @@ -260,6 +263,10 @@ - (NSBorderType) _resolvedBorderType; - (void) _themeDidActivate: (NSNotification*)notification; @end +// Category to handle bindings +@interface NSBrowser (GSBindingsPrivate) +@end + // // NSBrowser implementation // @@ -2268,7 +2275,7 @@ - (void) setDelegate: (id)anObject _itemBasedDelegate = NO; if ([anObject respondsToSelector: - @selector(browser:numberOfChildrenOfItem:)] + @selector(browser:numberOfChildrenOfItem:)] && [anObject respondsToSelector: @selector(browser:child:ofItem:)] && [anObject respondsToSelector: @@ -2486,6 +2493,10 @@ + (void) initialize } [self _themeDidActivate: nil]; + + // Bindings... + [self exposeBinding: NSContentBinding]; + [self exposeBinding: NSContentValuesBinding]; } } @@ -2558,6 +2569,7 @@ - (id) initWithFrame: (NSRect)rect // Item based delegate, 10.6+ _itemBasedDelegate = NO; + _columnDictionary = [[NSMutableDictionary alloc] init]; [[NSNotificationCenter defaultCenter] addObserver: self @@ -2583,6 +2595,7 @@ - (void) dealloc RELEASE(_pathSeparator); RELEASE(_horizontalScroller); RELEASE(_browserColumns); + RELEASE(_columnDictionary); TEST_RELEASE(_charBuffer); [super dealloc]; @@ -3009,6 +3022,7 @@ - (id) initWithCoder: (NSCoder*)aDecoder // Item based delegate, 10.6+ _itemBasedDelegate = NO; + _columnDictionary = [[NSMutableDictionary alloc] init]; // Horizontal scroller _scrollerRect.origin.x = bs.width; @@ -3296,10 +3310,19 @@ - (void) _remapColumnSubviews: (BOOL)fromFirst - (id) _itemForColumn: (NSInteger)column { id item = nil; - + GSKeyValueBinding *theBinding; + theBinding = [GSKeyValueBinding getBinding: NSContentBinding + forObject: self]; if (column == 0) { - item = [_browserDelegate rootItemForBrowser: self]; + if (theBinding == nil) + { + item = [_browserDelegate rootItemForBrowser: self]; + } + else + { + item = nil; // [NSNull null]; + } } else { @@ -3319,7 +3342,20 @@ - (id) _itemForColumn: (NSInteger)column { id cell = [selectedCells objectAtIndex: 0]; - item = [cell objectValue]; + if (theBinding != nil) + { + NSNumber *colNum = [NSNumber numberWithInteger: col]; + NSArray *array = [_columnDictionary objectForKey: colNum]; + if ([array count] > 0) + { + NSInteger row = [self selectedRowInColumn: col]; + item = [array objectAtIndex: row]; + } + } + else + { + item = [cell objectValue]; + } } } } @@ -3328,22 +3364,104 @@ - (id) _itemForColumn: (NSInteger)column return item; } +- (NSString *) _keyPathForValueBinding +{ + NSString *keyPath = nil; + NSDictionary *info = [GSKeyValueBinding infoForBinding: NSContentValuesBinding + forObject: self]; + if (info != nil) + { + NSString *ikp = [info objectForKey: NSObservedKeyPathKey]; + NSUInteger location = [ikp rangeOfString: @"."].location; + + keyPath = (location == NSNotFound ? ikp : [ikp substringFromIndex: location + 1]); + } + + return keyPath; +} + /* Loads column 'column' (asking the delegate). */ - (void) _performLoadOfColumn: (NSInteger)column { - NSBrowserColumn *bc; - NSScrollView *sc; - NSMatrix *matrix; - NSInteger i, rows, cols; + NSBrowserColumn *bc = nil; + NSScrollView *sc = nil; + NSMatrix *matrix = nil; + NSInteger i = 0, rows = 0, cols = 0; id child = nil; id item = nil; + NSNumber *colNum = nil; + NSTreeController *tc = nil; + NSArray *children = nil; if (_itemBasedDelegate) { + GSKeyValueBinding *theBinding; + + theBinding = [GSKeyValueBinding getBinding: NSContentBinding + forObject: self]; + item = [self _itemForColumn: column]; + if (theBinding != nil) + { + id observedObject = [theBinding observedObject]; + + rows = 0; + colNum = [NSNumber numberWithInteger: column]; + if ([observedObject isKindOfClass: [NSTreeController class]]) + { + tc = (NSTreeController *)observedObject; + + if (item == nil) + { + NSTreeNode *node = (NSTreeNode *)[theBinding destinationValue]; + + if (node != nil) + { + /* Per the documentation 10.4/5+ uses NSTreeNode as the return value for + * the contents of this tree node consists of a dictionary with a single + * key of "children". This is per the tests for this at + * https://github.com/gcasa/NSTreeController_test. Specifically it returns + * _NSControllerTreeProxy. The equivalent of that class in GNUstep is + * GSControllerTreeProxy. + */ + children = [node mutableChildNodes]; + rows = [children count]; + item = node; + } + } + else + { + NSString *childrenKeyPath = [tc childrenKeyPathForNode: item]; - // Ask the delegate for the number of rows for a given item... - rows = [_browserDelegate browser: self numberOfChildrenOfItem: item]; + if (childrenKeyPath != nil) + { + NSString *countKeyPath = [tc countKeyPathForNode: item]; + + children = [item valueForKeyPath: childrenKeyPath]; + if (countKeyPath == nil) + { + rows = [children count]; // get the count directly... + } + else + { + NSNumber *countValue = [item valueForKeyPath: countKeyPath]; + rows = [countValue integerValue]; + } + } + } + + // If the node has children, add them to the column... + if (children != nil) + { + [_columnDictionary setObject: children forKey: colNum]; + } + } + } + else + { + // Ask the delegate for the number of rows for a given item... + rows = [_browserDelegate browser: self numberOfChildrenOfItem: item]; + } cols = 1; } else if (_passiveDelegate) @@ -3410,23 +3528,64 @@ - (void) _performLoadOfColumn: (NSInteger)column [sc setDocumentView: matrix]; // Loading is different based upon item/passive/active delegate - if (_itemBasedDelegate) + if (_itemBasedDelegate == YES) // && item != nil && tc != nil) { - // Iterate over the children for the item.... - for (i = 0; i < rows; i++) + NSString *childrenKeyPath = [tc childrenKeyPathForNode: item]; + + if (childrenKeyPath != nil) { - id aCell = [matrix cellAtRow: i column: 0]; - if (![aCell isLoaded]) + NSString *leafKeyPath = [tc leafKeyPathForNode: item]; + NSString *valueKeyPath = [self _keyPathForValueBinding]; + + // Iterate over the children for the item.... + for (i = 0; i < rows; i++) { - BOOL leaf = YES; - id val = nil; - - child = [_browserDelegate browser: self child: i ofItem: item]; - leaf = [_browserDelegate browser: self isLeafItem: child]; - val = [_browserDelegate browser: self objectValueForItem: child]; - [aCell setLeaf: leaf]; - [aCell setObjectValue: val]; - [aCell setLoaded: YES]; + id aCell = [matrix cellAtRow: i column: 0]; + if (![aCell isLoaded]) + { + BOOL leaf = YES; + id val = nil; + NSNumber *leafBool = nil; + + child = [children objectAtIndex: i]; + leafBool = [child valueForKeyPath: leafKeyPath]; + leaf = [leafBool boolValue]; + + // If a content values binding is present, it uses that key path, + // but if one isn't it uses the description... per documentation. + if (valueKeyPath != nil) + { + val = [child valueForKeyPath: valueKeyPath]; + } + else + { + val = [child description]; // per documentation. + } + + [aCell setLeaf: leaf]; + [aCell setObjectValue: val]; + [aCell setLoaded: YES]; + } + } + } + else + { + // Iterate over the children for the item.... + for (i = 0; i < rows; i++) + { + id aCell = [matrix cellAtRow: i column: 0]; + if (![aCell isLoaded]) + { + BOOL leaf = YES; + id val = nil; + + child = [_browserDelegate browser: self child: i ofItem: item]; + leaf = [_browserDelegate browser: self isLeafItem: child]; + val = [_browserDelegate browser: self objectValueForItem: child]; + [aCell setLeaf: leaf]; + [aCell setObjectValue: val]; + [aCell setLoaded: YES]; + } } } } @@ -3592,3 +3751,40 @@ - (void) _themeDidActivate: (NSNotification*)notification } @end + +@implementation NSBrowser (GSBindingsPrivate) + +/* Private methods to handle bindings */ + +- (void) setValue: (id)anObject forKey: (NSString*)aKey +{ + if ([aKey isEqual: NSContentBinding] + || [aKey isEqual: NSContentValuesBinding]) + { + // Reload data + _passiveDelegate = NO; + _itemBasedDelegate = YES; + + [self loadColumnZero]; + NSDebugLLog(@"NSBinding", @"Setting browser view content/values to %@", anObject); + } + else + { + [super setValue: anObject forKey: aKey]; + } +} + +- (id) valueForKey: (NSString*)aKey +{ + if ([aKey isEqual: NSContentBinding] + || [aKey isEqual: NSContentValuesBinding]) + { + return nil; + } + else + { + return [super valueForKey: aKey]; + } +} + +@end diff --git a/Source/NSTextFieldCell.m b/Source/NSTextFieldCell.m index 4d9cb7144c..34534b4367 100644 --- a/Source/NSTextFieldCell.m +++ b/Source/NSTextFieldCell.m @@ -8,7 +8,7 @@ Date: 1996 Author: Nicola Pero Date: November 1999 - + This file is part of the GNUstep GUI Library. This library is free software; you can redistribute it and/or @@ -23,10 +23,10 @@ You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. - If not, see or write to the - Free Software Foundation, 51 Franklin Street, Fifth Floor, + If not, see or write to the + Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -*/ +*/ #import "config.h" #import @@ -36,17 +36,21 @@ #import "AppKit/NSEvent.h" #import "AppKit/NSFont.h" #import "AppKit/NSGraphics.h" +#import "AppKit/NSKeyValueBinding.h" #import "AppKit/NSStringDrawing.h" #import "AppKit/NSTextField.h" #import "AppKit/NSTextFieldCell.h" #import "AppKit/NSText.h" +#import "GSBindingHelpers.h" + @implementation NSTextFieldCell + (void) initialize { if (self == [NSTextFieldCell class]) { + [self exposeBinding: NSValueBinding]; [self setVersion: 2]; } } @@ -87,7 +91,7 @@ - (id) copyWithZone: (NSZone*)zone } // -// Modifying Graphic Attributes +// Modifying Graphic Attributes // - (void) setBackgroundColor: (NSColor *)aColor { @@ -199,25 +203,25 @@ - (NSText *) setUpFieldEditorAttributes: (NSText *)textObject return textObject; } -- (void) _drawBackgroundWithFrame: (NSRect)cellFrame - inView: (NSView*)controlView +- (void) _drawBackgroundWithFrame: (NSRect)cellFrame + inView: (NSView*)controlView { if (_textfieldcell_draws_background) { if ([self isEnabled]) - { - [_background_color set]; - } + { + [_background_color set]; + } else - { - [[NSColor controlBackgroundColor] set]; - } + { + [[NSColor controlBackgroundColor] set]; + } NSRectFill([self drawingRectForBounds: cellFrame]); - } + } } -- (void) _drawBorderAndBackgroundWithFrame: (NSRect)cellFrame - inView: (NSView*)controlView +- (void) _drawBorderAndBackgroundWithFrame: (NSRect)cellFrame + inView: (NSView*)controlView { // FIXME: Should use the bezel style if set. [super _drawBorderAndBackgroundWithFrame: cellFrame inView: controlView]; @@ -233,16 +237,16 @@ - (void) drawInteriorWithFrame: (NSRect)cellFrame inView: (NSView*)controlView NSRect titleRect; /* Make sure we are a text cell; titleRect might return an incorrect - rectangle otherwise. Note that the type could be different if the - user has set an image on us, which we just ignore (OS X does so as - well). */ + rectangle otherwise. Note that the type could be different if the + user has set an image on us, which we just ignore (OS X does so as + well). */ _cell.type = NSTextCellType; titleRect = [self titleRectForBounds: cellFrame]; [[self _drawAttributedString] drawInRect: titleRect]; } } -/* +/* Attributed string that will be displayed. */ - (NSAttributedString*) _drawAttributedString @@ -254,31 +258,31 @@ - (NSAttributedString*) _drawAttributedString { attrStr = [self placeholderAttributedString]; if ((attrStr == nil) || ([[attrStr string] length] == 0)) - { - NSString *string; - NSDictionary *attributes; - NSMutableDictionary *newAttribs; - - string = [self placeholderString]; - if (string == nil) - { - return nil; - } - - attributes = [self _nonAutoreleasedTypingAttributes]; - newAttribs = [NSMutableDictionary - dictionaryWithDictionary: attributes]; - [newAttribs setObject: [NSColor disabledControlTextColor] - forKey: NSForegroundColorAttributeName]; - - return AUTORELEASE([[NSAttributedString alloc] - initWithString: string - attributes: newAttribs]); - } + { + NSString *string; + NSDictionary *attributes; + NSMutableDictionary *newAttribs; + + string = [self placeholderString]; + if (string == nil) + { + return nil; + } + + attributes = [self _nonAutoreleasedTypingAttributes]; + newAttribs = [NSMutableDictionary + dictionaryWithDictionary: attributes]; + [newAttribs setObject: [NSColor disabledControlTextColor] + forKey: NSForegroundColorAttributeName]; + + return AUTORELEASE([[NSAttributedString alloc] + initWithString: string + attributes: newAttribs]); + } else - { - return attrStr; - } + { + return attrStr; + } } else { @@ -296,12 +300,12 @@ - (void) _updateFieldEditor: (NSText*)textObject - (BOOL) isOpaque { - if (_textfieldcell_draws_background == NO - || _background_color == nil + if (_textfieldcell_draws_background == NO + || _background_color == nil || [_background_color alphaComponent] < 1.0) return NO; else - return YES; + return YES; } // @@ -319,9 +323,9 @@ - (void) encodeWithCoder: (NSCoder*)aCoder [aCoder encodeObject: [self textColor] forKey: @"NSTextColor"]; [aCoder encodeBool: [self drawsBackground] forKey: @"NSDrawsBackground"]; if ([self isBezeled]) - { - [aCoder encodeInt: [self bezelStyle] forKey: @"NSTextBezelStyle"]; - } + { + [aCoder encodeInt: [self bezelStyle] forKey: @"NSTextBezelStyle"]; + } } else { @@ -337,53 +341,53 @@ - (id) initWithCoder: (NSCoder*)aDecoder self = [super initWithCoder: aDecoder]; if (self == nil) return self; - + if ([aDecoder allowsKeyedCoding]) { if ([aDecoder containsValueForKey: @"NSBackgroundColor"]) - { - [self setBackgroundColor: [aDecoder decodeObjectForKey: - @"NSBackgroundColor"]]; - } + { + [self setBackgroundColor: [aDecoder decodeObjectForKey: + @"NSBackgroundColor"]]; + } if ([aDecoder containsValueForKey: @"NSTextColor"]) - { - [self setTextColor: [aDecoder decodeObjectForKey: @"NSTextColor"]]; - } + { + [self setTextColor: [aDecoder decodeObjectForKey: @"NSTextColor"]]; + } if ([aDecoder containsValueForKey: @"NSDrawsBackground"]) - { - [self setDrawsBackground: [aDecoder decodeBoolForKey: - @"NSDrawsBackground"]]; - } + { + [self setDrawsBackground: [aDecoder decodeBoolForKey: + @"NSDrawsBackground"]]; + } if ([aDecoder containsValueForKey: @"NSTextBezelStyle"]) - { - [self setBezelStyle: [aDecoder decodeIntForKey: - @"NSTextBezelStyle"]]; - } + { + [self setBezelStyle: [aDecoder decodeIntForKey: + @"NSTextBezelStyle"]]; + } if ([aDecoder containsValueForKey: @"NSPlaceholderString"]) - { - [self setPlaceholderString: [aDecoder decodeObjectForKey: - @"NSPlaceholderString"]]; - } + { + [self setPlaceholderString: [aDecoder decodeObjectForKey: + @"NSPlaceholderString"]]; + } } else { BOOL tmp; if ([aDecoder versionForClassName:@"NSTextFieldCell"] < 2) - { - /* Replace the old default _action_mask with the new default one - if it's set. There isn't really a way to modify this value - on an NSTextFieldCell encoded in a .gorm file. The old default value - causes problems with newer NSTableViews which uses this to discern - whether it should trackMouse:inRect:ofView:untilMouseUp: or not. - This also disables the action from being sent on an uneditable and - unselectable text fields. - */ - if (_action_mask == NSLeftMouseUpMask) - { - _action_mask = NSKeyUpMask | NSKeyDownMask; - } - } + { + /* Replace the old default _action_mask with the new default one + if it's set. There isn't really a way to modify this value + on an NSTextFieldCell encoded in a .gorm file. The old default value + causes problems with newer NSTableViews which uses this to discern + whether it should trackMouse:inRect:ofView:untilMouseUp: or not. + This also disables the action from being sent on an uneditable and + unselectable text fields. + */ + if (_action_mask == NSLeftMouseUpMask) + { + _action_mask = NSKeyUpMask | NSKeyDownMask; + } + } [aDecoder decodeValueOfObjCType: @encode(id) at: &_background_color]; [aDecoder decodeValueOfObjCType: @encode(id) at: &_text_color];