Enable a button based on a value in the NSTableview - objective-c

I have a NSTableview. I need to enable the button based on a value of a column in the tableview. For instance, In the table view i have a column, Status. I have 2 kinds of status, Withdrawn and Booked. If i click on a row which has the status as Withdrawn, i need to disable the withdraw button.
Can i be able to do it through binding? How could i do it? Pls help me out. Thanks.

Provided you create a custom NSValueTransformer, you can enable or disable the button using bindings.
You can bind the Enabled property of the button as follows:
Bind to: arrayController
Controller Key: selection
Model Key Path: status
Value Transformer: MDStatusValueTransformer
NOTE: in place of arrayController, you should select whatever the name of your array controller is in the nib file. In place of MDStatusValueTransformer, you should specify whatever class name you end up naming the class I've provided below.
As I mentioned, you'll need to create a custom NSValueTransformer. The enabled property expects a BOOL wrapped in an NSNumber, but your status property is an NSString. So, you'll create a custom NSValueTransformer that will examine the incoming status NSString, and return NO if status is equal to #"Withdrawn".
The custom NSValueTransformer should look something like this:
MDStatusValueTransformer.h:
#interface MDStatusValueTransformer : NSValueTransformer
#end
MDStatusValueTransformer.m:
#implementation MDStatusValueTransformer
+ (Class)transformedValueClass {
return [NSNumber class];
}
+ (BOOL)allowsReverseTransformation {
return NO;
}
- (id)transformedValue:(id)value {
if (value == nil) return nil;
if (![value isKindOfClass:[NSString class]]) return nil;
if ([value isEqualToString:#"Withdrawn"]) {
return [NSNumber numberWithBool:NO];
}
return [NSNumber numberWithBool:YES];
}
#end

Related

Cocoa Bindings - NSTableView - Swapping Values

Is an NSValueTransform subclass a good choice for displaying Core Data attributes into UI views displaying:
A number string like (0,1,2,3,etc) into a string such as (Pending, Completed, Frozen, In progress, etc)
A number string like (0,1) into a app-based image (red.png if 0, green.png if 1)
Here's what Core Data displays for the two attributes, timer and status:
Here is what I want to be displayed instead, without changing the values in Core Data:
If not to use NSValueTransformer, in what other way is this possible?
I do not want to see the data permanently converted, only for the benefit of less data stored in Core Data and better UI view items.
I have also tried to modify the attributes in the managed object class (with out KVO notification) with no luck.
Yes, NSValueTransformer subclasses work just fine for this purpose.
You can also add read-only computed properties to your managed object class, and that should work, too. Those properties can even be added by a category in the controller code, if they don't make sense as part of the model code.
For example:
+ (NSSet*) keyPathsForValuesAffectingStatusDisplayName
{
return [NSSet setWithObject:#"status"];
}
- (NSString*) statusDisplayName
{
NSString* status = self.status;
if ([status isEqualToString:#"0"])
return #"Pending";
else if ([status isEqualToString:#"1"])
return #"Completed";
// ...
}
The +keyPathsForValuesAffectingStatusDisplayName method lets KVO and bindings know that when status changes, so does this new statusDisplayName property. See the docs for +keyPathsForValuesAffectingValueForKey: to learn how that works.
I ended up using what at first appeared to be blocking the display of different info in those cells, using:
#pragma mark - Table View Delegate
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
/* tableColumn = (string) #"AutomaticTableColumnIdentifier.0"
row = (int) 0 */
NSString *identifier = [tableColumn identifier];
NSTableCellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self];
NSManagedObject *item = [self.itemArrayController.arrangedObjects objectAtIndex:row];
if ([identifier isEqualToString:#"AutomaticTableColumnIdentifier.0"]) {
/* subviews returns array with 0 = Image View &
1 = Text Field */
/* First, set the correct timer image */
... logic ...
NSImageView *theImage = (NSImageView *)[[cellView subviews] objectAtIndex:0];
theImage.image = [NSImage imageNamed:#"green.gif"];
/* Second, display the desired status */
NSTextField *theTextField = (NSTextField *)[[result subviews] objectAtIndex:1];
... logic ...
theTextField.stringValue = #"Pending";
}
return cellView;
}
Apple's documentation states (somewhere) that bindings with an Array Controller can work in combination with manually populating the table view cells. It seems best and easiest to start with bindings and then refine display values manually.

Subclassing NSPopUpButton to add a bindable property

I'm trying to add a bindable property to a custom NSPopUpButton subclass.
I've created a "selectedKey" property, which is meant to store a NSString associated with selected menu item.
In control init, I set self as button target and an action for the button (valueChanged:), which in turn sets "selectedKey" in accordance with user selection:
#interface MyPopUpButton : NSPopUpButton {
NSMutableDictionary *_items;
NSString *_selectedKey;
}
#property(nonatomic, readwrite, copy) NSString* selectedKey;
- (void)addItemWithTitle:(NSString *)title andKey:(NSString *)key;
#end
#implementation MyPopUpButton
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
_items = [NSMutableDictionary new];
[NSObject exposeBinding:#"selectedKey"];
[super setTarget:self];
[super setAction:#selector(valueChanged:)];
}
return self;
}
- (void)addItemWithTitle:(NSString *)title andKey:(NSString *)key {
[super addItemWithTitle:title];
[_items setValue:title forKey:key];
}
- (void)valueChanged:(id)sender {
for (NSString *aKey in [_items allKeys]) {
if ([[_items valueForKey:aKey] isEqualToString:[self titleOfSelectedItem]]) {
self.selectedKey = aKey;
}
}
}
- (void)setSelectedKey:(NSString *)selectedKey {
[self willChangeValueForKey:#"selectedKey"];
_selectedKey = selectedKey;
[self didChangeValueForKey:#"selectedKey"];
[self selectItemWithTitle:[_items valueForKey:selectedKey]];
}
#end
This seems to work as expected: "selectedKey" property is changed when user changes PopUpButton selection.
Unfortunately, trying to bind this property, doesn't work.
[selectButton bind:#"selectedKey" toObject:savingDictionary withKeyPath:key options:#{NSContinuouslyUpdatesValueBindingOption : #YES }]
When selection is changed bind object is not updated accordingly.
What am I doing wrong?
I've created a "selectedKey" property, which is meant to store an NSString associated with selected menu item.
Bindings is definitely the way to go here, but your use of bind:toObject:withKeyPath:options is incorrect.
The value that you pass to the first argument must be one of the predefined values made available by Apple for that particular control. For NSPopUpButton objects, the available values are documented in the NSPopUpButton Bindings Reference. When you look through this document you'll see that there is no selectedKey option. There is however a selectedValue which has the following description:
An NSString that specifies the title of the selected item in the NSPopUpButton.
Thus the correct way to set up the binding is as follows:
[self.btn bind:#"selectedValue"
toObject:self
withKeyPath:#"mySelectedString"
options:nil];
This is all you need to do: when the action selector is fired the property stored at the keyPath you passed in as the third argument will already have been updated. This means that you can (i) get rid of the setSelectedKey method entirely, (ii) remove exposeBinding line, and (iii) remove the code within valueChanged: - Cocoa has already done this bit.
The example below implements just two methods, but, if I've understood your intentions, they should be all you need:
- (void)awakeFromNib {
self.btn.target = self;
self.btn.action = #selector(popUpActivity:);
[self.btn bind:#"selectedValue"
toObject:self
withKeyPath:#"mySelectedString"
options:nil];
// I've added a couple of additional bindings here; they're
// not required, but I thought they'd be instructive.
[self.btn bind:#"content"
toObject:self
withKeyPath:#"myItems"
options:nil];
[self.btn bind:#"selectedIndex"
toObject:self
withKeyPath:#"mySelectedIndex"
options:nil];
// Now that you've set the bindings up, use them!
self.myItems = #[#"Snow", #"Falling", #"On", #"Cedars"];
self.mySelectedIndex = #3; // "Cedars" will be selected on startup
// no need to set value of mySelectedString, because it will be
// updated automatically by the selectedIndex binding.
NSLog("%#", self.mySelectedString) // -> "Cedars"
}
- (void)popUpActivity:(id)sender {
NSLog(#"value of <selectedIndex> -> %#", self.mySelectedIndex);
NSLog(#"value of <selectedString> -> %#", self.mySelectedString);
}
A final point worth making is that none of the above should be a part of an NSPopUpButton subclass. It looks like you can - and therefore should - do everything you need to do without a custom subclass of this control. In my demo-app the code above belongs to the ViewController class, you should try doing this also.

NSPopupButton Bindings with Value Transformer

I don't know if what I see with a popup button populated by bindings with a value transformer is the way it's supposed to be or not -- the unusual thing I'm seeing (at least with respect to what I've seen with value transformers and table views) is that the "value" parameter in the transformedValue: method is the whole array bound to the array controller, not the individual strings in the array. When I've done this with table views, the transformer is called once for each displayed row in the table, and the "value" parameter is whatever object is bound to that row and column, not the whole array that serves as the content array for the array controller.
I have a very simple app to test this. In the app delegate there is this:
+(void)initialize {
RDTransformer *transformer = [[RDTransformer alloc] init];
[NSValueTransformer setValueTransformer:transformer forName:#"testTransformer"];
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.theData = #[#{#"name":#"William", #"age":#"24"},#{#"name":#"Thomas", #"age":#"23"},#{#"name":#"Alexander", #"age":#"64"},#{#"name":#"James", #"age":#"47"}];
}
In the RDTransformer class is this:
+ (Class)transformedValueClass {
return [NSString class];
}
+(BOOL)allowsReverseTransformation {
return NO;
}
-(id)transformedValue:(id)value {
NSLog(#"%#",value);
return value;
}
In IB, I added an NSPopupButton to the window and an array controller to the objects list. The content array of the controller is bound to App Delegate.theData, and the Content Values of the popup button is bound to Array Controller.arrangedObjects.name with the value transformer, testTransformer.
When I run the program, the log from the transformedValue: method is this:
2012-09-19 20:31:39.975 PopupBindingWithTransformer[793:303] (
)
2012-09-19 20:31:40.019 PopupBindingWithTransformer[793:303] (
William,
Thomas,
Alexander,
James
)
This doesn't seem to be other people's experience from what I can see on SO. Is there something I'm doing wrong with either the bindings or the value transformer?
Unfortunately, this is how NSPopUpButton works. The problem is not limited to that control. If you try binding an NSArrayController.contentArray to another NSArrayControllers.arrangedObject.someProperty you will get the same problem. Here is a simple workaround that I use in all my value transformers, which makes them work with both tables and popups:
You can modify your value transformer in the following way:
-(id)transformedArrayValue:(NSArray*)array
{
NSMutableArray *result = [NSMutableArray array];
for (id value in array)
[result addObject:[self transformedValue:value]];
return result;
}
-(id)transformedValue:(id)value
{
if ([value isKindOfClass:[NSArray class]])
return [self transformedArrayValue:value];
// Do your normal-case transform...
return [value lowercaseString];
}
It's not perfect but it's easy to replicate. I actually put the transformedArrayValue: in a class category so that I don't need to copy it everywhere.

Objective-C bindings - Binding an enum to an NSPopupButton

I'm working on a project which would be ideally suit Cocoa bindings for the UI but I'm having an issue binding the value of an object property and can't find a suitable solution. The object is as follows:
typedef enum tagCSQuality {
kQualityBest = 0,
kQualityWorst = 1
} CSQuality;
#interface CSProfile : NSObject {
NSString *identifier;
NSString *name;
CSQuality quality;
}
In the XIB, I have an object controller whose content object is bound to a "currentSelection" property of the window controller which is an instance of the above object. I've then bound the name and identifier which all work as expected but I cannot see how I can bind the enums.
Ideally I would like an NSPopupButton to display "Best" and "Worst" and pick the correct enum value. I have updated the enum to have an explicit numeric value and I believe that I need a value transformer to convert the values but I'm stuck on exactly how this could be implemented.
Can anyone help me out or point me in the right direction?
Thanks,
J
You can use an NSValueTransformerfor this.
Since the enumeration values are integers only, they are encapsulated in an NSNumber object.
An valid transformer could look like the following.
+(Class)transformedValueClass {
return [NSString class];
}
-(id)transformedValue:(id)value {
CSQuality quality = [value intValue];
if (quality == kQualityBest)
return #"Best";
else if (quality == kQualityWorst)
return #"Worst";
return nil;
}
This can be bound to the Selected Value binding of the NSPopupButton.
If you want to create a bidirectional binding (i.e. be able to select something in the NSPopupButton you have to add the following code for the reverse transformation:
+(BOOL)allowsReverseTransformation {
return YES;
}
-(id)reverseTransformedValue:(id)value {
if ([#"Worst" isEqualToString:value])
return [NSNumber numberWithInt: kQualityWorst];
else if ([#"Best" isEqualToString:value])
return [NSNumber numberWithInt: kQualityBest];
return nil;
}
An enum is not an object. Cocoa bindings are a way to connect model objects to view objects.
If you are using Interface Builder, you can embed enum represented integer for each NSMenuItem items through property panel. Then select NSPopUpButton and specify binding 'selected tag' to the property with key path.
In this example, assume, IB's file owner is CSProfile. Prepare NSPopUpButton with two NSMenuItem items and tag them with 0(kQualityBest) and 1(kQualityWorst). Then navigate 'selected tag' of NSPopUpButton and check bind to 'File's owner'(CSProfile) with Model Key Path 'quality'.
#interface CSProfile : NSObject {
NSString *identifier;
NSString *name;
CSQuality quality;
}
#property (assign) CSQuality quality;

how to disable a button dynamically

How to disable a button after entering a particular letter in a textfield?
Bind the text field's value to one of your object's properties and ensure to check the "updates continuously" box in Interface Builder. For this example, the property will be called theText. Then, bind the enabled state of the button using a key-value path of say containsLetterA, then in your object put the method
- (BOOL) containsLetterA
{
NSRange rangeOfLetterA = [[self theText] rangeOfString:#"A"];
return rangeOfLetterA.location != NSNotFound;
}
Then, also in your object, add the class method:
+ (NSSet *) keyPathsForValuesAffectingValueForContainsLetterA
{
return [NSSet setWithObjects:#"theText", nil];
}