unit grideditlinks;

// The editor links, instanciated by VirtualTree.CreateEditor

{$mode delphi}{$H+}

interface

uses
  Forms, Graphics, Messages, laz.VirtualTrees, ComCtrls, SysUtils, Classes,
  StdCtrls, ExtCtrls, CheckLst, Controls, Types, Dialogs, Menus, MaskEdit, DateUtils, Math,
  dbconnection, dbstructures, apphelpers, generic_types, texteditor, bineditor, lazaruscompat,
  {$IFNDEF FREEBSD}System.UITypes,{$ENDIF} RegExpr, extra_controls, EditBtn, LCLType, LCLIntf;

type
  // Radio buttons and checkboxes which do not pass <Enter> key to their parent control
  // so a OnKeyDown event using <Enter> has the chance to end editing.
  {TAllKeysRadioButton = class(TRadioButton)
    procedure WMGetDlgCode(var Msg: TMessage); message WM_GETDLGCODE;
  end;}
  TAllKeysRadioButton = TRadioButton;
  {TAllKeysCheckBox = class(TCheckBox)
    procedure WMGetDlgCode(var Msg: TMessage); message WM_GETDLGCODE;
  end;}
  TAllKeysCheckBox = TCheckBox;

  TBaseGridEditorLink = class(TInterfacedObject, IVTEditLink)
  private
    FInstanceId: Integer;
    FParentForm: TWinControl;      // A back reference to the main form
    FTree: TVirtualStringTree;        // A back reference to the tree calling.
    FNode: PVirtualNode;              // The node to be edited.
    FColumn: TColumnIndex;            // The column of the node.
    FCellText: String;            // Original cell text value
    FCellFont: TFont;                 // Cosmetic
    FCellBackground: TColor;
    FMainControl: TWinControl;        // The editor's most important component
    FStopping: Boolean;               // Set to True when the edit link requests stopping the edit action.
    FLastKeyDown: Integer;            // Set in OnKeyDown on the editor's main control
    FLastShiftState: TShiftState;
    FOldWindowProc: TWndMethod;       // Temporary switched to TempWindowProc to be able to catch Tab key
    FTableColumn: TTableColumn;
    FModified: Boolean;
    FAllowEdit: Boolean;
    FBeginEditTime: Cardinal;
    procedure Log(Msg: String);
    //procedure TempWindowProc(var Message: TMessage);
    procedure DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure DoEndEdit(Sender: TObject);
    procedure AsyncEndEdit(Data: PtrInt);
    procedure DoCancelEdit(Sender: TObject);
    procedure AsyncCancelEdit(Data: PtrInt);
    function GetCellRect(InnerTextBounds: Boolean): TRect;
  public
    // The table column of the cell being edited. Mostly used in data grids.
    property TableColumn: TTableColumn read FTableColumn;
    // The original constructor, not used any more, throws an exception if you do
    constructor Create; overload;
    // The right constructor, we need the Tree reference
    constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); overload; virtual;
    destructor Destroy; override;
    property Tree: TVirtualStringTree read FTree;
    function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; virtual; stdcall;
    function BeginEdit: Boolean; virtual; stdcall;
    function CancelEdit: Boolean; virtual; stdcall;
    function EndEdit: Boolean; virtual; stdcall; abstract;
    function EndEditHelper(NewText: String): Boolean;
    function GetBounds: TRect; virtual; stdcall;     // Normally useless and unused
    procedure ProcessMessage(var Message: TMessage); stdcall;
    procedure SetBounds(R: TRect); virtual; stdcall; abstract;
  end;

  THexEditorLink = class(TBaseGridEditorLink)
  private
    FForm: TfrmBinEditor;
  public
    MaxLength: Int64;
    TitleText: String;
    constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override;
    destructor Destroy; override;
    function BeginEdit: Boolean; override;
    function CancelEdit: Boolean; override;
    function EndEdit: Boolean; override;
    function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override;
    procedure SetBounds(R: TRect); override;
  end;

  TDateTimeEditorLink = class(TBaseGridEditorLink)
  private
    FPanel: TPanel;
    FMaskEdit: TMaskEdit;
    FTimer: TTimer;
    FModifyOffset: Integer;
    FTimerCalls: Integer;
    FUpDown: TUpDown;
    procedure DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure DoKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure UpDownChangingEx(Sender: TObject; var AllowChange: Boolean;
      NewValue: SmallInt; Direction: TUpDownDirection);
    procedure UpDownMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure DoOnTimer(Sender: TObject);
    procedure ModifyDate(Offset: Integer);
    procedure TextChange(Sender: TObject);
    function MicroSecondsPrecision: Integer;
  public
    constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override;
    destructor Destroy; override;
    function BeginEdit: Boolean; override;
    function EndEdit: Boolean; override;
    function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override;
    procedure SetBounds(R: TRect); override;
  end;

  TEnumEditorLink = class(TBaseGridEditorLink)
  private
    FCombo: TExtComboBox;
    procedure DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure DoSelect(Sender: TObject);
  public
    ValueList, DisplayList: TStringList;
    AllowCustomText: Boolean;
    ItemMustExist: Boolean;
    constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override;
    destructor Destroy; override;
    function BeginEdit: Boolean; override;
    function EndEdit: Boolean; override;
    function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override;
    procedure SetBounds(R: TRect); override;
  end;

  TSetEditorLink = class(TBaseGridEditorLink)
  private
    FPanel: TPanel;
    FCheckList: TCheckListBox;
    FBtnOK, FBtnCancel: TButton;
    FEndTimer: TTimer;
    procedure BtnOkClick(Sender: TObject);
    procedure BtnCancelClick(Sender: TObject);
  public
    ValueList: TStringList;
    constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override;
    destructor Destroy; override;
    function BeginEdit: Boolean; override;
    function EndEdit: Boolean; override;
    function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override;
    procedure SetBounds(R: TRect); override;
  end;

  // Inplace editor with button
  TInplaceEditorLink = class(TBaseGridEditorLink)
  private
    FPanel: TPanel;
    FEdit: TEdit;
    FButton: TButton;
    FMaxLength: Int64;
    procedure DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure ButtonClick(Sender: TObject);
  public
    ButtonVisible: Boolean;
    TitleText: String;
    constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override;
    destructor Destroy; override;
    function BeginEdit: Boolean; override;
    function EndEdit: Boolean; override;
    function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override;
    procedure SetBounds(R: TRect); override;
    property MaxLength: Int64 read FMaxLength write FMaxLength;
  end;

  TColumnDefaultEditorLink = class(TBaseGridEditorLink)
  private
    FPanel: TPanel;
    FRadioNothing, FRadioText, FRadioNULL, FRadioExpression, FRadioAutoInc: TAllKeysRadioButton;
    FlblOnUpdate: TLabel;
    FTextEdit: TEditButton;
    FTextDropDown: TPopupMenu;
    FExpressionEdit: TComboBox;
    FOnUpdateEdit: TComboBox;
    FBtnOK, FBtnCancel: TButton;
    FEndTimer: TTimer;
    procedure RadioClick(Sender: TObject);
    procedure EditChange(Sender: TObject);
    procedure EditButtonClick(Sender: TObject);
    procedure EditDropDownClick(Sender: TObject);
    procedure BtnOkClick(Sender: TObject);
    procedure BtnCancelClick(Sender: TObject);
  public
    DefaultType, OnUpdateType: TColumnDefaultType;
    DefaultText, OnUpdateText: String;
    constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override;
    destructor Destroy; override;
    function BeginEdit: Boolean; override;
    function EndEdit: Boolean; override;
    function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override;
    procedure SetBounds(R: TRect); override;
  end;

  TDataTypeEditorLink = class(TBaseGridEditorLink)
  private
    FTreeSelect: TVirtualStringTree;
    FMemoHelp: TMemo;
    procedure DoTreeSelectGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
      Column: TColumnIndex; TextType: TVSTTextType; var CellText: String);
    procedure DoTreeSelectInitNode(Sender: TBaseVirtualTree;
      ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
    procedure DoTreeSelectInitChildren(Sender: TBaseVirtualTree;
      Node: PVirtualNode; var ChildCount: Cardinal);
    procedure DoTreeSelectHotChange(Sender: TBaseVirtualTree; OldNode, NewNode: PVirtualNode);
    procedure DoTreeSelectPaintText(Sender: TBaseVirtualTree;
      const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
      TextType: TVSTTextType);
    procedure DoTreeSelectFocusChanging(Sender: TBaseVirtualTree; OldNode, NewNode:
        PVirtualNode; OldColumn, NewColumn: TColumnIndex; var Allowed: Boolean);
    procedure DoTreeSelectFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex);
  public
    constructor Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn); override;
    destructor Destroy; override;
    function BeginEdit: Boolean; override;
    function EndEdit: Boolean; override;
    function PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; override;
    procedure SetBounds(R: TRect); override;
  end;


