diff --git a/AppKit/CPRadio.j b/AppKit/CPRadio.j index f0c725a7e..a2b6f51d9 100644 --- a/AppKit/CPRadio.j +++ b/AppKit/CPRadio.j @@ -67,6 +67,18 @@ CPRadioImageOffset = 4.0; [[button1 radioGroup] selectedRadio] returns the currently selected option. + UPDATE 09/2020 : Implementation of modern Cocoa behavior : + + As in Cocoa, radio buttons grouping is now automatic. + + To be associated in a common group (and so being mutually exclusive), + radio buttons must combine 2 criteria : + + - same superview (enclosing view) + - same action + + TODO: This first implementation uses "as is" CPRadioGroup. This could + be simplified (no more need for radio group action, for example) */ @implementation CPRadio : CPButton { @@ -160,6 +172,63 @@ CPRadioImageOffset = 4.0; [CPApp sendAction:[_radioGroup action] to:[_radioGroup target] from:_radioGroup]; } +- (void)viewDidMoveToSuperview +{ + [self _setRadioGroup]; + [super viewDidMoveToSuperview]; +} + +- (void)setAction:(SEL)anAction +{ + if (anAction === _action) + return; + + [super setAction:anAction]; + [self _setRadioGroup]; +} + +#pragma mark Private methods + +- (void)_setRadioGroup +{ + // Implementation of modern Cocoa behavior : automatic radio group + + // If no action is set or no superview, no grouping can be done. + if (![self action] || ![self superview]) + { + // If I'm in a group (size > 1), remove me. + if ([[self radioGroup] size] > 1) + [self setRadioGroup:[CPRadioGroup new]]; + + return; + } + + // Search in superview subviews for other radio buttons having the same action. + // Take the one with the radio group having the greatest number of members. + + var radioGroup; + + for (var i = 0, superviewSubviews = [[self superview] subviews], count = [superviewSubviews count], aSubview, myAction = [self action], radioGroupSize = -1; (i < count); i++) + { + aSubview = superviewSubviews[i]; + + if ([aSubview isKindOfClass:CPRadio] && (aSubview !== self) && ([aSubview action] === myAction) && ([[aSubview radioGroup] size] > radioGroupSize)) + { + radioGroup = [aSubview radioGroup]; + radioGroupSize = [radioGroup size]; + } + } + + if (radioGroup) + [self setRadioGroup:radioGroup]; + else + // No other radio buttons to group with found. + // It may be because this radio button was in a radio group and its action was changed. + // If this is the case, we must reisolate it in a new radio group. + if ([_radioGroup size] > 1) + [self setRadioGroup:[CPRadioGroup new]]; +} + @end var CPRadioRadioGroupKey = @"CPRadioRadioGroupKey"; @@ -320,6 +389,11 @@ var CPRadioRadioGroupKey = @"CPRadioRadioGroupKey"; return _radios; } +- (int)size +{ + return [_radios count]; +} + - (void)setEnabled:(BOOL)enabled { [_radios makeObjectsPerformSelector:@selector(setEnabled:) withObject:enabled]; diff --git a/Tests/AppKit/CPButtonTest.j b/Tests/AppKit/CPButtonTest.j index 36efd1247..8a0f92e78 100644 --- a/Tests/AppKit/CPButtonTest.j +++ b/Tests/AppKit/CPButtonTest.j @@ -190,6 +190,69 @@ [self assertTrue:wasClicked message:@"a user click on a radio button should fire the group action"]; } +- (void)testAutomaticRadioGroup +{ + var radioButton1 = [CPRadio radioWithTitle:@"Radio 1"], + radioButton2 = [CPRadio radioWithTitle:@"Radio 2"], + radioButton3 = [CPRadio radioWithTitle:@"Radio 3"], + simpleView1 = [[CPView alloc] initWithFrame:CGRectMakeZero()], + simpleView2 = [[CPView alloc] initWithFrame:CGRectMakeZero()]; + + // Initially, buttons are isolated + [self assertFalse:([radioButton1 radioGroup] === [radioButton2 radioGroup]) message:@"initially, buttons should be isolated"]; + + [simpleView1 addSubview:radioButton1]; + [simpleView1 addSubview:radioButton2]; + + // As no actions are defined, buttons are still isolated + [self assertFalse:([radioButton1 radioGroup] === [radioButton2 radioGroup]) message:@"no actions defined, buttons should be isolated"]; + + [radioButton1 setAction:@selector(dummyAction1:)]; + [radioButton2 setAction:@selector(dummyAction2:)]; + + // As different actions are defined, buttons are still isolatdd + [self assertFalse:([radioButton1 radioGroup] === [radioButton2 radioGroup]) message:@"different actions defined, buttons should be isolated"]; + + [radioButton2 setAction:@selector(dummyAction1:)]; + + // As the same action is defined, buttons must be grouped + [self assertTrue:([radioButton1 radioGroup] === [radioButton2 radioGroup]) message:@"same action defined, buttons should be grouped"]; + + [radioButton3 setAction:@selector(dummyAction1:)]; + + // As radioButton3 is not inserted in a view, it's isolated + [self assertTrue:([[radioButton3 radioGroup] size] === 1) message:@"not in a view, button should be isolated"]; + + [simpleView2 addSubview:radioButton3]; + + // As radioButton3 is in another view, it's isolated from radioButton1 & 2 + [self assertTrue:([[radioButton3 radioGroup] size] === 1) message:@"alone in a view, button should be isolated"]; + + [simpleView1 addSubview:radioButton3]; + + // As all 3 buttons are in the same view, with the same action, they are grouped + [self assertTrue:([[radioButton3 radioGroup] size] === 3) message:@"after moving to the same view, buttons should be grouped"]; + + [radioButton3 setAction:@selector(dummyAction2:)]; + + // As the action of button 3 is now different, it's isolated + [self assertTrue:([[radioButton3 radioGroup] size] === 1) message:@"after changing the action, button 3 should be isolated"]; + + // And buttons 1 & 2 are still grouped + [self assertTrue:([radioButton1 radioGroup] === [radioButton2 radioGroup]) message:@"buttons 1 & 2 should remain grouped"]; + [self assertTrue:([[radioButton1 radioGroup] size] === 2) message:@"radio group should contain only buttons 1 & 2"]; +} + +- (IBAction)dummyAction1:(id)sender +{ + +} + +- (IBAction)dummyAction2:(id)sender +{ + +} + - (void)testTypeMasks { button = [[CPButton alloc] initWithFrame:CGRectMakeZero()];