{
'*******************************************************************************
'
'   Michael Koecher/six1 & Werner Pamler/wp_xyz  https://www.lazarusforum.de/
'
'   LGPL2/Linking Exception
'   oder
'   http://creativecommons.org/licenses/by-sa/3.0/de/
'   
'   Based on post in the German Lazarus forum https://www.lazarusforum.de/
'
'*******************************************************************************
}
unit ExQuestionDlg;

{$mode ObjFPC}{$H+}

interface

uses                                    
  LclIntf, LclType, Types,
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, Buttons,
  ExtCtrls, Menus;


// AX, AY >= 0 absolute position
//
// AX= -ord(poXXXX) and AY = MaxInt
//   0 poDesigned,        // places form as if AX and AY were not specified (both MaxInt).
//  -1 poDefault,         // LCL decision (normally window manager decides) 
//  -2 poDefaultPosOnly,  // designed size and LCL position
//  -3 poDefaultSizeOnly, // designed position and LCL size
//  -4 poScreenCenter,    // center form on screen (depends on DefaultMonitor)
//  -5 poDesktopCenter,   // center form on desktop (total of all screens)
//  -6 poMainFormCenter,  // center form on main form (depends on DefaultMonitor)
//  -7 poOwnerFormCenter, // center form on owner form (depends on DefaultMonitor)
//  -8 poWorkAreaCenter
// NOTE: poDefaultXXXX are not working as expected 

function QuestionDlgEx(const ACaption, AMsg: string; ADlgType: TMsgDlgType;
  const AButtons: array of const; AX: Integer = MaxInt; AY: Integer = MaxInt): TModalResult; overload;

function QuestionDlgEx(const ACaption, AMsg: string; ADlgType: TMsgDlgType;
  const AButtons: array of const; AHelpCtx, AX, AY: Integer): TModalResult; overload;

function QuestionDlgEx(const ACaption, AMsg: string; ADlgType: TMsgDlgType;
  const AButtons: array of const; AHelpKeyword: String; 
  AX: Integer = MaxInt; AY: Integer= MaxInt): TModalResult; overload;

function CreateQuestionDlgEx(const ACaption, AMsg: string; ADlgType: TMsgDlgType;
  const AButtons: array of const; AX, AY: Integer ): TForm;

var
  QuestionDlgEx_MaxWidth: Integer = 500;
  QuestionDlgEx_MinWidth: Integer = 250;
  QuestionDlgEx_MinHeight: Integer = 50;  // Text part only
  QuestionDlgEx_ButtonAlignment: TAlignment = taCenter;
  QuestionDlgEx_CustomIcon: TPicture;
  QuestionDlgEx_TextAlignment: TAlignment = taLeftJustify;
  QuestionDlgEx_TextBkColor: TColor = clWindow;
  QuestionDlgEx_TextLayout: TTextLayout = tlCenter;
  QuestionDlgEx_FontName: String = '';    // the same as "default"
  QuestionDlgEx_FontSize: Integer = 0;
  QuestionDlgEx_GlyphShowMode: TGlyphShowMode = gsmApplication;
  
implementation

uses
  TypInfo, Math;

const
  DEFAULT_IMAGE_SIZE = 32;
  DEFAULT_IMAGE_BORDER_X = 10;
  DEFAULT_IMAGE_BORDER_Y = 5;
  DEFAULT_TEXT_BORDER_X = 10;
  DEFAULT_TEXT_BORDER_Y = 10;
  BUTTON_MIN_WIDTH = 75;
  BUTTON_SPACING = 6;
  
function GetBKType(str:string):TBitBtnKind;
var
  value: Integer;
begin
  value := GetEnumValue(TypeInfo(TBitBtnKind), 'bk' + str);
  if value > -1 then
    Result := TBitBtnKind(value)
  else
    Result := bkCancel;
end;