var
  ActiveGridEditor: TBaseGridEditorLink=nil;

implementation

uses
  main;


{procedure TAllKeysRadioButton.WMGetDlgCode(var Msg: TMessage);
begin
  inherited;
  Msg.Result := Msg.Result or DLGC_WANTALLKEYS;
end;}


{procedure TAllKeysCheckBox.WMGetDlgCode(var Msg: TMessage);
begin
  inherited;
  Msg.Result := Msg.Result or DLGC_WANTALLKEYS;
end;}

procedure TBaseGridEditorLink.Log(Msg: String);
begin
  MainForm.LogSQL('#'+FInstanceId.ToString+': '+Msg, lcDebug);
end;


constructor TBaseGridEditorLink.Create;
begin
  raise Exception.CreateFmt(_('Wrong constructor called: %s.%s. Instead, please call the overloaded version %s.%s.'),
    [Self.ClassName, 'Create', Self.ClassName, 'Create(VirtualStringTree)']);
end;

constructor TBaseGridEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn);
begin
  inherited Create;
  FInstanceId := Random(100);
  FTree := Tree;
  // Enable mouse scrolling, plus ensure the editor component
  // is not partly hidden when it pops up in a bottom cell
  FParentForm := GetParentForm(FTree);
  // Avoid flicker
  FParentForm.Repaint;
  FMainControl := nil;
  FModified := False;
  FAllowEdit := AllowEdit;
  ActiveGridEditor := Self;
  FTableColumn := Col;
end;

destructor TBaseGridEditorLink.Destroy;
var
  NewColumn, FirstCol, LastCol: TColumnIndex;
  NewNode: PVirtualNode;
  DoPrev: Boolean;
begin
  ActiveGridEditor := nil;
  if Assigned(FMainControl) then begin
    FMainControl.WindowProc := FOldWindowProc;
    FMainControl := nil;
  end;
  if FLastKeyDown = VK_TAB then begin
    DoPrev := ssShift in FLastShiftState;
    // Advance to next/previous visible column/node.
    NewNode := FNode;
    NewColumn := FColumn;
    FirstCol := FTree.Header.Columns.GetFirstVisibleColumn;
    LastCol := FTree.Header.Columns.GetLastVisibleColumn;
    while true do begin
      // Find a column for the current node which can be focused.
      if DoPrev then begin
        if NewColumn = FirstCol then begin
          NewColumn := LastCol;
          NewNode := FTree.GetPreviousVisible(NewNode);
        end else
          NewColumn := FTree.Header.Columns.GetPreviousVisibleColumn(NewColumn);
      end else begin
        if NewColumn = LastCol then begin
          NewColumn := FirstCol;
          NewNode := FTree.GetNextVisible(NewNode);
        end else
          NewColumn := FTree.Header.Columns.GetNextVisibleColumn(NewColumn);
      end;
      if not Assigned(NewNode) then
        Break;
      if not FTree.CanEdit(NewNode, NewColumn) then
        Continue;
      FTree.ClearSelection;
      FTree.Selected[NewNode] := True;
      FTree.EditNode(NewNode, NewColumn);
      Break;
    end;
  end;
  inherited;
end;

function TBaseGridEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean;
var
  FCellTextBounds: TRect;
  HasNulls: Boolean;
begin
  Result := not FStopping;
  if not Result then
    Exit;
  FNode := Node;
  FColumn := Column;
  FCellFont := TFont.Create;
  FCellTextBounds := Rect(0, 0, 0, 0);
  FTree.GetTextInfo(FNode, FColumn, FCellFont, FCellTextBounds, FCellText);
  HasNulls := False;
  apphelpers.RemoveNullChars(FCellText, HasNulls);
  if HasNulls and FAllowEdit then begin
    FAllowEdit := False;
  end;

  // Not all editors have a connection assigned, e.g. session manager tree
  if Assigned(FTableColumn) then begin
    FCellFont.Color := AppColorSchemes.First.GridTextColors[FTableColumn.DataType.Category];
  end;
  FCellBackground := FTree.Header.Columns[FColumn].Color;
  if Assigned(FMainControl) then begin
    FOldWindowProc := FMainControl.WindowProc;
    //FMainControl.WindowProc := TempWindowProc;
  end;
  // Adjust editor position and allow repainting mainform
  SetBounds(FCellTextBounds);
  FParentForm.Repaint;
end;

function TBaseGridEditorLink.BeginEdit: Boolean;
begin
  Result := not FStopping;
  FBeginEditTime := GetTickCount;
end;

function TBaseGridEditorLink.CancelEdit: Boolean;
begin
  Result := not FStopping;
  if Result then begin
    FStopping := True;
    FTree.CancelEditNode;
    if FTree.CanFocus then
      FTree.SetFocus;
  end;
end;

function TBaseGridEditorLink.EndEditHelper(NewText: String): Boolean;
begin
  Result := not FStopping;
  if FStopping then Exit;
  FStopping := True;
  if FModified and FAllowEdit then
    FTree.Text[FNode, FColumn] := NewText;
  if FTree.CanFocus and (FLastKeyDown <> VK_TAB) then
    FTree.SetFocus;
end;

{procedure TBaseGridEditorLink.TempWindowProc(var Message: TMessage);
begin
  case Message.Msg of
    WM_CHAR: // Catch hotkeys
      if not (TWMChar(Message).CharCode = VK_TAB) then
        FOldWindowProc(Message);
    WM_GETDLGCODE: // "WantTabs" mode for main control
      Message.Result := Message.Result or DLGC_WANTARROWS or DLGC_WANTALLKEYS or DLGC_WANTTAB;
    else begin
      try
        FOldWindowProc(Message);
      except
        // EAccessViolation occurring in some cases
        on E:Exception do begin
          Log(E.Message+' Message CharCode:'+TWMChar(Message).CharCode.ToString+' Msg:'+Message.Msg.ToString);
        end;
      end;
    end;
  end;
end;}

procedure TBaseGridEditorLink.ProcessMessage(var Message: TMessage);
begin
  if (FMainControl <> nil) and FMainControl.HandleAllocated then
    FMainControl.WindowProc(Message);
end;

function TBaseGridEditorLink.GetBounds: TRect; stdcall;
begin
  // Only important if the editor resizes itself.
  Result := Rect(0, 0, 0, 0);
end;

function TBaseGridEditorLink.GetCellRect(InnerTextBounds: Boolean): TRect;
var
  Text: String;
  CellBounds, TextBounds: TRect;
  Ghosted: Boolean;
  ImageIndex: Integer;
  f: TFont;
begin
  // Return the cell's rectangle, relative to the parent form.
  f := TFont.Create;
  TextBounds := Rect(0, 0, 0, 0);
  FTree.GetTextInfo(FNode, FColumn, f, TextBounds, Text);
  CellBounds := FTree.GetDisplayRect(FNode, FColumn, False);

  Inc(CellBounds.Left, Integer(FTree.GetNodeLevel(FNode)) * (Integer(FTree.Indent)+FTree.TextMargin));
  if (toShowRoot in FTree.TreeOptions.PaintOptions)
    and (FColumn = FTree.Header.MainColumn) then begin
    // Reserve space for plus or minus button
    Inc(CellBounds.Left, FTree.Indent);
  end;
  if Assigned(FTree.Images) and Assigned(FTree.OnGetImageIndex) then begin
    // Reserve space for image
    ImageIndex := -1;
    Ghosted := False;
    FTree.OnGetImageIndex(FTree, FNode, ikNormal, FColumn, Ghosted, ImageIndex);
    if ImageIndex > -1 then
      Inc(CellBounds.Left, FTree.Images.Width+2);
  end;
  TextBounds.Left := CellBounds.Left + 2*FTree.TextMargin;

  if InnerTextBounds then begin
    // Inner bounds are considered to be relative to the outer cell bounds
    Result := Rect(TextBounds.Left-CellBounds.Left,
      TextBounds.Top-CellBounds.Top,
      CellBounds.Right-CellBounds.Left, // Far right edge of cell, not of text
      CellBounds.Bottom-CellBounds.Top
      );
  end else begin
    // Recalculate top left corner of rectangle, so it is relative to the parent form (which is FParentForm)
    Result := CellBounds;
    Types.OffsetRect(Result,
      FTree.ClientOrigin.X - FParentForm.ClientOrigin.X,
      FTree.ClientOrigin.Y - FParentForm.ClientOrigin.Y
      );
    Dec(Result.Bottom, 1);
  end;
end;

