1{%MainUnit ../extctrls.pas}
2{******************************************************************************
3                                TCustomRadioGroup
4 ******************************************************************************
5
6 *****************************************************************************
7  This file is part of the Lazarus Component Library (LCL)
8
9  See the file COPYING.modifiedLGPL.txt, included in this distribution,
10  for details about the license.
11 *****************************************************************************
12
13  Delphi compatibility:
14
15   - the interface is almost like in delphi 5
16}
17
18
19type
20
21  { TRadioGroupStringList }
22
23  TRadioGroupStringList = class(TStringList)
24  private
25    FRadioGroup: TCustomRadioGroup;
26  protected
27    procedure Changed; override;
28  public
29    constructor Create(TheRadioGroup: TCustomRadioGroup);
30    procedure Assign(Source: TPersistent); override;
31  end;
32
33{ TRadioGroupStringList }
34
35procedure TRadioGroupStringList.Changed;
36begin
37  inherited Changed;
38  if (UpdateCount = 0) then
39    FRadioGroup.UpdateAll
40  else
41    FRadioGroup.UpdateInternalObjectList;
42  FRadioGroup.FLastClickedItemIndex := FRadioGroup.FItemIndex;
43end;
44
45constructor TRadioGroupStringList.Create(TheRadioGroup: TCustomRadioGroup);
46begin
47  inherited Create;
48  FRadioGroup := TheRadioGroup;
49end;
50
51procedure TRadioGroupStringList.Assign(Source: TPersistent);
52var
53  SavedIndex: Integer;
54begin
55  SavedIndex := FRadioGroup.ItemIndex;
56  inherited Assign(Source);
57  if SavedIndex < Count then FRadioGroup.ItemIndex := SavedIndex;
58end;
59
60
61{------------------------------------------------------------------------------
62  Method: TCustomRadioGroup.Create
63  Params:  TheOwner: the owner of the class
64  Returns: Nothing
65
66  Constructor for the radiogroup
67 ------------------------------------------------------------------------------}
68constructor TCustomRadioGroup.Create(TheOwner : TComponent);
69begin
70  inherited Create (TheOwner);
71  ControlStyle := ControlStyle + [csCaptureMouse, csClickEvents, csSetCaption,
72                                  csDoubleClicks];
73  FItems := TRadioGroupStringList.Create(Self);
74  FAutoFill := true;
75  FItemIndex  := -1;
76  FLastClickedItemIndex := -1;
77  FButtonList := TFPList.Create;
78  FColumns  := 1;
79  FColumnLayout := clHorizontalThenVertical;
80  ChildSizing.Layout:=cclLeftToRightThenTopToBottom;
81  ChildSizing.ControlsPerLine:=FColumns;
82  ChildSizing.ShrinkHorizontal:=crsScaleChilds;
83  ChildSizing.ShrinkVertical:=crsScaleChilds;
84  ChildSizing.EnlargeHorizontal:=crsHomogenousChildResize;
85  ChildSizing.EnlargeVertical:=crsHomogenousChildResize;
86  ChildSizing.LeftRightSpacing:=6;
87  ChildSizing.TopBottomSpacing:=0;
88end;
89
90
91{------------------------------------------------------------------------------
92  Method: TCustomRadioGroup.Destroy
93  Params:  none
94  Returns: Nothing
95
96  Destructor for the radiogroup
97 ------------------------------------------------------------------------------}
98destructor TCustomRadioGroup.Destroy;
99begin
100  FreeAndNil(FItems);
101  FreeAndNil(FButtonList);
102  FreeAndNil(FHiddenButton);
103  inherited Destroy;
104end;
105
106{------------------------------------------------------------------------------
107  Method: TCustomRadioGroup.InitializeWnd
108  Params:  none
109  Returns: Nothing
110
111  Create the visual component of the Radiogroup.
112 ------------------------------------------------------------------------------}
113procedure TCustomRadioGroup.InitializeWnd;
114
115  procedure RealizeItemIndex;
116  var
117    i: Integer;
118  begin
119    if (FItemIndex <> -1) and (FItemIndex<FButtonList.Count) then
120      TRadioButton(FButtonList[FItemIndex]).Checked := true
121    else if FHiddenButton<>nil then
122      FHiddenButton.Checked:=true;
123    for i:=0 to FItems.Count-1 do begin
124      TRadioButton(FButtonList[i]).Checked := fItemIndex = i;
125    end;
126  end;
127
128begin
129  if FCreatingWnd then RaiseGDBException('TCustomRadioGroup.InitializeWnd');
130  FCreatingWnd := true;
131  //DebugLn(['[TCustomRadioGroup.InitializeWnd] A ',DbgSName(Self),' FItems.Count=',FItems.Count,' HandleAllocated=',HandleAllocated,' ItemIndex=',ItemIndex]);
132  UpdateItems;
133  inherited InitializeWnd;
134  RealizeItemIndex;
135  //debugln(['TCustomRadioGroup.InitializeWnd END']);
136  FCreatingWnd := false;
137end;
138
139function TCustomRadioGroup.Rows: integer;
140begin
141  if FItems.Count>0 then
142    Result:=((FItems.Count-1) div Columns)+1
143  else
144    Result:=0;
145end;
146
147procedure TCustomRadioGroup.ItemEnter(Sender: TObject);
148begin
149  DoEnter;
150end;
151
152procedure TCustomRadioGroup.ItemExit(Sender: TObject);
153begin
154  DoExit;
155end;
156
157procedure TCustomRadioGroup.ItemResize(Sender: TObject);
158begin
159
160end;
161
162procedure TCustomRadioGroup.UpdateItems;
163var
164  i: integer;
165  ARadioButton: TRadioButton;
166begin
167  if FUpdatingItems then exit;
168  FUpdatingItems:=true;
169  try
170    // destroy radiobuttons, if there are too many
171    while FButtonList.Count>FItems.Count do
172    begin
173      TRadioButton(FButtonList[FButtonList.Count-1]).Free;
174      FButtonList.Delete(FButtonList.Count-1);
175    end;
176
177    // create as many TRadioButton as needed
178    while (FButtonList.Count<FItems.Count) do
179    begin
180      ARadioButton := TRadioButton.Create(Self);
181      with ARadioButton do
182      begin
183        Name := 'RadioButton'+IntToStr(FButtonList.Count);
184        OnClick := @Self.Clicked;
185        OnChange := @Self.Changed;
186        OnEnter := @Self.ItemEnter;
187        OnExit := @Self.ItemExit;
188        OnKeyDown := @Self.ItemKeyDown;
189        OnKeyUp := @Self.ItemKeyUp;
190        OnKeyPress := @Self.ItemKeyPress;
191        OnUTF8KeyPress := @Self.ItemUTF8KeyPress;
192        OnResize := @Self.ItemResize;
193        ParentFont := True;
194        BorderSpacing.CellAlignHorizontal := ccaLeftTop;
195        BorderSpacing.CellAlignVertical := ccaCenter;
196        ControlStyle := ControlStyle + [csNoDesignSelectable];
197      end;
198      FButtonList.Add(ARadioButton);
199    end;
200    if FHiddenButton=nil then begin
201      FHiddenButton:=TRadioButton.Create(nil);
202      with FHiddenButton do
203      begin
204        Name := 'HiddenRadioButton';
205        Visible := False;
206        ControlStyle := ControlStyle + [csNoDesignSelectable, csNoDesignVisible];
207      end;
208    end;
209
210    if (FItemIndex>=FItems.Count) and not (csLoading in ComponentState) then FItemIndex:=FItems.Count-1;
211
212    if FItems.Count>0 then
213    begin
214      // to reduce overhead do it in several steps
215
216      // assign Caption and then Parent
217      for i:=0 to FItems.Count-1 do
218      begin
219        ARadioButton := TRadioButton(FButtonList[i]);
220        ARadioButton.Caption := FItems[i];
221        ARadioButton.Parent := Self;
222      end;
223      FHiddenButton.Parent:=Self;
224
225      // the checked and unchecked states can be applied only after all other
226      for i := 0 to FItems.Count-1 do
227      begin
228        ARadioButton := TRadioButton(FButtonList[i]);
229        ARadioButton.Checked := (i = FItemIndex);
230        ARadioButton.Visible := true;
231      end;
232      //FHiddenButton must remain the last item in Controls[], so that Controls[] is in sync with Items[]
233      Self.RemoveControl(FHiddenButton);
234      Self.InsertControl(FHiddenButton);
235      if HandleAllocated then
236        FHiddenButton.HandleNeeded;
237      FHiddenButton.Checked := (FItemIndex = -1);
238      UpdateTabStops;
239    end;
240  finally
241    FUpdatingItems:=false;
242  end;
243end;
244
245procedure TCustomRadioGroup.UpdateControlsPerLine;
246var
247  NewControlsPerLine: LongInt;
248begin
249  if ChildSizing.Layout=cclLeftToRightThenTopToBottom then
250    NewControlsPerLine:=Max(1,FColumns)
251  else
252    NewControlsPerLine:=Max(1,Rows);
253  ChildSizing.ControlsPerLine:=NewControlsPerLine;
254  //DebugLn('TCustomRadioGroup.UpdateControlsPerLine ',dbgs(ChildSizing.ControlsPerLine),' ',dbgs(NewControlsPerLine),' FColumns=',dbgs(FColumns),' FItems.Count=',dbgs(FItems.Count),' ',dbgs(ChildSizing.Layout=cclLeftToRightThenTopToBottom));
255end;
256
257procedure TCustomRadioGroup.ItemKeyDown(Sender: TObject; var Key: Word;
258  Shift: TShiftState);
259
260  procedure MoveSelection(HorzDiff, VertDiff: integer);
261  var
262    Count: integer;
263    StepSize: integer;
264    BlockSize : integer;
265    NewIndex : integer;
266    WrapOffset: integer;
267  begin
268    Count := FButtonList.Count;
269    if FColumnLayout=clHorizontalThenVertical then begin
270      //add a row for ease wrapping
271      BlockSize := Columns * (Rows+1);
272      StepSize := HorzDiff + VertDiff * Columns;
273      WrapOffSet := VertDiff;
274    end
275    else begin
276      //add a column for ease wrapping
277      BlockSize := (Columns+1) * Rows;
278      StepSize := HorzDiff * Rows + VertDiff;
279      WrapOffSet := HorzDiff;
280    end;
281    NewIndex := ItemIndex;
282    repeat
283      Inc(NewIndex, StepSize);
284      if (NewIndex >= Count) or (NewIndex < 0) then begin
285        NewIndex := (NewIndex + WrapOffSet + BlockSize) mod BlockSize;
286        // Keep moving in the same direction until in valid range
287        while NewIndex >= Count do
288           NewIndex := (NewIndex + StepSize) mod BlockSize;
289      end;
290    until (NewIndex = ItemIndex) or TRadioButton(FButtonList[NewIndex]).Enabled;
291    ItemIndex := NewIndex;
292    TRadioButton(FButtonList[ItemIndex]).SetFocus;
293    Key := 0;
294  end;
295
296begin
297  if Shift=[] then begin
298    case Key of
299      VK_LEFT: MoveSelection(-1,0);
300      VK_RIGHT: MoveSelection(1,0);
301      VK_UP: MoveSelection(0,-1);
302      VK_DOWN: MoveSelection(0,1);
303    end;
304  end;
305  if Key <> 0 then
306    KeyDown(Key, Shift);
307end;
308
309procedure TCustomRadioGroup.ItemKeyUp(Sender: TObject; var Key: Word;
310  Shift: TShiftState);
311begin
312  if Key <> 0 then
313    KeyUp(Key, Shift);
314end;
315
316procedure TCustomRadioGroup.ItemKeyPress(Sender: TObject; var Key: Char);
317begin
318  if Key <> #0 then
319    KeyPress(Key);
320end;
321
322procedure TCustomRadioGroup.ItemUTF8KeyPress(Sender: TObject;
323  var UTF8Key: TUTF8Char);
324begin
325  UTF8KeyPress(UTF8Key);
326end;
327
328{------------------------------------------------------------------------------
329  Method: TCustomRadioGroup.SetColumns
330  Params:  value - no of columns of the radiogroup
331  Returns: Nothing
332
333  Set the FColumns property which determines the number of columns in
334  which the radiobuttons should be arranged.
335  Range: 1 .. ???
336 ------------------------------------------------------------------------------}
337procedure TCustomRadioGroup.SetColumns(Value: integer);
338begin
339  if Value <> FColumns then begin
340    if (Value < 1)
341       then raise Exception.Create('TCustomRadioGroup: Columns must be >= 1');
342    FColumns := Value;
343    UpdateControlsPerLine;
344  end;
345end;
346
347{------------------------------------------------------------------------------
348  Method: TCustomRadioGroup.SetItems
349  Params:  value - Stringlist containing items to be displayed as radiobuttons
350  Returns: Nothing
351
352  Assign items from a stringlist.
353 ------------------------------------------------------------------------------}
354procedure TCustomRadioGroup.SetItems(Value: TStrings);
355begin
356  if (Value <> FItems) then
357  begin
358    FItems.Assign(Value);
359    UpdateItems;
360    UpdateControlsPerLine;
361  end;
362end;
363
364{------------------------------------------------------------------------------
365  Method: TCustomRadioGroup.SetItemIndex
366  Params:  value - index of RadioButton to be selected
367  Returns: Nothing
368
369  Select one of the radiobuttons
370 ------------------------------------------------------------------------------}
371procedure TCustomRadioGroup.SetItemIndex(Value : integer);
372var
373  OldItemIndex: LongInt;
374  OldIgnoreClicks: Boolean;
375begin
376  //DebugLn('TCustomRadioGroup.SetItemIndex ',dbgsName(Self),' Old=',dbgs(FItemIndex),' New=',dbgs(Value));
377  if Value = FItemIndex then exit;
378  // needed later if handle isn't allocated
379  OldItemIndex := FItemIndex;
380  if FReading then
381    FItemIndex:=Value
382  else begin
383    if (Value < -1) or (Value >= FItems.Count) then
384      raise Exception.CreateFmt(rsIndexOutOfBounds,[ClassName,Value,FItems.Count-1]);
385
386    if (HandleAllocated) then
387    begin
388      // the radiobuttons are grouped by the widget interface
389      // and some does not allow to uncheck all buttons in a group
390      // Therefore there is a hidden button
391      FItemIndex:=Value;
392      OldIgnoreClicks:=FIgnoreClicks;
393      FIgnoreClicks:=true;
394      try
395        if (FItemIndex <> -1) then
396          TRadioButton(FButtonList[FItemIndex]).Checked := true
397        else
398          FHiddenButton.Checked:=true;
399        // uncheck old radiobutton
400        if (OldItemIndex <> -1) then begin
401          if (OldItemIndex>=0) and (OldItemIndex<FButtonList.Count) then
402            TRadioButton(FButtonList[OldItemIndex]).Checked := false
403        end else
404          FHiddenButton.Checked:=false;
405      finally
406        FIgnoreClicks:=OldIgnoreClicks;
407      end;
408      // this has automatically unset the old button. But they do not recognize
409      // it. Update the states.
410      CheckItemIndexChanged;
411      UpdateTabStops;
412
413      OwnerFormDesignerModified(Self);
414    end else
415    begin
416      FItemIndex := Value;
417      // maybe handle was recreated. issue #26714
418      FLastClickedItemIndex := -1;
419
420      // trigger event to be delphi compat, even if handle isn't allocated.
421      // issue #15989
422      if (Value <> OldItemIndex) and not FCreatingWnd then
423      begin
424        if Assigned(FOnClick) then FOnClick(Self);
425        if Assigned(FOnSelectionChanged) then FOnSelectionChanged(Self);
426        FLastClickedItemIndex := FItemIndex;
427      end;
428    end;
429  end;
430  //DebugLn('TCustomRadioGroup.SetItemIndex ',dbgsName(Self),' END Old=',dbgs(FItemIndex),' New=',dbgs(Value));
431end;
432
433{------------------------------------------------------------------------------
434  Method: TCustomRadioGroup.GetItemIndex
435  Params:  value - index of RadioButton to be selected
436  Returns: Nothing
437
438  Retrieve the index of the radiobutton currently selected.
439 ------------------------------------------------------------------------------}
440function TCustomRadioGroup.GetItemIndex : integer;
441begin
442  //debugln('TCustomRadioGroup.GetItemIndex ',dbgsName(Self),' FItemIndex=',dbgs(FItemIndex));
443  Result := FItemIndex;
444end;
445
446procedure TCustomRadioGroup.CheckItemIndexChanged;
447begin
448  if FCreatingWnd or FUpdatingItems then
449    exit;
450  if [csLoading,csDestroying]*ComponentState<>[] then exit;
451  UpdateRadioButtonStates;
452  if [csDesigning]*ComponentState<>[] then exit;
453  if FLastClickedItemIndex=FItemIndex then exit;
454  FLastClickedItemIndex:=FItemIndex;
455  EditingDone;
456  // for Delphi compatibility: OnClick should be invoked, whenever ItemIndex
457  // has changed
458  if Assigned (FOnClick) then FOnClick(Self);
459  // And a better named LCL equivalent
460  if Assigned (FOnSelectionChanged) then FOnSelectionChanged(Self);
461end;
462
463{------------------------------------------------------------------------------
464  Method: TCustomRadioGroup.CanModify
465  Params:  none
466  Returns: always true
467
468  Is the user allowed to select a different radiobutton?
469 ------------------------------------------------------------------------------}
470function TCustomRadioGroup.CanModify : boolean;
471begin
472  Result := true;
473end;
474
475{------------------------------------------------------------------------------
476  Method: TCustomRadioGroup.ReadState
477  Params:  Reader: TReader
478
479  executed when component is read from stream
480 ------------------------------------------------------------------------------}
481procedure TCustomRadioGroup.ReadState(Reader: TReader);
482begin
483  FReading := True;
484  inherited ReadState(Reader);
485  FReading := False;
486  if (fItemIndex<-1) or (fItemIndex>=FItems.Count) then fItemIndex:=-1;
487  FLastClickedItemIndex:=FItemIndex;
488end;
489
490{------------------------------------------------------------------------------
491  Method: TCustomRadioGroup.Clicked
492  Params: sender - the calling object
493
494  This is the callback for all radiobuttons in the group. If an OnClick
495  handler is assigned it will be called
496 ------------------------------------------------------------------------------}
497procedure TCustomRadioGroup.Clicked(Sender : TObject);
498Begin
499  if FIgnoreClicks then exit;
500  CheckItemIndexChanged;
501end;
502
503{------------------------------------------------------------------------------
504  Method: TCustomRadioGroup.Changed
505  Params: sender - the calling object
506
507  Checks for changes. Does the same as Clicked for Delphi compatibility.
508 ------------------------------------------------------------------------------}
509procedure TCustomRadioGroup.Changed(Sender : TObject);
510Begin
511  CheckItemIndexChanged;
512end;
513
514procedure TCustomRadioGroup.UpdateTabStops;
515var
516  i: Integer;
517  RadioBtn: TRadioButton;
518begin
519  for i := 0 to FButtonList.Count - 1 do
520  begin
521    RadioBtn := TRadioButton(FButtonList[i]);
522    RadioBtn.TabStop := RadioBtn.Checked;
523  end;
524end;
525
526class procedure TCustomRadioGroup.WSRegisterClass;
527begin
528  inherited WSRegisterClass;
529  RegisterCustomRadioGroup;
530end;
531
532procedure TCustomRadioGroup.UpdateInternalObjectList;
533begin
534  UpdateItems;
535end;
536
537procedure TCustomRadioGroup.UpdateAll;
538begin
539  UpdateItems;
540  UpdateControlsPerLine;
541  OwnerFormDesignerModified(Self);
542end;
543
544procedure TCustomRadioGroup.SetAutoFill(const AValue: Boolean);
545begin
546  if FAutoFill=AValue then exit;
547  FAutoFill:=AValue;
548  DisableAlign;
549  try
550    if FAutoFill then begin
551      ChildSizing.EnlargeHorizontal:=crsHomogenousChildResize;
552      ChildSizing.EnlargeVertical:=crsHomogenousChildResize;
553    end else begin
554      ChildSizing.EnlargeHorizontal:=crsAnchorAligning;
555      ChildSizing.EnlargeVertical:=crsAnchorAligning;
556    end;
557  finally
558    EnableAlign;
559  end;
560end;
561
562procedure TCustomRadioGroup.SetColumnLayout(const AValue: TColumnLayout);
563begin
564  if FColumnLayout=AValue then exit;
565  FColumnLayout:=AValue;
566  if FColumnLayout=clHorizontalThenVertical then
567    ChildSizing.Layout:=cclLeftToRightThenTopToBottom
568  else
569    ChildSizing.Layout:=cclTopToBottomThenLeftToRight;
570  UpdateControlsPerLine;
571end;
572
573procedure TCustomRadioGroup.FlipChildren(AllLevels: Boolean);
574begin
575  // no flipping
576end;
577
578{------------------------------------------------------------------------------
579  procedure TCustomRadioGroup.UpdateRadioButtonStates;
580
581  Read all Checked properties of all radiobuttons, to update any changes in
582  the interface to the LCL.
583 ------------------------------------------------------------------------------}
584procedure TCustomRadioGroup.UpdateRadioButtonStates;
585var
586  i: Integer;
587begin
588  FItemIndex:=-1;
589  FHiddenButton.Checked;
590  for i:=0 to FButtonList.Count-1 do
591    if TRadioButton(FButtonList[i]).Checked then FItemIndex:=i;
592  UpdateTabStops;
593end;
594