type
  
  { TQForm }

  TQForm = class(TForm)
  private
    type 
      TBtnParams = record
        Caption: string;
        Kind: integer;
        IsDefaultOrCancel: Byte;  // 1 = IsDefault, 2 = IsCancel
      end;
      TBtnParamsArray = array of TBtnParams;
  private
    FButtonAlignment: TAlignment;
    FButtons: array of TBitBtn;
    FButtonPanel: TPanel;
    FDialogType: TMsgDlgType;
    FImage: TGraphic;
    FImageBorder: TSize;
    FInnerButtonPanel: TPanel;
    FText: String;
    FTextBorder: TSize;
    FTextAlignment: TAlignment;
    FTextBkColor: TColor;
    FTextLayout: TTextLayout;
    FTextPanel: TPanel;
    FXPos, FYPos: Integer;
    procedure SetDialogType(AValue: TMsgDlgType);
  protected         
    procedure AdjustForm;
    procedure ApplyButtonAlignment;
    procedure CreateButtonPanel;
    procedure CreateInnerButtonPanel;
    procedure CreateTextPanel;
    procedure HelpClickHandler(Sender: TObject);
    procedure MeasureButtonPanel(var AWidth, AHeight: Integer);
    procedure MeasureText(var AWidth, AHeight: Integer);
    procedure MeasureTextPanel(Wrapped: Boolean; var AWidth, AHeight: Integer);
    procedure PaintTextPanelHandler(Sender: TObject);
    function PrepareButtons(const AButtons: array of const): TBtnParamsArray;
    procedure SetFormPosition(AX, AY: Integer);
  public
    constructor CreateNew(AOwner: TComponent; Num: Integer = 0); override;
    destructor Destroy; override;
    procedure AddButtons(const AButtons: array of const);
    function ShowModal: TModalResult; override;
    property ButtonAlignment: TAlignment read FButtonAlignment write FButtonAlignment;
    property DialogType: TMsgDlgType read FDialogType write SetDialogType;
    property Msg: String read FText write FText;
    property TextAlignment: TAlignment read FTextAlignment write FTextAlignment;
    property TextBkColor: TColor read FTextBkColor write FTextBkColor;
    property TextLayout: TTextLayout read FTextLayout write FTextLayout;
    property XPos: Integer read FXPos write FXPos;
    property YPos: Integer read FYPos write FYPos;
  end;
  
constructor TQForm.CreateNew(AOwner: TComponent; Num: Integer = 0);
begin
  inherited;
  BorderStyle := bsDialog;
  PopupMode := pmAuto;
  
  FImageBorder.CX := Scale96ToFont(DEFAULT_IMAGE_BORDER_X);
  FImageBorder.CY := Scale96ToFont(DEFAULT_IMAGE_BORDER_Y);
  FTextBorder.CX := Scale96ToFont(DEFAULT_TEXT_BORDER_X);
  FTextBorder.CY := Scale96ToFont(DEFAULT_TEXT_BORDER_Y);
  
  CreateTextPanel;
  CreateButtonPanel;
  CreateInnerButtonPanel;
end;
  
destructor TQForm.Destroy;
begin
  FImage.Free;
  inherited;
end;

procedure TQForm.AddButtons(const AButtons: array of const);
const
  MsgDlgBtnStr : array [0..11] of string = (
    'Yes', 'No', 'OK', 'Cancel', 'Abort', 'Retry', 'Ignore',
    'All', 'NoToAll', 'YesToAll', 'Help', 'Close'
  );
var
  btnParams: TBtnParamsArray;
  i: Integer;
  btn: TBitBtn;
begin
  // Translate given Buttons into array of button parameters.
  btnParams := PrepareButtons(AButtons);
  
  // Create BitBtn's
  SetLength(FButtons, Length(btnParams));
  for i := 0 to high(btnParams) do
  begin
    btn := TBitBtn.Create(Self);
    btn.Parent := FInnerButtonPanel;
    btn.GlyphShowMode := QuestionDlgEx_GlyphShowMode;
    btn.Kind := GetBKType(MsgDlgBtnStr[btnParams[i].Kind]);
    if btn.Kind = bkHelp then
      btn.OnClick := @HelpClickHandler;
    if trim(btnParams[i].Caption) <> '' then
      btn.Caption := btnParams[i].Caption;   
    btn.Constraints.MinWidth := Scale96ToFont(BUTTON_MIN_WIDTH);
    btn.Cancel := (btnParams[i].IsDefaultOrCancel = 2);
    btn.Default := (btnParams[i].IsDefaultOrCancel = 1);
    if btn.Default then ActiveControl := btn;
    btn.AutoSize := true;
    btn.BorderSpacing.Left := Scale96ToFont(BUTTON_SPACING);
    btn.BorderSpacing.Right := Scale96ToFont(BUTTON_SPACING);
//    btn.BorderSpacing.Around := Scale96ToFont(BUTTON_SPACING);
    if i = 0 then
      btn.AnchorSideLeft.Control := FInnerButtonPanel
    else
    begin
      btn.AnchorSideLeft.Control := FButtons[i-1];
      btn.AnchorSideLeft.Side := asrBottom;
    end;
    btn.AnchorSideTop.Control := FInnerButtonPanel;
    FButtons[i] := btn;
  end;