procedure TBaseGridEditorLink.DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  FLastKeyDown := Key;
  FLastShiftState := Shift;
  case Key of
    // Cancel by Escape
    VK_ESCAPE: DoCancelEdit(Sender);
    // Apply changes and end editing by [Ctrl +] Enter or Tab
    VK_RETURN, VK_TAB: DoEndEdit(Sender);
  end;
end;

procedure TBaseGridEditorLink.DoEndEdit(Sender: TObject);
begin
  Application.QueueAsyncCall(AsyncEndEdit, PtrInt(FTree));
end;

procedure TBaseGridEditorLink.AsyncEndEdit(Data: PtrInt);
begin
  TVirtualStringTree(Data).EndEditNode;
end;

procedure TBaseGridEditorLink.DoCancelEdit(Sender: TObject);
begin
  Application.QueueAsyncCall(AsyncCancelEdit, PtrInt(FTree));
end;

procedure TBaseGridEditorLink.AsyncCancelEdit(Data: PtrInt);
begin
  TVirtualStringTree(Data).CancelEditNode;
end;



constructor THexEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn);
begin
  inherited;
end;

destructor THexEditorLink.Destroy;
begin
  inherited;
  FForm.Close;
  FreeAndNil(FForm);
end;


function THexEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall;
begin
  Result := inherited PrepareEdit(Tree, Node, Column);
  if not Result then
    Exit;

  // Create the text editor form
  FForm := TfrmBinEditor.Create(Ftree);
  FForm.SetFont(FCellFont);
  FForm.SetText(FCellText);
  FForm.SetTitleText(TitleText);
  FForm.SetMaxLength(MaxLength);
  FForm.memoText.ReadOnly := not FAllowEdit;
end;


function THexEditorLink.BeginEdit: Boolean; stdcall;
begin
  Result := inherited BeginEdit;
  if Result then
    FForm.ShowModal;
end;


function THexEditorLink.CancelEdit: Boolean;
begin
  Result := inherited CancelEdit;
  if Result then
    FForm.Close;
end;


function THexEditorLink.EndEdit: Boolean; stdcall;
begin
  FForm.Close;
  FModified := FForm.Modified;
  Result := EndEditHelper(FForm.GetText);
end;


procedure THexEditorLink.SetBounds(R: TRect); stdcall;
begin
  // Not in use, form's position is centered on mainform
end;



{ DateTime editor }

constructor TDateTimeEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn);
begin
  inherited;

  FPanel := TPanel.Create(FParentForm);
  FPanel.Parent := FParentForm;
  FPanel.Hide;
  FPanel.ParentBackground := False;
  FPanel.BevelOuter := bvNone;
  FPanel.OnExit := DoEndEdit;

  FMaskEdit := TMaskEdit.Create(FPanel);
  FMaskEdit.Parent := FPanel;
  FMaskEdit.ParentColor := True;
  FMaskEdit.BorderStyle := bsNone;
  FMaskEdit.OnKeyDown := DoKeyDown;
  FMaskEdit.OnKeyUp := DoKeyUp;
  FMaskEdit.OnChange := TextChange;
  FMainControl := FMaskEdit;

  FUpDown := TUpDown.Create(FPanel);
  FUpDown.Parent := FPanel;
  FUpDown.OnChangingEx := UpDownChangingEx;
  FUpDown.OnMouseUp := UpDownMouseUp;

  FTimer := TTimer.Create(FMaskEdit);
  FTimer.Interval := 50;
  FTimer.OnTimer := DoOnTimer;
  FTimer.Enabled := False;
end;


destructor TDateTimeEditorLink.Destroy;
begin
  AppSettings.WriteInt(asDateTimeEditorCursorPos, FMaskEdit.SelStart, IntToStr(Integer(FTableColumn.DataType.Category)));
  FreeAndNil(FTimer);
  FreeAndNil(FUpDown);
  FreeAndNil(FMaskEdit);
  FreeAndNil(FPanel);
  inherited;
end;


function TDateTimeEditorLink.BeginEdit: Boolean; stdcall;
begin
  Result := inherited BeginEdit;
  if Result then begin
    FPanel.Show;
    FMaskEdit.SetFocus;
    // Focus very last segment of date
    FMaskEdit.SelStart := AppSettings.ReadInt(asDateTimeEditorCursorPos, IntToStr(Integer(FTableColumn.DataType.Category)));
    FMaskEdit.SelLength := 1;
  end;
end;


function TDateTimeEditorLink.EndEdit: Boolean;
begin
  Result := EndEditHelper(Trim(FMaskEdit.Text));
end;


function TDateTimeEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall;
var
  MinColWidth,
  ForceTextLen: Integer;
begin
  Result := inherited PrepareEdit(Tree, Node, Column);
  if not Result then
    Exit;
  FMaskEdit.ReadOnly := not FAllowEdit;

  case FTableColumn.DataType.Index of
    dbdtDate:
      FMaskEdit.EditMask := '0000-00-00;1; ';

    dbdtDatetime, dbdtDatetime2, dbdtTimestamp,
    dbdtInt, dbdtBigint,
    dbdtFloat, dbdtDouble, dbdtDecimal, dbdtNumeric, dbdtReal, dbdtDoublePrecision: begin
        if FCellText.IsEmpty then
          FCellText := DateTimeToStr(Now);
        if MicroSecondsPrecision > 0 then
          FMaskEdit.EditMask := '0000-00-00 00\:00\:00.'+StringOfChar('0', MicroSecondsPrecision)+';1; '
        else
          FMaskEdit.EditMask := '0000-00-00 00\:00\:00;1; ';
      end;

    dbdtTime: begin
        ForceTextLen := 10;
        if MicroSecondsPrecision > 0 then begin
          FMaskEdit.EditMask := '#900\:00\:00.'+StringOfChar('0', MicroSecondsPrecision)+';1; ';
          Inc(ForceTextLen, MicroSecondsPrecision + 1);
        end else
          FMaskEdit.EditMask := '#900\:00\:00;1; ';
        while Length(FCellText) < ForceTextLen do
          FCellText := ' ' + FCellText;
      end;

    dbdtYear:
      FMaskEdit.EditMask := '0000;1; ';
  end;
  FMaskEdit.Text := FCellText;
  FModified := False;
  FMaskEdit.Font.Assign(FCellFont);
  FPanel.Color := FCellBackground;
  // Auto-enlarge current tree column so the text in the edit is not cut
  MinColWidth := FTree.Canvas.TextWidth(FCellText) + FTree.TextMargin + FUpDown.Width + 5;
  if FTree.Header.Columns[FColumn].Width < MinColWidth then
    FTree.Header.Columns[FColumn].Width := MinColWidth;
end;


procedure TDateTimeEditorLink.SetBounds(R: TRect); stdcall;
var
  EditRect: TRect;
  OldSelStart, OldSelLen: Integer;
begin
  FPanel.BoundsRect := GetCellRect(False);

  FUpDown.Left := FPanel.Width - FUpDown.Width;
  FUpDown.Height := FPanel.Height;

  EditRect := GetCellRect(True);
  EditRect.Right := FUpDown.Left;
  FMaskEdit.BoundsRect := EditRect;

  OldSelStart := FMaskEdit.SelStart;
  OldSelLen := FMaskEdit.SelLength;
  FMaskEdit.SelStart := 0;
  FMaskEdit.SelStart := OldSelStart;
  FMaskEdit.SelLength := OldSelLen;
end;


procedure TDateTimeEditorLink.DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  inherited DoKeyDown(Sender, Key, Shift);
  if (Key in [VK_UP, VK_DOWN]) and (not FTimer.Enabled) then begin
    if Key = VK_UP then FModifyOffset := 1
    else FModifyOffset := -1;
    FTimerCalls := 0;
    DoOnTimer(Sender);
    FTimer.Enabled := True;
  end;
end;


procedure TDateTimeEditorLink.DoKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  FTimer.Enabled := False;
end;


procedure TDateTimeEditorLink.UpDownChangingEx(Sender: TObject; var AllowChange: Boolean;
  NewValue: SmallInt; Direction: TUpDownDirection);
begin
  if FTimer.Enabled then
    Exit;
  if Direction = updUp then FModifyOffset := 1
  else FModifyOffset := -1;
  FTimerCalls := 0;
  DoOnTimer(Sender);
  FTimer.Enabled := True;
end;


procedure TDateTimeEditorLink.UpDownMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  FTimer.Enabled := False;
end;


procedure TDateTimeEditorLink.DoOnTimer(Sender: TObject);
var
  DelayCalls: Integer;