end;

procedure TQForm.AdjustForm;
var
  buttonPanelWidth: Integer = 0;
  buttonPanelHeight: Integer = 0;
  textPanelWidth: Integer = 0;
  textPanelHeight: Integer = 0;
begin
  if (FTextPanel = nil) or (FButtonPanel = nil) then
    exit;
  
  MeasureButtonPanel(buttonPanelWidth, buttonPanelHeight);
  MeasureTextPanel(false, textPanelWidth, textPanelHeight);
  
  if buttonPanelWidth > textPanelWidth then
  begin
    textPanelWidth := buttonPanelWidth;
  end else
  begin
    MeasureTextPanel(true, textPanelWidth, textPanelHeight);
    if textPanelWidth > QuestionDlgEx_MaxWidth then
    begin
      if buttonPanelWidth > QuestionDlgEx_Maxwidth then
        textPanelWidth := buttonPanelWidth
      else
        textPanelWidth := QuestionDlgEx_MaxWidth;
      MeasureTextPanel(true, textPanelWidth, textPanelheight);
    end;
  end;
  
  FTextPanel.SetBounds(0, 0, textPanelWidth, textPanelHeight);
  FInnerButtonPanel.SetBounds(0, 0, buttonPanelWidth, buttonPanelHeight);
  FButtonPanel.SetBounds(0, textPanelHeight, textPanelWidth, buttonPanelHeight);
  
  ClientWidth := textPanelWidth;
  ClientHeight := textPanelHeight + buttonPanelHeight;

  ApplyButtonAlignment;
end;

procedure TQForm.ApplyButtonAlignment;
begin
  // Set button and text alignments
  case FButtonAlignment of
    taLeftJustify: 
      FInnerButtonPanel.AnchorSideLeft.Side := asrLeft;
    taCenter: 
      FInnerButtonPanel.AnchorSideLeft.Side := asrCenter;
    taRightJustify: 
      begin
        FInnerButtonPanel.Anchors := [akTop, akRight];
        FInnerButtonPanel.AnchorSideRight.Control := FButtonPanel;
        FInnerButtonPanel.AnchorSideRight.Side := asrRight;
      end;
  end;
end;

procedure TQForm.CreateButtonPanel;
begin
  FButtonPanel := TPanel.Create(Self);
  with FButtonPanel do
  begin
    BevelOuter := bvNone;
    Caption := '';
    Color := clBtnFace;
    Parent := Self;
  end;
end;
  
procedure TQForm.CreateInnerButtonPanel;
begin
  FInnerButtonPanel := TPanel.Create(Self);
  with FInnerButtonPanel do 
  begin
    AnchorSideLeft.Control := FButtonPanel;
    AnchorSideTop.Control := FButtonPanel;
    BevelOuter := bvNone;
    BorderSpacing.Top := Scale96ToFont(BUTTON_SPACING);
    BorderSpacing.Bottom := Scale96ToFont(BUTTON_SPACING);
    Caption := '';
    AutoSize := true;
    Parent := FButtonPanel;
  end;
end;
    
procedure TQForm.CreateTextPanel;
begin
  FTextPanel := TPanel.Create(Self);
  with FTextPanel do
  begin
    BevelOuter := bvNone;
    Caption := '';
    Color := QuestionDlgEx_TextBkColor;
    Parent := Self;
    OnPaint := @PaintTextPanelHandler;
  end;
end;
  
procedure TQForm.HelpClickHandler(Sender: TObject);
begin
  ShowHelp;
end;

procedure TQForm.MeasureButtonPanel(var AWidth, AHeight: Integer);
var
  dy1, dy2: Integer;
begin
  AWidth := 0;
  AHeight := 0;
  
  FInnerButtonPanel.HandleNeeded;
  FInnerButtonPanel.GetPreferredSize(AWidth, AHeight);

  dy1 := Max(FInnerButtonPanel.BorderSpacing.Top, FInnerButtonPanel.BorderSpacing.Around);
  dy2 := Max(FInnerButtonPanel.BorderSpacing.Bottom, FInnerButtonPanel.BorderSpacing.Around);
  inc(AHeight, dy1 + dy2);
  
  if QuestionDlgEx_MinWidth > AWidth then 
    AWidth := QuestionDlgEx_MinWidth;
end;

{ Measures width and height of the message text. 
  The max text width is given as input parameter AWidth. When the text is wider
  it is wrapped. }
procedure TQForm.MeasureText(var AWidth, AHeight: Integer);
var
  R: TRect;
  flags: integer;