begin
  Inc(FTimerCalls);
  // short delay before counting up/down
  DelayCalls := 350 Div FTimer.Interval;
  if (FTimerCalls > DelayCalls) or (not (Sender is TTimer)) then
    ModifyDate(FModifyOffset);
  // Speed up counting in steps
  if FTimerCalls in [DelayCalls*5, DelayCalls*10] then begin
    if FModifyOffset > 0 then
      Inc(FModifyOffset, 3)
    else
      Dec(FModifyOffset, 3);
  end;
end;


procedure TDateTimeEditorLink.ModifyDate(Offset: Integer);
var
  dt: TDateTime;
  d: TDate;
  i, MaxSeconds, MinSeconds: Int64;
  text: String;
  OldSelStart, OldSelLength,
  ms: Integer;
  msStr, StrWithoutMs: String;

  function TimeToSeconds(Str: String): Int64;
  var
    Hours: String;
    Seconds: Int64;
  begin
    Hours := Trim(Copy(Str, 1, 4));
    Result := MakeInt(Hours) * 60 * 60;
    Seconds := MakeInt(Copy(Str, 6, 2)) * 60 + MakeInt(Copy(Str, 9, 2));
    if (Result < 0) or ((Result = 0) and (Hours[1]='-')) then
      Dec(Result, Seconds)
    else
      Inc(Result, Seconds);
  end;

  function SecondsToTime(Seconds: Int64): String;
  var
    HoursNum, Minutes: Int64;
    Hours: String;
  begin
    HoursNum := Abs(Seconds div (60*60));
    Hours := IntToStr(HoursNum);
    if Length(Hours) = 1 then
      Hours := '0' + Hours;
    if Seconds < 0 then
      Hours := '-' + Hours;
    Seconds := Abs(Seconds) mod (60*60);
    Minutes := Seconds div 60;
    Seconds := Seconds mod 60;
    Result := Format('%4s:%.2u:%.2u', [Hours, Minutes, Seconds]);
  end;
begin
  try
    // Detect microseconds part of value if any
    if MicroSecondsPrecision > 0 then begin
      msStr := RegExprGetMatch('\.(\d+)$', FMaskEdit.Text, 1);
      ms := MakeInt(msStr);
    end else begin
      ms := 0;
    end;

    case FTableColumn.DataType.Index of
      dbdtYear: begin
        i := MakeInt(FMaskEdit.Text);
        i := i + Offset;
        text := IntToStr(i);
      end;

      dbdtDate: begin
        d := StrToDate(FMaskEdit.Text);
        // De- or increase focused date segment
        case FMaskEdit.SelStart of
          0..3: d := IncYear(d, Offset);
          5,6: d := IncMonth(d, Offset);
          8..10: d := IncDay(d, Offset);
        end;
        text := DateToStr(d);
      end;

      dbdtDateTime, dbdtDateTime2, dbdtTimestamp,
      dbdtInt, dbdtBigint,
      dbdtFloat, dbdtDouble, dbdtDecimal, dbdtNumeric, dbdtReal, dbdtDoublePrecision: begin
        StrWithoutMs := ReplaceRegExpr('\.\d+$', FMaskEdit.Text, '');
        dt := StrToDateTime(StrWithoutMs);
        case FMaskEdit.SelStart of
          0..3: dt := IncYear(dt, Offset);
          5,6: dt := IncMonth(dt, Offset);
          8,9: dt := IncDay(dt, Offset);
          11,12: dt := IncHour(dt, Offset);
          14,15: dt := IncMinute(dt, Offset);
          17..19: dt := IncSecond(dt, Offset);
          20..26: Inc(ms, Offset);
        end;
        text := DateTimeToStr(dt);
        if Length(text) = 10 then
          text := text + ' 00:00:00';
        if MicroSecondsPrecision > 0 then
          text := text + '.' + Format('%.'+IntToStr(MicroSecondsPrecision)+'d', [ms]);
      end;

      dbdtTime: begin
        i := TimeToSeconds(FMaskEdit.Text);
        case FMaskEdit.SelStart of
          0..3: Inc(i, Offset*60*60);
          5,6: Inc(i, Offset*60);
          8,9: Inc(i, Offset);
          10..16: Inc(ms, Offset);
        end;
        // Stop at max and min values. See http://dev.mysql.com/doc/refman/5.0/en/time.html
        MaxSeconds := 839*60*60-1;
        MinSeconds := -(MaxSeconds);
        if i > MaxSeconds then
          i := MaxSeconds;
        if i < MinSeconds then
          i := MinSeconds;
        text := SecondsToTime(i);
        if MicroSecondsPrecision > 0 then
          text := text + '.' + Format('%.'+IntToStr(MicroSecondsPrecision)+'d', [ms]);
      end;

      else text := '';
    end;

    if text <> '' then begin
      OldSelStart := FMaskEdit.SelStart;
      OldSelLength := FMaskEdit.SelLength;
      FMaskEdit.Text := text;
      FMaskEdit.SelStart := OldSelStart;
      FMaskEdit.SelLength := OldSelLength;
    end;
  except
    on E:Exception do begin
      MainForm.LogSQL(E.Message);
    end;
  end;
end;


procedure TDateTimeEditorLink.TextChange;
begin
  FModified := True;
end;


function TDateTimeEditorLink.MicroSecondsPrecision: Integer;
var
  rx: TRegExpr;
  msStr: String;
begin
  case FTableColumn.DataType.Category of
    dtcTemporal: begin
      if not FTableColumn.LengthSet.IsEmpty then
        // Read microseconds precision from MySQL length/set
        Result := MakeInt(FTableColumn.LengthSet)
      else begin
        // Find default length of supported microseconds in datatype definition
        // See dbstructures
        rx := TRegExpr.Create;
        rx.Expression := '\.([^\.]+)$';
        if rx.Exec(FTableColumn.DataType.Format) then
          Result := rx.MatchLen[1]
        else
          Result := 0;
        rx.Free;
      end;
    end;

    dtcInteger: begin
      // UNIX timestamps from integers
      Result := 0;
    end;

    dtcReal: begin
      // UNIX timestamps from floats
      // Detect number of decimals from original cell string
      msStr := RegExprGetMatch('\.(\d+)$', FCellText, 1);
      Result := Length(msStr);
    end;

    else
      Result := 0;
  end;
end;



{ Enum editor }

constructor TEnumEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn);
begin
  inherited;
  AllowCustomText := False;
  ItemMustExist := False;
  FCombo := TExtComboBox.Create(FParentForm);
  FCombo.Hide;
  FCombo.Parent := FParentForm;
  FCombo.OnKeyDown := DoKeyDown;
  FCombo.OnExit := DoEndEdit;
  FCombo.OnSelect := DoSelect;
  // Show some more than the default 8 items
  FCombo.DropDownCount := 16;
  ValueList := TStringList.Create;
  DisplayList := TStringList.Create;
  FMainControl := FCombo;
end;


destructor TEnumEditorLink.Destroy;
begin
  FCombo.Free;
  inherited;
end;


function TEnumEditorLink.BeginEdit: Boolean; stdcall;
begin
  Result := inherited BeginEdit;
  if Result then begin
    FCombo.Show;
    FCombo.SetFocus;
  end;
end;


function TEnumEditorLink.EndEdit: Boolean; stdcall;
var
  NewText: String;
begin
  if AllowCustomText and FAllowEdit then begin
    if (not ItemMustExist) or ValueList.Contains(FCombo.Text) then
      NewText := FCombo.Text
    else
      NewText := '';
  end
  else if (ValueList.Count > 0) and (FCombo.ItemIndex > -1) then
    NewText := ValueList[FCombo.ItemIndex]
  else
    NewText := '';
  FModified := NewText <> FCellText;
  Result := EndEditHelper(NewText);
end;


function TEnumEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall;
begin
  Result := inherited PrepareEdit(Tree, Node, Column);
  if Result then begin
    if DisplayList.Count = ValueList.Count then
      FCombo.Items.AddStrings(DisplayList)
    else
      FCombo.Items.AddStrings(ValueList);
    FCombo.ItemIndex := ValueList.IndexOf(FCellText);
    if AllowCustomText and FAllowEdit then begin
      FCombo.Style := csDropDown;
      FCombo.Text := FCellText;
    end else begin
      // Set style to OwnerDraw, otherwise we wouldn't be able to adjust the combo's height
      FCombo.Style := csOwnerDrawFixed;
    end;
  end;
end;


procedure TEnumEditorLink.SetBounds(R: TRect); stdcall;
begin
  FCombo.BoundsRect := GetCellRect(False);
end;


procedure TEnumEditorLink.DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  // Work around a magic automatic TAB key arriving the editor if the user got
  // into this cell via TAB. Only seen for a TComboBox with style=csDropDown.
  // See issue #2809
  if (not AllowCustomText) and (GetTickCount-FBeginEditTime > 200) then
    inherited;
end;