begin
  R := Rect(0, 0, AWidth, AHeight);
  flags := DT_CALCRECT or DT_WORDBREAK;
  DrawText(Canvas.Handle, PChar(FText), Length(FText), R, flags);
  AWidth := R.Right;
  AHeight := R.Bottom;
end;
  
{ Measures the total width and heigth of the text panel including icon and
  borders. 
  Is called twice: 
  - When Wrapped is false the max width is determine so that wrapping does not 
    occur. 
  - When wrapped is true the parameter AWidth on input defines the width into 
    which the text must fit; when it is wider it is wrapped. }
procedure TQForm.MeasureTextPanel(Wrapped: Boolean; var AWidth, AHeight: Integer);
var
  R: TRect;
  x: Integer;
  flags: Integer;
  imgHeight: Integer;
begin
  x := FImageBorder.CX;
  if Assigned(FImage) then
    inc(x, FImage.Width + Max(FImageBorder.CX, FTextBorder.CX));
  if not Wrapped then AWidth := 9999;
  R := Rect(x, FTextBorder.CY, AWidth, 9999);
  flags := DT_CALCRECT;
  if Wrapped then flags := flags or DT_WORDBREAK;
  HandleNeeded;
  DrawText(Canvas.Handle, PChar(FText), Length(FText), R, flags);
  inc(R.Bottom, FTextBorder.CY);
  AWidth := R.Right;
  if AWidth < QuestionDlgEx_MinWidth then
    AWidth := QuestionDlgEx_MinWidth;
  AHeight := R.Bottom;
  if Assigned(FImage) then
    imgHeight := FImage.Height
  else
    imgHeight := Scale96ToFont(DEFAULT_IMAGE_SIZE);
  if (AHeight < 2*FImageBorder.CY + imgHeight) then
    AHeight := 2*FImageBorder.CY + imgHeight;
  if AHeight < QuestionDlgEx_MinHeight then
    AHeight := QuestionDlgEx_MinHeight;
end;

{ The TextPanels OnPaint handler. It draws the icon and the text. }
procedure TQForm.PaintTextPanelHandler(Sender: TObject);
var
  x: Integer;
  R: TRect;
  flags: Integer;
  w, h: Integer;
begin
  with FTextPanel do begin
    Canvas.Brush.Color := Color;
    Canvas.FillRect(0, 0, Width, Height);
    x := FImageBorder.CX;
    if Assigned(FImage) then
    begin
      Canvas.Draw(x, FImageBorder.CY, FImage);
      inc(x, FImage.Width + Max(FImageBorder.CX, FTextBorder.CX));
    end;
    if FText <> '' then
    begin
      R := Rect(x, FTextBorder.CY, Width - FTextBorder.CX, Height - FTextBorder.CY);
      w := R.Right - x;
      h := R.Bottom - FTextBorder.CY;
      MeasureText(w, h);
      flags := DT_WORDBREAK;
      case FTextAlignment of
        taLeftJustify: flags := flags or DT_LEFT;
        taCenter: flags := flags or DT_CENTER;
        taRightJustify: flags := flags or DT_RIGHT;
      end;
      case FTextLayout of
        tlTop: ;
        tlCenter: R.Top := (R.Top + R.Bottom - h) div 2;
        tlBottom: R.Top := R.Bottom - h;
      end;
      DrawText(Canvas.Handle, PChar(FText), Length(FText), R, flags);
    end;
  end;
end;

function TQForm.PrepareButtons(const AButtons: array of const): TBtnParamsArray; 
var
  i, n: Integer;
begin
  Result := nil;
  SetLength(Result, Length(AButtons));
  n := -1;
  for i := 0 to high(AButtons) do
  begin
    if AButtons[i].VType = vtAnsiString then //text
    begin
      if SameText(AButtons[i].VPChar, 'IsDefault') then
        Result[n].IsDefaultOrCancel := 1
      else if SameText(AButtons[i].VPChar, 'IsCancel') then
        Result[n].IsDefaultOrCancel := 2
      else
        Result[n].Caption := AButtons[i].VPChar
    end else
    if AButtons[i].VType = vtInteger then
    begin
      inc(n);
      Result[n].Kind := AButtons[i].VInteger;
    end;
  end;
  SetLength(Result, n+1);
end;

procedure TQForm.SetDialogType(AValue: TMsgDlgType);
var
  iconKind: Integer;