procedure TEnumEditorLink.DoSelect(Sender: TObject);
begin
  // Read only mode?
  if not FAllowEdit then begin
    FCombo.ItemIndex := ValueList.IndexOf(FCellText);
  end;
end;



{ SET editor }

constructor TSetEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn);
begin
  inherited;
  ValueList := TStringList.Create;

  FPanel := TPanel.Create(FParentForm);
  FPanel.Hide;
  FPanel.Parent := FParentForm;
  FPanel.ParentBackground := False;
  FPanel.Height := TExtForm.ScaleSize(150, FParentForm);
  FPanel.OnExit := DoEndEdit;

  FCheckList := TCheckListBox.Create(FPanel);
  FCheckList.Parent := FPanel;
  FCheckList.OnKeyDown := DoKeyDown;
  FMainControl := FCheckList;

  FBtnOk := TButton.Create(FPanel);
  FBtnOk.Parent := FPanel;
  FBtnOk.Caption := _('OK');
  FBtnOk.OnClick := BtnOkClick;

  FBtnCancel := TButton.Create(FPanel);
  FBtnCancel.Parent := FPanel;
  FBtnCancel.Caption := _('Cancel');
  FBtnCancel.OnClick := BtnCancelClick;

  FEndTimer := TTimer.Create(FPanel);
  FEndTimer.Interval := 50;
  FEndTimer.Enabled := False;
end;


destructor TSetEditorLink.Destroy;
begin
  FreeAndNil(FPanel);
  inherited;
end;


function TSetEditorLink.BeginEdit: Boolean; stdcall;
begin
  Result := inherited BeginEdit;
  if Result then begin
    FPanel.Show;
    FCheckList.SetFocus;
  end;
end;


function TSetEditorLink.EndEdit: Boolean; stdcall;
var
  newtext: String;
  i: Integer;
begin
  Result := not FStopping;
  if FStopping then Exit;
  newText := '';
  for i := 0 to FCheckList.Items.Count - 1 do
    if FCheckList.Checked[i] then newText := newText + FCheckList.Items[i] + ',';
  Delete(newText, Length(newText), 1);
  FModified := newText <> FCellText;
  Result := EndEditHelper(newText);
end;


function TSetEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall;
var
  SelValues: TStringList;
  i: Integer;
begin
  Result := inherited PrepareEdit(Tree, Node, Column);
  if not Result then
    Exit;

  FCheckList.Font.Assign(FCellFont);
  FCheckList.Items.Assign(ValueList);
  SelValues := TStringList.Create;
  SelValues.Delimiter := ',';
  SelValues.StrictDelimiter := True;
  SelValues.DelimitedText := FCellText;
  for i:=0 to FCheckList.Items.Count-1 do begin
    FCheckList.Checked[i] := SelValues.IndexOf(FCheckList.Items[i]) > -1;
    FCheckList.ItemEnabled[i] := FAllowEdit;
  end;
  SelValues.Free;
end;


procedure TSetEditorLink.SetBounds(R: TRect); stdcall;
const
  margin = 5;
begin
  R := GetCellRect(False);
  FPanel.Top := R.Top;
  FPanel.Left := R.Left;
  FPanel.Width := R.Width;

  FBtnOk.Width := (FPanel.Width - 3*margin) div 2;
  FBtnOk.Left := margin;
  FBtnOk.Height := TExtForm.ScaleSize(24, FParentForm);
  FBtnOk.Top := FPanel.Height - 2*margin - FBtnOk.Height;
  FBtnOk.Enabled := FAllowEdit;

  FBtnCancel.Width := FBtnOk.Width;
  FBtnCancel.Left := 2*margin + FBtnOk.Width;
  FBtnCancel.Height := FBtnOk.Height;
  FBtnCancel.Top := FBtnOk.Top;

  FCheckList.Top := margin;
  FCheckList.Left := margin;
  FCheckList.Width := FPanel.Width - 2*margin;
  FCheckList.Height := FBtnOk.Top - margin - FCheckList.Top;
  // FCheckList.Enabled := FAllowEdit; // crashes with "cannot focus if disabled"
end;


procedure TSetEditorLink.BtnOkClick(Sender: TObject);
begin
  // Timer based click on OK button, to prevent crash when theming is active
  FEndTimer.OnTimer := DoEndEdit;
  FEndTimer.Enabled := True;
end;


procedure TSetEditorLink.BtnCancelClick(Sender: TObject);
begin
  // Timer based click on Cancel button, to prevent crash when theming is active
  FEndTimer.OnTimer := DoCancelEdit;
  FEndTimer.Enabled := True;
end;



{ TInplaceEditorLink }

constructor TInplaceEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn);
begin
  inherited;
  ButtonVisible := false;

  FPanel := TPanel.Create(FParentForm);
  FPanel.Parent := FParentForm;
  FPanel.Hide;
  FPanel.ParentBackground := False;
  FPanel.BevelOuter := bvNone;
  FPanel.OnExit := DoEndEdit;

  FEdit := TEdit.Create(FPanel);
  FEdit.Parent := FPanel;
  FEdit.ParentColor := True;
  FEdit.BorderStyle := bsNone;
  FEdit.AnchorSideLeft.Control := FPanel;
  FEdit.AnchorSideTop.Control := FPanel;
  FEdit.AnchorSideBottom.Control := FPanel;
  FEdit.AnchorSideBottom.Side := asrBottom;
  FEdit.Anchors := [akTop, akLeft, akRight, akBottom];
  FEdit.OnKeyDown := DoKeyDown;
  FMainControl := FEdit;

  FButton := TButton.Create(FPanel);
  FButton.Parent := FPanel;
  FButton.AnchorSideLeft.Side := asrBottom;
  FButton.AnchorSideTop.Control := FPanel;
  FButton.AnchorSideRight.Control := FPanel;
  FButton.AnchorSideRight.Side := asrBottom;
  FButton.AnchorSideBottom.Control := FPanel;
  FButton.AnchorSideBottom.Side := asrBottom;
  FButton.Anchors := [akTop, akRight, akBottom];
  FButton.Constraints.MaxWidth := TExtForm.ScaleSize(20, FPanel);
  FButton.TabStop := False;
  FButton.Caption := '...';
  FButton.Hint := _('Edit text in popup editor ...');
  FButton.ShowHint := True;
  FButton.OnClick := ButtonClick;

  FEdit.AnchorSideRight.Control := FButton;
end;

destructor TInplaceEditorLink.Destroy;
begin
  if not ((csDestroying in FPanel.ComponentState) or (csCreating in FPanel.ControlState)) then begin
    FEdit.Free;
    FButton.Free;
    FPanel.Free;
  end;
  inherited;
end;

function TInplaceEditorLink.BeginEdit: Boolean;
begin
  Result := inherited BeginEdit;
  if Result then begin
    FButton.Visible := ButtonVisible;
    SetBounds(Rect(0, 0, 0, 0));
    if (Length(FEdit.Text) > SIZE_KB) or (ScanLineBreaks(FEdit.Text) <> lbsNone) then
      ButtonClick(FTree)
    else begin
      FPanel.Show;
      FEdit.SetFocus;
    end;
  end;
end;


function TInplaceEditorLink.EndEdit: Boolean;
var
  NewText: String;
begin
  Result := not FStopping;
  if FStopping then Exit;
  NewText := FEdit.Text;
  FModified := NewText <> FCellText;
  Result := EndEditHelper(NewText);
end;

procedure TInplaceEditorLink.DoKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  inherited DoKeyDown(Sender, Key, Shift);
  if Key = VK_F2 then
    ButtonClick(FButton);
end;

procedure TInplaceEditorLink.ButtonClick(Sender: TObject);
var
  Editor: TfrmTextEditor;
begin
  if not FButton.Visible then Exit; // Button was invisible, but hotkey was pressed
  Editor := TfrmTextEditor.Create(FTree);
  Editor.SetFont(MainForm.SynMemoQuery.Font);
  Editor.SetText(FEdit.Text);
  if FEdit.HandleAllocated then begin
    Editor.MemoText.SelStart := FEdit.SelStart;
    Editor.MemoText.SelEnd := FEdit.SelStart + FEdit.SelLength;
  end;
  Editor.SetTitleText(TitleText);
  Editor.Modified := FEdit.Modified;
  Editor.SetMaxLength(FMaxLength);
  Editor.TableColumn := FTableColumn;
  Editor.MemoText.ReadOnly := not FAllowEdit;
  if Editor.ShowModal = mrYes then begin
    FEdit.Text := Editor.GetText;
    DoEndEdit(Sender);
  end
  else begin
    DoCancelEdit(Sender);
  end;
  Editor.Free;
end;

function TInplaceEditorLink.PrepareEdit(Tree: TBaseVirtualTree;
  Node: PVirtualNode; Column: TColumnIndex): Boolean;
begin
  Result := inherited PrepareEdit(Tree, Node, Column);
  if not Result then
    Exit;

  FEdit.ReadOnly := not FAllowEdit;
  FEdit.Font.Assign(FCellFont);
  FEdit.Font.Color := GetThemeColor(clWindowText);
  FPanel.Color := FCellBackground;
  FEdit.Text := FCellText;
  FEdit.Modified := False;
end;

procedure TInplaceEditorLink.SetBounds(R: TRect);
begin
  if not FStopping then begin
    // Position edit control according to cell text bounds
    FPanel.BoundsRect := GetCellRect(False);
    // R is too big in a grid, for some reason
  end;
end;



{ Column default editor }

constructor TColumnDefaultEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn);
var
  SQLFunc: TSQLFunction;
  m: Integer;
begin
  inherited;

  // Margin between controls and to edge of panel
  m := TExtForm.ScaleSize(5, FParentForm);

  FPanel := TPanel.Create(FParentForm);
  FPanel.Hide;
  FPanel.Parent := FParentForm;
  FPanel.OnExit := DoEndEdit;
  FPanel.ParentBackground := False;
  FPanel.Color := GetThemeColor(clWindow);
  //FPanel.BevelKind := bkFlat;
  FPanel.BevelOuter := bvRaised;
  FPanel.DoubleBuffered := True; // Avoid flicker?
  FMainControl := FPanel;

  FRadioNothing := TAllKeysRadioButton.Create(FPanel);
  FRadioNothing.Parent := FPanel;
  FRadioNothing.Top := m;
  FRadioNothing.Left := m;
  FRadioNothing.Width := FRadioNothing.Parent.Width - 2 * FRadioNothing.Left;
  FRadioNothing.OnClick := RadioClick;
  FRadioNothing.OnKeyDown := DoKeyDown;
  FRadioNothing.Caption := _('No default value');

  FRadioText := TAllKeysRadioButton.Create(FPanel);
  FRadioText.Parent := FPanel;
  FRadioText.Top := FRadioNothing.Top + FRadioNothing.Height + m;;
  FRadioText.Left := m;
  FRadioText.Width := FRadioText.Parent.Width - 2 * FRadioText.Left;
  FRadioText.OnClick := RadioClick;
  FRadioText.OnKeyDown := DoKeyDown;
  FRadioText.Caption := _('Custom text')+':';

  FTextDropDown := TPopupMenu.Create(FPanel);

  FTextEdit := TEditButton.Create(FPanel);
  FTextEdit.Parent := FPanel;
  FTextEdit.Top := FRadioText.Top + FRadioText.Height + m;
  FTextEdit.Left := 2*m;
  FTextEdit.Width := FTextEdit.Parent.Width - 2*FTextEdit.Left;
  FTextEdit.OnChange := EditChange;
  FTextEdit.Images := Tree.Images;
  FTextEdit.ImageIndex := 75; // Drop down arrow
  FTextEdit.OnButtonClick := EditButtonClick;
  //FTextEdit.RightButton.DropDownMenu := FTextDropDown;

  FRadioNull := TAllKeysRadioButton.Create(FPanel);
  FRadioNull.Parent := FPanel;
  FRadioNull.Top := FTextEdit.Top + FTextEdit.Height + 2*m;
  FRadioNull.Left := m;
  FRadioNull.Width := FRadioNull.Parent.Width - 2 * FRadioNull.Left;
  FRadioNull.OnClick := RadioClick;
  FRadioNull.OnKeyDown := DoKeyDown;
  FRadioNull.Caption := 'NULL';

  FRadioExpression := TAllKeysRadioButton.Create(FPanel);
  FRadioExpression.Parent := FPanel;
  FRadioExpression.Top := FRadioNull.Top + FRadioNull.Height + m;
  FRadioExpression.Left := m;
  FRadioExpression.Width := FRadioExpression.Parent.Width - 2 * FRadioExpression.Left;
  FRadioExpression.OnClick := RadioClick;
  FRadioExpression.OnKeyDown := DoKeyDown;
  FRadioExpression.Caption := _('Expression')+':';

  FExpressionEdit := TComboBox.Create(FPanel);
  FExpressionEdit.Parent := FPanel;
  FExpressionEdit.Top := FRadioExpression.Top + FRadioExpression.Height + m;
  FExpressionEdit.Left := 2*m;
  FExpressionEdit.Width := FExpressionEdit.Parent.Width - 2*FExpressionEdit.Left;
  FExpressionEdit.OnChange := EditChange;
  FExpressionEdit.DropDownCount := 20;
  for SQLFunc in FTableColumn.Connection.SQLFunctions do begin
    FExpressionEdit.Items.Add(SQLFunc.Name + SQLFunc.Declaration);
  end;

  FlblOnUpdate := TLabel.Create(FPanel);
  FlblOnUpdate.Parent := FPanel;
  FlblOnUpdate.Top := FExpressionEdit.Top + FExpressionEdit.Height + m;
  FlblOnUpdate.Left := 2*m;
  FlblOnUpdate.Width := FlblOnUpdate.Parent.Width - 2*FlblOnUpdate.Left;
  FlblOnUpdate.Caption := _('On update') + ':';

  FOnUpdateEdit := TComboBox.Create(FPanel);
  FOnUpdateEdit.Parent := FPanel;
  FOnUpdateEdit.Top := FlblOnUpdate.Top + FlblOnUpdate.Height + m;
  FOnUpdateEdit.Left := 2*m;
  FOnUpdateEdit.Width := FOnUpdateEdit.Parent.Width - 2*FOnUpdateEdit.Left;
  FOnUpdateEdit.OnChange := EditChange;
  FOnUpdateEdit.DropDownCount := 20;
  for SQLFunc in FTableColumn.Connection.SQLFunctions do begin
    FOnUpdateEdit.Items.Add(SQLFunc.Name + SQLFunc.Declaration);
  end;

  FRadioAutoInc := TAllKeysRadioButton.Create(FPanel);
  FRadioAutoInc.Parent := FPanel;
  FRadioAutoInc.Top := FOnUpdateEdit.Top + FOnUpdateEdit.Height + m;
  FRadioAutoInc.Left := m;
  FRadioAutoInc.Width := FRadioAutoInc.Parent.Width - 2 * FRadioAutoInc.Left;
  FRadioAutoInc.OnClick := RadioClick;
  FRadioAutoInc.OnKeyDown := DoKeyDown;
  FRadioAutoInc.Caption := Col.AutoIncName;

  FBtnOk := TButton.Create(FPanel);
  FBtnOk.Parent := FPanel;
  FBtnOk.Width := TExtForm.ScaleSize(60, FParentForm);
  FBtnOk.Top := FRadioAutoInc.Top + FRadioAutoInc.Height + m;
  FBtnOk.Left := FPanel.Width - 3*m - 2*FBtnOk.Width - 2*FPanel.BorderWidth;
  FBtnOk.OnClick := BtnOkClick;
  FBtnOk.Default := True;
  FBtnOk.Caption := _('OK');

  FBtnCancel := TButton.Create(FPanel);
  FBtnCancel.Parent := FPanel;
  FBtnCancel.Top := FBtnOk.Top;
  FBtnCancel.Width := FBtnOk.Width;
  FBtnCancel.Left := FBtnOk.Left + FBtnOk.Width + m;
  FBtnCancel.OnClick := BtnCancelClick;
  FBtnCancel.Cancel := True;
  FBtnCancel.Caption := _('Cancel');

  FEndTimer := TTimer.Create(FPanel);
  FEndTimer.Interval := 50;
  FEndTimer.Enabled := False;

  // Set outer panel (minimum) dimensions. Width is set in .SetBounds()
  FPanel.Height := 2*FPanel.BorderWidth + FBtnOk.Top + FBtnOk.Height + 2*m;
  FPanel.Constraints.MinWidth := 2*m + FBtnOK.Width + m + FBtnCancel.Width + 2*m;

  // Set anchors for all controls, so they are sticky when resizing the underlying column width
  FRadioNothing.Anchors := [akLeft, akTop, akRight];
  FRadioText.Anchors := [akLeft, akTop, akRight];
  FTextEdit.Anchors := [akLeft, akTop, akRight, akBottom];
  FRadioNull.Anchors := [akLeft, akBottom, akRight];
  FRadioExpression.Anchors := [akLeft, akBottom, akRight];
  FExpressionEdit.Anchors := [akLeft, akBottom, akRight];
  FOnUpdateEdit.Anchors := [akLeft, akBottom, akRight];
  FRadioAutoInc.Anchors := [akLeft, akBottom, akRight];
  FBtnOk.Anchors := [akBottom, akRight];
  FBtnCancel.Anchors := FBtnOk.Anchors;