begin
  FDialogType := AValue;
  iconKind := idDialogBase + 1 + ord(FDialogType);
  FImage.Free;
  if (FDialogType = mtCustom) and Assigned(QuestionDlgEx_CustomIcon) then
  begin
    if QuestionDlgEx_CustomIcon.Png <> nil then
      FImage := TPortableNetworkGraphic.Create
    else if QuestionDlgEx_CustomIcon.Bitmap <> nil then
      FImage := TBitmap.Create
    else if QuestionDlgEx_CustomIcon.Icon <> nil then
      FImage := TIcon.Create
    else if QuestionDlgEx_CustomIcon.Graphic <> nil then
      FImage := TGraphicClass(QuestionDlgEx_CustomIcon.ClassType).Create
    else
      exit;
    Fimage.Assign(QuestionDlgEx_CustomIcon);
  end else
    FImage := GetDialogIcon(iconKind);
end;
  
procedure TQForm.SetFormPosition(AX, AY: Integer);
const
  NOT_USED = MaxInt;
var
  R: TRect;
begin
  if (AX < 0) and (AY = MaxInt) then
  begin
     Position := TPosition(-AX);
  end else
  begin
    R := Monitor.WorkAreaRect;
    Position := poDesigned;
    if AY = NOT_USED then // Screen Center  Y
      Top := (R.Top + R.Bottom - Height) div 2
    else
    if (AY + Height) <= (Monitor.WorkareaRect.Bottom - 30) then
      Top := AY
    else // prevent displaying outside bottom screen border
      Top := R.Bottom - Height - 30;

    if AX = NOT_USED then // Screen Center  X
      Left := (R.Left + R.Right - Width) div 2
    else
    if (AX + Width) <= Monitor.WorkareaRect.Right then
      Left := AX
    else // prevent displaying outside right screen border
      Left := R.Right - Width;
  end;
end;

function TQForm.ShowModal: TModalResult;
begin
  AdjustForm;
  SetFormPosition(FXPos, FYPos);
  Result := inherited;
end;

function CreateQuestionDlgEx(const ACaption, AMsg: string; ADlgType: TMsgDlgType;
  const AButtons: array of const; AX, AY: Integer): TForm;
var
  QForm: TQForm;
begin
  QForm := TQForm.CreateNew(Screen.ActiveCustomForm);
  if QuestionDlgEx_FontName = '' then
    QForm.Font.Name := 'default'
  else
    QForm.Font.Name := QuestionDlgEx_FontName;
  QForm.Font.Size := QuestionDlgEx_FontSize;
  QForm.ButtonAlignment := QuestionDlgEx_ButtonAlignment;
  QForm.TextAlignment := QuestionDlgEx_TextAlignment;
  QForm.TextBkColor := QuestionDlgEx_TextBkColor;
  QForm.TextLayout := QuestionDlgEx_TextLayout;
  QForm.Caption := ACaption;
  QForm.DialogType := ADlgtype;
  QForm.Msg := AMsg;
  QForm.AddButtons(AButtons);
  QForm.XPos := AX;
  QForm.YPos := AY;
  Result := QForm;
end;

function QuestionDlgEx(const ACaption, AMsg: string; ADlgType: TMsgDlgType;
  const AButtons: array of const; AX: Integer = MaxInt; AY: Integer = MaxInt): TModalResult; 
var
  QForm: TForm;
begin
  QForm := CreateQuestionDlgEx( ACaption, AMsg, ADlgType, AButtons, AX, AY);
  try
    result := QForm.ShowModal;
  finally
    FreeAndNil(QForm);
  end;
end;

function QuestionDlgEx(const ACaption, AMsg: string; ADlgType: TMsgDlgType;
  const AButtons: array of const; AHelpCtx, AX, AY: Integer): TModalResult; 
var
  QForm: TForm;
begin
  QForm := CreateQuestionDlgEx( ACaption, AMsg, ADlgType, AButtons, AX, AY);
  try
    QForm.HelpContext := AHelpCtx;
    result := QForm.ShowModal;
  finally
    FreeAndNil(QForm);
  end;
end;

function QuestionDlgEx(const ACaption, AMsg: string; ADlgType: TMsgDlgType;
  const AButtons: array of const; AHelpKeyword: String; 
  AX: Integer = MaxInt; AY: Integer= MaxInt): TModalResult; overload;
var
  QForm: TForm;
begin
  QForm:=CreateQuestionDlgEx( ACaption, AMsg, ADlgType, AButtons, AX, AY);
  try
    QForm.HelpKeyword := AHelpKeyword;
    result := QForm.ShowModal;
  finally
    FreeAndNil(QForm);
  end;
end;

finalization
  FreeAndNil(QuestionDlgex_CustomIcon);
  
end.