end;


destructor TColumnDefaultEditorLink.Destroy;
begin
  FPanel.Free;
  inherited;
end;


function TColumnDefaultEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean; stdcall;
var
  ValueList, SelectedValues: TStringList;
  i: Integer;
  Item: TMenuItem;
begin
  inherited PrepareEdit(Tree, Node, Column);

  // Check relevant radio button
  FRadioNothing.Checked := DefaultType = cdtNothing;
  FRadioText.Checked := DefaultType = cdtText;
  FRadioNull.Checked := DefaultType = cdtNull;
  FRadioExpression.Checked := DefaultType = cdtExpression;
  FRadioAutoInc.Checked := DefaultType = cdtAutoInc;

  if FRadioText.Checked then begin
    FTextEdit.Text := DefaultText;
  end;

  if FRadioExpression.Checked then begin
    FExpressionEdit.Text := DefaultText;
  end;

  FOnUpdateEdit.Text := OnUpdateText;

  // Disable non working default options per data type
  FRadioAutoInc.Enabled := FRadioAutoInc.Checked or (FTableColumn.DataType.Category = dtcInteger);

  // Provide items with a check mark for ENUM and SET columns
  if FTableColumn.DataType.Index in [dbdtEnum, dbdtSet] then begin
    FTextEdit.Button.Enabled := True;
    ValueList := FTableColumn.ValueList;
    SelectedValues := Explode(',', FTextEdit.Text);

    for i:=0 to ValueList.Count-1 do begin
      Item := TMenuItem.Create(FTextDropDown);
      Item.Caption := ValueList[i];
      Item.RadioItem := FTableColumn.DataType.Index = dbdtEnum;
      Item.Checked := SelectedValues.IndexOf(Item.Caption) > -1;
      Item.OnClick := EditDropDownClick;
      FTextDropDown.Items.Add(Item);
    end;

    ValueList.Free;
    SelectedValues.Free;
  end;

  Result := True;
end;


procedure TColumnDefaultEditorLink.SetBounds(R: TRect); stdcall;
var
  CellRect: TRect;
  P: TPoint;
  Room: Integer;
begin
  CellRect := GetCellRect(False);
  FPanel.Left := CellRect.Left;
  FPanel.Top := CellRect.Top;
  FPanel.Width := CellRect.Width;

  // Reposition editor so it's not outside the main form
  P := FParentForm.ClientToScreen(FPanel.BoundsRect.TopLeft);
  Room := FParentForm.BoundsRect.Bottom - 8 {Borderwidth} - (P.Y + FPanel.Height);
  if Room < 0 then
    FPanel.Top := CellRect.Top + Room;
  P := FParentForm.ClientToScreen(FPanel.BoundsRect.BottomRight);
  Room := FParentForm.BoundsRect.Right - 8 {Borderwidth} - P.X;
  if Room < 0 then
    FPanel.Left := CellRect.Left + Room;
end;


function TColumnDefaultEditorLink.BeginEdit: Boolean; stdcall;
begin
  Result := not FStopping;
  if Result then begin
    FPanel.Show;
    if FRadioNothing.Checked then FRadioNothing.SetFocus
    else if FRadioText.Checked then FTextEdit.SetFocus
    else if FRadioNull.Checked then FRadioNull.SetFocus
    else if FRadioExpression.Checked then FExpressionEdit.SetFocus
    else if FRadioAutoInc.Checked then FRadioAutoInc.SetFocus;
  end;
end;


function TColumnDefaultEditorLink.EndEdit: Boolean; stdcall;
var
  Col: PTableColumn;
begin
  Result := not FStopping;
  if Result then begin
    FStopping := True;
    Col := FTree.GetNodeData(FNode);

    if FRadioNothing.Checked then
      Col.DefaultType := cdtNothing
    else if FRadioText.Checked then
      Col.DefaultType := cdtText
    else if FRadioNull.Checked then
      Col.DefaultType := cdtNull
    else if FRadioExpression.Checked then
      Col.DefaultType := cdtExpression
    else if FRadioAutoInc.Checked then
      Col.DefaultType := cdtAutoInc
    else
      Col.DefaultType := cdtText;

    case Col.DefaultType of
      cdtNothing: Col.DefaultText := '';
      cdtText: Col.DefaultText := FTextEdit.Text;
      cdtNull: Col.DefaultText := 'NULL';
      cdtExpression: Col.DefaultText := FExpressionEdit.Text;
      cdtAutoInc: Col.DefaultText := Col.AutoIncName;
    end;

    if FOnUpdateEdit.Text <> '' then
      Col.OnUpdateType := cdtExpression
    else
      Col.OnUpdateType := cdtNothing;
    Col.OnUpdateText := FOnUpdateEdit.Text;

    FTree.Text[FNode, FColumn] := Col.DefaultText;
    if FTree.CanFocus then
      FTree.SetFocus;
  end;
end;


procedure TColumnDefaultEditorLink.RadioClick(Sender: TObject);
begin
  if not FRadioText.Checked then
    FTextEdit.Color := clBtnFace
  else begin
    FTextEdit.Color := clWindow;
    if FTextEdit.CanFocus then
      FTextEdit.SetFocus;
  end;
  if not FRadioExpression.Checked then
    FExpressionEdit.Color := clBtnFace
  else begin
    FExpressionEdit.Color := clWindow;
    if FExpressionEdit.CanFocus then
      FExpressionEdit.SetFocus;
  end;
  FModified := True;
end;


procedure TColumnDefaultEditorLink.EditChange(Sender: TObject);
begin
  if Sender = FTextEdit then
    FRadioText.Checked := True
  else if Sender = FExpressionEdit then
    FRadioExpression.Checked := True;
  FModified := True;
end;


procedure TColumnDefaultEditorLink.EditButtonClick(Sender: TObject);
begin
  TExtForm.ShowPopup(FTextEdit.Button, FTextDropDown);
end;

procedure TColumnDefaultEditorLink.EditDropDownClick(Sender: TObject);
var
  Item: TMenuItem;
  NewValue: String;
begin
  // ENUM or SET value clicked in drop down menu
  Item := Sender as TMenuItem;
  Item.Checked := not Item.Checked;
  NewValue := '';
  for Item in Item.GetParentMenu.Items do begin
    if Item.Checked then
      NewValue := NewValue + StripHotkey(Item.Caption) + ',';
  end;
  if not NewValue.IsEmpty then
    Delete(NewValue, Length(NewValue), 1);
  FTextEdit.Text := NewValue;
  FModified := True;
end;

procedure TColumnDefaultEditorLink.BtnOkClick(Sender: TObject);
begin
  // Timer based click on OK button, to prevent crash when theming is active
  FEndTimer.OnTimer := DoEndEdit;
  FEndTimer.Enabled := True;
end;

procedure TColumnDefaultEditorLink.BtnCancelClick(Sender: TObject);
begin
  // Timer based click on Cancel button, to prevent crash when theming is active
  FEndTimer.OnTimer := DoCancelEdit;
  FEndTimer.Enabled := True;
end;




{ Datatype selector }
constructor TDataTypeEditorLink.Create(Tree: TVirtualStringTree; AllowEdit: Boolean; Col: TTableColumn);
begin
  inherited;

  FTreeSelect := TVirtualStringTree.Create(FParentForm);
  FTreeSelect.Hide;
  FTreeSelect.TreeOptions.PaintOptions := FTreeSelect.TreeOptions.PaintOptions
    - [toShowTreeLines, toShowButtons, toShowRoot]
    + [toHotTrack, toUseExplorerTheme, toHideTreeLinesIfThemed];
  FTreeSelect.TreeOptions.SelectionOptions := FTreeSelect.TreeOptions.SelectionOptions
    + [toFullRowSelect];
  FTreeSelect.Header.Columns.Add;
  FTreeSelect.Parent := FParentForm;
  FTreeSelect.TextMargin := 0;
  //FTreeSelect.BorderStyle := bsNone;
  //FTreeSelect.BevelKind := bkFlat;
  //FTreeSelect.BevelInner := bvNone;
  FTreeSelect.IncrementalSearch := isAll;
  FTreeSelect.RootNodeCount := Length(DatatypeCategories);
  FTreeSelect.OnGetText := DoTreeSelectGetText;
  FTreeSelect.OnInitNode := DoTreeSelectInitNode;
  FTreeSelect.OnInitChildren := DoTreeSelectInitChildren;
  FTreeSelect.OnKeyDown := DoKeyDown;
  FTreeSelect.OnHotChange := DoTreeSelectHotChange;
  FTreeSelect.OnPaintText := DoTreeSelectPaintText;
  FTreeSelect.OnExit := DoEndEdit;
  // See further events in PrepareEdit
  FixVT(FTreeSelect);
  FMainControl := FTreeSelect;

  FMemoHelp := TMemo.Create(FParentForm);
  FMemoHelp.Hide;
  FMemoHelp.Parent := FParentForm;
  FMemoHelp.Color := clInfoBk;
  FMemoHelp.Font.Color := clInfoText;
  //FMemoHelp.BevelKind := bkFlat;
  {if TStyleManager.IsCustomStyleActive then begin
    FMemoHelp.BorderStyle := bsSingle;
    FMemoHelp.BevelInner := bvNone;
  end else begin
    FMemoHelp.BorderStyle := bsNone;
    FMemoHelp.BevelInner := bvSpace;
  end;}
end;


destructor TDataTypeEditorLink.Destroy;
begin
  FreeAndNil(FTreeSelect);
  FreeAndNil(FMemoHelp);
  inherited;
end;


function TDataTypeEditorLink.PrepareEdit(Tree: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex): Boolean;
var
  dt: TDBDatatype;
  CatNode, TypeNode: PVirtualNode;
begin
  Result := inherited PrepareEdit(Tree, Node, Column);
  if not Result then
    Exit;
  // Just use font name and size, avoid bold style and white color
  FTreeSelect.Font.Name := FCellFont.Name;
  FTreeSelect.Font.Size := FCellFont.Size;

  // Find and select current datatype in tree
  dt := FTableColumn.Connection.GetDataTypeByName(FCellText, False, FTableColumn.Name);
  CatNode := FTreeSelect.GetFirst;
  while Assigned(CatNode) do begin
    // Since recent update to VT 5.2.1 we need to initialize root nodes by hand for some reason:
    FTreeSelect.ReinitNode(CatNode, True);
    if CatNode.Index = Cardinal(dt.Category) then begin
      TypeNode := FTreeSelect.GetFirstChild(CatNode);
      while Assigned(TypeNode) do begin
        if FTreeSelect.Text[TypeNode, 0] = FCellText then begin
          FTreeSelect.FocusedNode := TypeNode;
          FTreeSelect.Selected[TypeNode] := True;
          break;
        end;
        TypeNode := FTreeSelect.GetNextSibling(TypeNode);
      end;
    end;
    CatNode := FTreeSelect.GetNextSibling(CatNode);
  end;
  FTreeSelect.Header.AutoFitColumns(False, smaUseColumnOption, 0, 0);
  if Assigned(FTreeSelect.FocusedNode) then
    FTreeSelect.ScrollIntoView(FTreeSelect.FocusedNode, True);
  FTreeSelect.OnFocusChanging := DoTreeSelectFocusChanging;
  FTreeSelect.OnFocusChanged := DoTreeSelectFocusChanged;
  FTreeSelect.OnClick := DoEndEdit;
end;


function TDataTypeEditorLink.BeginEdit: Boolean;
begin
  Result := inherited BeginEdit;
  if Result then begin
    FTreeSelect.Show;
    FTreeSelect.SetFocus;
  end;
end;


function TDataTypeEditorLink.EndEdit: Boolean;
begin
  if Assigned(FTreeSelect.FocusedNode) then
    Result := EndEditHelper(FTreeSelect.Text[FTreeSelect.FocusedNode, 0])
  else
    Result := FTree.CancelEditNode;
end;


procedure TDataTypeEditorLink.SetBounds(R: TRect);
var
  CellRect: TRect;
  TreeHeight: Integer;
begin
  // Set position of tree. As the tree's parent is mainform, not listcolumns, add listcolumn's x + y positions
  CellRect := GetCellRect(False);
  // Do not exceed lower edge of mainform, as that portion would be hidden
  TreeHeight := Min(250, FParentForm.ClientHeight-CellRect.Top-10);
  FTreeSelect.SetBounds(CellRect.Left,
    CellRect.Top,
    FTreeSelect.Header.Columns[0].Width + GetSystemMetrics(SM_CXVSCROLL) + 5,
    TreeHeight);
end;


procedure TDataTypeEditorLink.DoTreeSelectInitNode(Sender: TBaseVirtualTree;
  ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
begin
  // First level nodes always expanded
  if Sender.GetNodeLevel(Node) = 0 then
    InitialStates := InitialStates + [ivsExpanded, ivsHasChildren];
end;


procedure TDataTypeEditorLink.DoTreeSelectInitChildren(Sender: TBaseVirtualTree;
  Node: PVirtualNode; var ChildCount: Cardinal);
var
  i: Integer;
begin
  // Tell number of datatypes per category
  ChildCount := 0;
  if Sender.GetNodeLevel(Node) = 0 then for i:=0 to High(FTableColumn.Connection.Datatypes) do begin
    if FTableColumn.Connection.Datatypes[i].Category = TDBDatatypeCategoryIndex(Node.Index) then
      Inc(ChildCount);
  end;
end;


procedure TDataTypeEditorLink.DoTreeSelectGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
  Column: TColumnIndex; TextType: TVSTTextType; var CellText: String);
var
  i: Integer;
  Counter: Cardinal;
begin
  // Get cell text
  case Sender.GetNodeLevel(Node) of
    0: CellText := DatatypeCategories[TDBDatatypeCategoryIndex(Node.Index)].Name;
    1: begin
         Counter := 0;
         for i:=0 to High(FTableColumn.Connection.Datatypes) do begin
           if FTableColumn.Connection.Datatypes[i].Category = TDBDatatypeCategoryIndex(Node.Parent.Index) then begin
             Inc(Counter);
             if Counter = Node.Index+1 then begin
               CellText := FTableColumn.Connection.Datatypes[i].Name;
               break;
             end;
           end;
         end;
       end;
  end;
end;


procedure TDataTypeEditorLink.DoTreeSelectHotChange(Sender: TBaseVirtualTree; OldNode, NewNode: PVirtualNode);
var
  R: TRect;
  NodeText: String;
  bmp: TBitMap;
begin
  // Display help box for hovered datatype
  FMemoHelp.Clear;
  if Assigned(NewNode) and (Sender.GetNodeLevel(NewNode) = 1) then begin
    R := FTreeSelect.GetDisplayRect(NewNode, 0, False);
    NodeText := FTreeSelect.Text[NewNode, 0];
    FMemoHelp.Width := Min(250, FTreeSelect.Left);
    FMemoHelp.Left := FTreeSelect.Left - FMemoHelp.Width + (Integer(FTreeSelect.Indent) Div 2);
    FMemoHelp.Top := FTreeSelect.Top + R.Top + 3;
    FMemoHelp.Text := FTableColumn.Connection.GetDatatypeByName(NodeText, False, FTableColumn.Name).Description;
    // Calc height of memo
    bmp := TBitMap.Create;
    bmp.Canvas.Font.Assign(FMemoHelp.Font);
    R := Rect(0, 0, FMemoHelp.Width-10, 0);
    DrawText(bmp.Canvas.Handle, PChar(FMemoHelp.Text), Length(FMemoHelp.Text), R, DT_WORDBREAK or DT_CALCRECT);
    FreeAndNil(bmp);
    FMemoHelp.Height := R.Bottom + 8;
    FMemoHelp.Show;
  end;
  if FMemoHelp.GetTextLen = 0 then
    FMemoHelp.Hide;
end;


procedure TDataTypeEditorLink.DoTreeSelectPaintText(Sender: TBaseVirtualTree;
  const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
  TextType: TVSTTextType);
begin
  // Give datatype column specific color, as set in preferences
  case Sender.GetNodeLevel(Node) of
    0: TargetCanvas.Font.Style := TargetCanvas.Font.Style + [fsBold];
    1: if not (vsSelected in Node.States) then
      TargetCanvas.Font.Color := AppColorSchemes.First.GridTextColors[TDBDatatypeCategoryIndex(Node.Parent.Index)];
  end;
end;


procedure TDataTypeEditorLink.DoTreeSelectFocusChanging(Sender: TBaseVirtualTree; OldNode, NewNode:
    PVirtualNode; OldColumn, NewColumn: TColumnIndex; var Allowed: Boolean);
var
  JumpToNode: PVirtualNode;
begin
  // Allow only 2nd level datatypes to be focused, not their category
  Allowed := Sender.GetNodeLevel(NewNode) = 1;
  if not Allowed then begin
    JumpToNode := nil;
    if FLastKeyDown = VK_UP then
      JumpToNode := Sender.GetPrevious(NewNode)
    else if FLastKeyDown = VK_DOWN then
      JumpToNode := Sender.GetNext(NewNode);
    if Assigned(JumpToNode) then
      Sender.FocusedNode := JumpToNode;
  end;
end;


procedure TDataTypeEditorLink.DoTreeSelectFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex);
begin
  FModified := True;
end;


end.
