unit ASTableEditor;

{ **************************************************************************** }
{ Rejbrand AlgoSim Table Editor                                                }
{ Copyright © 2017 Andreas Rejbrand                                            }
{ https://english.rejbrand.se/                                                 }
{ **************************************************************************** }

interface

uses
  Windows, Messages, SysUtils, Types, Classes, Graphics, Controls, Menus,
  ASTable, ASAttributes, TextEditor, Forms, UITypes;

type
  TCanEditCellQuery = procedure(X, Y: integer; var CanEdit: boolean) of object;

  TASTableEditor = class(TCustomControl)
  strict private type
    TDirection = (dirUp, dirRight, dirDown, dirLeft);
    TCellStyle = record
      BackgroundColor: TColor;
      BorderWidth: integer;
      BorderColor: TColor;
      FontFamily: string;
      FontSize: integer;
      TextColor: TColor;
      FontStyles: TFontStyles;
      TextAlign: TAlignment;
      NoEllipsis: boolean;
      Padding: integer;
    end;
  const
    WM_MOUSEHWHEEL = $020E;
  var
    FTable: TASTable;
    FActiveCell: TPoint;
    FSelAnchor: TPoint;
    FSelectedAll: boolean;
    FOldActiveCell: TPoint;
    FOldSelAnchor: TPoint;
    FCellWidth: integer;
    FCellHeight: integer;
    FBorderWidth: integer;
    FHorGridWidth, FVertGridWidth: integer;
    FEditor: TTextEditor;
    FDoubleClicked: boolean;
    FPopupMenu: TPopupMenu;
    FScrollPos: TPoint;
    FSPIScrollLines: integer;
    FReadOnly: boolean;
    FOnCanEdit: TCanEditCellQuery;
    FScrollAnchor: TPoint;
    FColLefts: array of integer;
    FColWidths: array of integer;
    FNoSelection: boolean;
    procedure SetTable(const Value: TASTable);
    function ApplyStyle(const AStyle: TASAttributes): TCellStyle;
    function GetCellRect(const ACell: TPoint): TRect;
    procedure SetCellHeight(const Value: integer);
    procedure SetCellWidth(const Value: integer);
    procedure SetActiveCell(const Value: TPoint; AExpandSel: boolean; AScrollToActive: boolean = true); overload;
    procedure SetActiveCell(const Value: TPoint); overload;
    procedure EditorKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure EditorExit(Sender: TObject);
    procedure MenuItemClicked(Sender: TObject);
    procedure WMVScroll(var Message: TWMVScroll); message WM_VSCROLL;
    procedure SetScrollPosY(const Y: integer);
    procedure UpdateScrollBars;
    procedure WMHScroll(var Message: TWMHScroll); message WM_HSCROLL;
    procedure SetScrollPosX(const X: integer);
    function GetFirstVisibleRow: integer;
    function GetLastVisibleRow: integer;
    function GetFirstVisibleCol: integer;
    function GetLastVisibleCol: integer;
    procedure UpdateSPI;
    procedure SetScrollPosXY(const X, Y: integer);
    function ScrollMode: boolean; overload;
    function ScrollMode(AAlt: boolean): boolean; overload;
    procedure ChangeCursor;
    function GetSelection(const APoint1, APoint2: TPoint): TRect; overload;
    function GetSelection: TRect; overload; inline;
    function GetIsSelected(const ACell: TPoint): boolean;
    function GetSelRect(const ASel: TRect): TRect;
    procedure SetSelection(const Value: TRect);
    procedure SaveEdit(FillSel: boolean = false);
    procedure GUISetDesign;
    procedure TableDesignApply(Sender: TObject);
    procedure GUICellStyle;
    procedure GUITransformResult(AType: integer);
    procedure GUIQuickDesign;
    procedure SetNoSelection(const Value: boolean);
  protected
    procedure DrawCell(const ACell: TPoint);
    procedure DrawCellNbh(const ACell: TPoint);
    procedure DrawBorder;
    procedure DrawGridLines;
    procedure ClearClientArea;
    procedure Paint; override;
    procedure CreateParams(var Params: TCreateParams); override;
    procedure WndProc(var Message: TMessage); override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
      Y: Integer); override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
    procedure DblClick; override;
    procedure KeyPress(var Key: Char); override;
    procedure WMContextMenu(var Message: TWMContextMenu);
      message WM_CONTEXTMENU;
    procedure WMMouseWheel(var Message: TWMMouseWheel); message WM_MOUSEWHEEL;
    procedure WMMouseHWheel(var Message: TWMMouseWheel); message WM_MOUSEHWHEEL;
    procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer;
      Y: Integer); override;
    procedure KeyUp(var Key: Word; Shift: TShiftState); override;
    procedure ShowEditor(const AInitialText: string = '');
    procedure HideEditor;
    function CanEdit(const ACell: TPoint): boolean;
    procedure Resize; override;
    function TableRect: TRect;
    procedure GUISetSize;
    procedure UpdateAll;
    function  Go(ADirection: TDirection; AJump: boolean = false): TPoint; overload;
      inline;
    function Go(const AOrigin: TPoint; ADirection: TDirection;
      AJump: boolean = false): TPoint; overload;
    function NextCell(const AFrom: TPoint): TPoint;
    function PrevCell(const AFrom: TPoint): TPoint;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    function GetTotalVerticalExtent: integer;
    function GetTotalHorizontalExtent: integer;

    property Table: TASTable read FTable write SetTable;
    property ActiveCell: TPoint read FActiveCell write SetActiveCell;
    property Selection: TRect read GetSelection write SetSelection;
    property CellRect[const ACell: TPoint]: TRect read GetCellRect;
    property SelRect[const ASel: TRect]: TRect read GetSelRect;
    property Selected[const ACell: TPoint]: boolean read GetIsSelected;
    function GetCellAt(const X, Y: integer): TPoint;
    function CellHitTest(const X, Y: integer): boolean;

    procedure ClearCurrentCell;
    function CurrentCellText: string;
    procedure CopyCurrentCellToClipboard;
    procedure CutCurrentCellToClipboard;
    procedure PasteFromClipboardToCurrentCell;

    procedure ClearSelection;
    function SelectionText: string;
    procedure CopyToClipboard;
    procedure CutToClipboard;
    procedure PasteFromClipboard;
    procedure SaveTableToFile;

    procedure SelectAll;
    procedure SelectColumn(AIndex: integer);
    procedure SelectRow(AIndex: integer);
    function HasSelection: boolean; inline;

    procedure InsertRow(AIndex: integer = -1);
    procedure InsertCol(AIndex: integer = -1);

    procedure DeleteRow(AIndex: integer = -1);
    procedure DeleteCol(AIndex: integer = -1);

    property FirstVisibleCol: integer read GetFirstVisibleCol;
    property LastVisibleCol: integer read GetLastVisibleCol;
    property FirstVisibleRow: integer read GetFirstVisibleRow;
    property LastVisibleRow: integer read GetLastVisibleRow;

    property TotalHorizontalExtent: integer read GetTotalHorizontalExtent;
    property TotalVerticalExtent: integer read GetTotalVerticalExtent;

    procedure UpdateMetricsFromStyle;
  published
    property Align;
    property AlignWithMargins;
    property Anchors;
    property AutoSize;
    property CellWidth: integer read FCellWidth write SetCellWidth default 100;
    property CellHeight: integer read FCellHeight write SetCellHeight default 32;
    property Color default clWindow;
    property Cursor;
    property Constraints;
    property DoubleBuffered;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property Font;
    property Margins;
    property NoSelection: boolean read FNoSelection write SetNoSelection default false;
    property ParentColor;
    property ParentDoubleBuffered;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ReadOnly: boolean read FReadOnly write FReadOnly default false;
    property ShowHint;
    property TabOrder;
    property TabStop;
    property Visible;
    property OnCanEdit: TCanEditCellQuery read FOnCanEdit write FOnCanEdit;
    property OnClick;
    property OnContextPopup;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDock;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnMouseActivate;
    property OnMouseDown;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnMouseMove;
    property OnMouseUp;
    property OnStartDock;
    property OnStartDrag;
  end;

function EditTable(ATable: TASTable; AOwner: TCustomForm;
  const ACaption: string = ''): boolean;

procedure Register;

implementation

uses
  Dialogs, Math, ASColors, Character, Clipbrd, IMouse, MultiInput,
  StdCtrls, ComCtrls, BitmapEffects, TableStyleDialogWin, TableDesignWin,
  StrUtils, TableQuickDesignWin;

{ TASTableEditor }

function TASTableEditor.ApplyStyle(const AStyle: TASAttributes): TCellStyle;
var
  vals: string;
begin

  if not (AStyle.TryGetValue('background-color', vals) and TryStrToColor(vals, result.BackgroundColor)) then
    result.BackgroundColor := Color;

  if not (AStyle.TryGetValue('border-color', vals) and TryStrToColor(vals, result.BorderColor)) then
    result.BorderColor := Font.Color;

  if not (AStyle.TryGetValue('border-width', vals) and TryStrToInt(vals, result.BorderWidth)) then
    result.BorderWidth := 0;

  if not AStyle.TryGetValue('font-family', result.FontFamily) then
    result.FontFamily := Font.Name;

  if not (AStyle.TryGetValue('font-size', vals) and TryStrToInt(vals, result.FontSize)) then
    result.FontSize := Font.Size;

  if not (AStyle.TryGetValue('text-color', vals) and TryStrToColor(vals, result.TextColor)) then
    result.TextColor := Font.Color;

  result.FontStyles := Font.Style;

  if AStyle.TryGetValue('bold', vals) then
    if SameText(vals, 'true') then
      result.FontStyles := result.FontStyles + [fsBold]
    else if SameText(vals, 'false') then
      result.FontStyles := result.FontStyles - [fsBold];

  if AStyle.TryGetValue('italic', vals) then
    if SameText(vals, 'true') then
      result.FontStyles := result.FontStyles + [fsItalic]
    else if SameText(vals, 'false') then
      result.FontStyles := result.FontStyles - [fsItalic];

  if AStyle.TryGetValue('underline', vals) then
    if SameText(vals, 'true') then
      result.FontStyles := result.FontStyles + [fsUnderline]
    else if SameText(vals, 'false') then
      result.FontStyles := result.FontStyles - [fsUnderline];

  if AStyle.TryGetValue('strike-through', vals) then
    if SameText(vals, 'true') then
      result.FontStyles := result.FontStyles + [fsStrikeOut]
    else if SameText(vals, 'false') then
      result.FontStyles := result.FontStyles - [fsStrikeOut];

  result.TextAlign := taLeftJustify;
  if AStyle.TryGetValue('text-align', vals) then
    if SameText(vals, 'center') then
      result.TextAlign := taCenter
    else if SameText(vals, 'right') then
      result.TextAlign := taRightJustify;

  result.NoEllipsis := AStyle.TryGetValue('no-ellipsis', vals) and
    SameText(vals, 'true');

  if not (AStyle.TryGetValue('padding', vals) and TryStrToInt(vals, result.Padding)) then
    result.Padding := 4;

end;

procedure TASTableEditor.ClearCurrentCell;
begin
  if FTable.CellExists(ActiveCell) and CanEdit(ActiveCell) then
  begin
    FTable[ActiveCell] := '';
    DrawCell(ActiveCell);
  end
  else
    Beep;
end;

procedure TASTableEditor.ClearSelection;
var
  P, Q: TPoint;
  y, x: integer;
begin
  P := Selection.TopLeft;
  Q := Selection.BottomRight;
  for y := P.Y to Q.Y do
    for x := P.X to Q.X do
      if CanEdit(Point(x, y)) then
        FTable[Point(x, y)] := '';
  InvalidateRect(Handle, GetSelRect(Selection), false);
end;

procedure TASTableEditor.CopyCurrentCellToClipboard;
begin
  Clipboard.AsText := CurrentCellText;
end;

procedure TASTableEditor.CopyToClipboard;
begin
  Clipboard.AsText := SelectionText;
end;

resourcestring
  SEdit = 'Edit cell';
  SSetSize = 'Set table size...';
  SDesign = 'Change table style...';
  SCellStyle = 'Change cell style...';
  SQuickDesign = 'Quick style...';
  SClear = 'Clear';
  SCut = 'Cut';
  SCopy = 'Copy';
  SPaste = 'Paste';
  SSelectAll = 'Select all';
  SSave = 'Save to file...';
  SSaveDialogFilter = 'Text files (*.txt)|*.txt|All files|*.*';
  STransform = 'Transform';
  STranspose = 'Transpose';
  SVectorization = 'To list (column-major order)';
  STransposeVectorization = 'To list (row-major order)';
  STransformResultCaption = 'Transformation';
  SInsert = 'Insert';
  SRow = 'Row';
  SColumn = 'Column';
  SDelete = 'Delete';

const
  MENU_EDIT = 10;
  MENU_CLEAR = 11;
  MENU_CUT = 12;
  MENU_COPY = 13;
  MENU_PASTE = 14;
  MENU_SELECT_ALL = 15;
  MENU_SAVE = 100;
  MENU_SET_SIZE = 101;
  MENU_DESIGN = 102;
  MENU_CELL_STYLE = 103;
  MENU_QUICK_DESIGN = 104;
  MENU_TRANSFORM_SUBMENU = 200;
  MENU_TRANSPOSE = 201;
  MENU_VECTORIZATION = 202;
  MENU_TRANSPOSE_VECTORIZATION = 203;
  MENU_INSERT_SUBMENU = 300;
  MENU_INSERT_ROW = 301;
  MENU_INSERT_COL = 302;
  MENU_DELETE_SUBMENU = 400;
  MENU_DELETE_ROW = 401;
  MENU_DELETE_COL = 402;

constructor TASTableEditor.Create(AOwner: TComponent);
var
  mi, smi: TMenuItem;
begin
  inherited;
  FTable := TASTable.Create;
  FCellWidth := 100;
  FCellHeight := 32;

  Color := clWindow;

  SetLength(FColLefts, 1);
  SetLength(FColWidths, 1);

  FColLefts[0] := 0;
  FColWidths[0] := 100;

  TabStop := true;

  UpdateSPI;

  ControlStyle := ControlStyle + [csPannable];

  FPopupMenu := TPopupMenu.Create(Self);
  FPopupMenu.AutoLineReduction := maAutomatic;

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SEdit;
  mi.Tag := MENU_EDIT;
  mi.Default := true;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SCellStyle;
  mi.Tag := MENU_CELL_STYLE;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  FPopupMenu.Items.NewBottomLine;

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SSetSize;
  mi.Tag := MENU_SET_SIZE;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SDesign;
  mi.Tag := MENU_DESIGN;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SQuickDesign;
  mi.Tag := MENU_QUICK_DESIGN;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  FPopupMenu.Items.NewBottomLine;

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SCut;
  mi.Tag := MENU_CUT;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SCopy;
  mi.Tag := MENU_COPY;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SPaste;
  mi.Tag := MENU_PASTE;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SClear;
  mi.Tag := MENU_CLEAR;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  FPopupMenu.Items.NewBottomLine;

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SSelectAll;
  mi.Tag := MENU_SELECT_ALL;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  FPopupMenu.Items.NewBottomLine;

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SInsert;
  mi.Tag := MENU_INSERT_SUBMENU;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  smi := TMenuItem.Create(mi);
  smi.Caption := SRow;
  smi.Tag := MENU_INSERT_ROW;
  smi.OnClick := MenuItemClicked;
  mi.Add(smi);

  smi := TMenuItem.Create(mi);
  smi.Caption := SColumn;
  smi.Tag := MENU_INSERT_COL;
  smi.OnClick := MenuItemClicked;
  mi.Add(smi);

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SDelete;
  mi.Tag := MENU_DELETE_SUBMENU;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  smi := TMenuItem.Create(mi);
  smi.Caption := SRow;
  smi.Tag := MENU_DELETE_ROW;
  smi.OnClick := MenuItemClicked;
  mi.Add(smi);

  smi := TMenuItem.Create(mi);
  smi.Caption := SColumn;
  smi.Tag := MENU_DELETE_COL;
  smi.OnClick := MenuItemClicked;
  mi.Add(smi);

  FPopupMenu.Items.NewBottomLine;

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := STransform;
  mi.Tag := MENU_TRANSFORM_SUBMENU;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

  smi := TMenuItem.Create(mi);
  smi.Caption := STranspose;
  smi.Tag := MENU_TRANSPOSE;
  smi.OnClick := MenuItemClicked;
  mi.Add(smi);

  smi := TMenuItem.Create(mi);
  smi.Caption := SVectorization;
  smi.Tag := MENU_VECTORIZATION;
  smi.OnClick := MenuItemClicked;
  mi.Add(smi);

  smi := TMenuItem.Create(mi);
  smi.Caption := STransposeVectorization;
  smi.Tag := MENU_TRANSPOSE_VECTORIZATION;
  smi.OnClick := MenuItemClicked;
  mi.Add(smi);

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SSave;
  mi.Tag := MENU_SAVE;
  mi.OnClick := MenuItemClicked;
  FPopupMenu.Items.Add(mi);

end;

procedure TASTableEditor.MenuItemClicked(Sender: TObject);
begin
  if Sender is TMenuItem then
    case TMenuItem(Sender).Tag of
      MENU_EDIT:
        ShowEditor;
      MENU_CLEAR:
        ClearSelection;
      MENU_CUT:
        CutToClipboard;
      MENU_COPY:
        CopyToClipboard;
      MENU_PASTE:
        PasteFromClipboard;
      MENU_SELECT_ALL:
        SelectAll;
      MENU_SAVE:
        SaveTableToFile;
      MENU_SET_SIZE:
        GUISetSize;
      MENU_DESIGN:
        GUISetDesign;
      MENU_QUICK_DESIGN:
        GUIQuickDesign;
      MENU_CELL_STYLE:
        GUICellStyle;
      MENU_TRANSPOSE, MENU_VECTORIZATION, MENU_TRANSPOSE_VECTORIZATION:
        GUITransformResult(TMenuItem(Sender).Tag);
      MENU_INSERT_ROW:
        InsertRow;
      MENU_INSERT_COL:
        InsertCol;
      MENU_DELETE_ROW:
        DeleteRow;
      MENU_DELETE_COL:
        DeleteCol;
    end;
end;

function TryParseSizeStr(const S: string; out num1, num2: integer): boolean;
var
  p: integer;
  s1, s2: string;
begin
  p := Pos('x', S);
  if p = 0 then
    Exit(false);
  s1 := Trim(Copy(S, 1, p - 1));
  s2 := Trim(Copy(S, p + 1));
  result := TryStrToInt(s1, num1) and TryStrToInt(s2, num2) and InRange(num1, 1, 1024*1024) and InRange(num2, 1, 1024*1024);
end;

function ValidSizeInput(const S: string): boolean;
var
  dummy1, dummy2: integer;
begin
  result := TryParseSizeStr(S, dummy1, dummy2);
end;

procedure TASTableEditor.GUISetSize;
resourcestring
  SSetSizeTitle = 'Table size';
  SSetSizeText = 'Please enter the new size of the table:';
var
  S: string;
  w, h: integer;
begin
  if FReadOnly then
    Exit;

  S := Format('%d x %d', [FTable.Width, FTable.Height]);
  if TMultiInputBox.TextInputBoxEx(GetParentForm(Self), SSetSizeTitle, SSetSizeText, S, ecNormal, ValidSizeInput) and TryParseSizeStr(S, w, h) then
  begin
    FTable.SetSize(w, h);
    UpdateAll;
  end;
end;

procedure TASTableEditor.GUISetDesign;
begin
  if FReadOnly then
    Exit;

  with TTableDesignFrm.Create(nil) do
    try
      Table := Self.Table;
      OnApply := TableDesignApply;
      ShowModal;
    finally
      Free;
    end;
end;


procedure TASTableEditor.GUIQuickDesign;
begin
  if FReadOnly then
    Exit;

  with TTableQuickDesignFrm.Create(nil) do
    try
      Table := Self.Table; // by ref here, will be copied by the editor
      if ShowModal = mrOk then
      begin
        Self.Table.Style := PreviewTable.Style;
        Self.Table.FirstRow := PreviewTable.FirstRow;
        Self.Table.FirstCol := PreviewTable.FirstCol;
        Self.Table.LastRow := PreviewTable.LastRow;
        Self.Table.LastCol := PreviewTable.LastCol;
        Self.Table.EvenRows := PreviewTable.EvenRows;
        Self.Table.EvenCols := PreviewTable.EvenCols;
        Self.UpdateMetricsFromStyle;
        Self.Invalidate;
      end;
    finally
      Free;
    end;
end;


procedure TASTableEditor.GUICellStyle;
var
  style: TASAttributes;
  Sel: TRect;
  y, x: integer;
begin
  style := FTable.CellStyles[FActiveCell].Clone;
  if StyleDialog(nil, '', style) then
  begin
    Sel := GetSelection;
    for y := Sel.Top to Sel.Bottom do
      for x := Sel.Left to Sel.Right do
        FTable.CellStyles[Point(x, y)] := style.Clone;
    InvalidateRect(Handle, SelRect[Sel], false);
  end;
end;

procedure TASTableEditor.GUITransformResult(AType: integer);
var
  Frm: TForm;
  Sel: TRect;
  Tbl: TASTable;
  TblEd: TASTableEditor;
  TxtEd: TTextEditor;
  x, y: Integer;
begin
  Frm := TForm.Create(GetParentForm(Self));
  try
    Frm.Caption := STransformResultCaption;
    Frm.Position := poOwnerFormCenter;
    Frm.BorderIcons := [biSystemMenu, biMaximize];
    Sel := Self.GetSelection;
    case AType of
      MENU_TRANSPOSE:
        begin
          TblEd := TASTableEditor.Create(Frm);
          TblEd.Parent := Frm;
          TblEd.Color := clWindow;
          TblEd.Align := alClient;

          Tbl := TASTable.Create;
          try
            Tbl.SetSize(RectHeight(Sel) + 1, RectWidth(Sel) + 1);
            for y := 0 to Tbl.Height - 1 do
              for x := 0 to Tbl.Width - 1 do
                Tbl.Strings[Point(x, y)] := FTable.Strings[Point(Sel.Left + y, Sel.Top + x)];
          except
            Tbl.Free;
            raise;
          end;

          TblEd.Table := Tbl;

          Frm.ClientWidth := Min(TblEd.GetTotalHorizontalExtent, 1024);
          Frm.ClientHeight := Min(TblEd.GetTotalVerticalExtent, 768);
        end;
      MENU_VECTORIZATION:
        begin
          TxtEd := TTextEditor.Create(Frm);
          TxtEd.Parent := Frm;
          TxtEd.Align := alClient;

          TxtEd.TextFile.BeginAddLine;
          for x := Sel.Left to Sel.Right do
            for y := Sel.Top to Sel.Bottom do
              TxtEd.AddLine(FTable.Strings[Point(x, y)]);
          TxtEd.TextFile.EndAddLine;
          TxtEd.ClearUndoHistory;
        end;
      MENU_TRANSPOSE_VECTORIZATION:
        begin
          TxtEd := TTextEditor.Create(Frm);
          TxtEd.Parent := Frm;
          TxtEd.Align := alClient;

          TxtEd.TextFile.BeginAddLine;
          for y := Sel.Top to Sel.Bottom do
            for x := Sel.Left to Sel.Right do
              TxtEd.AddLine(FTable.Strings[Point(x, y)]);
          TxtEd.TextFile.EndAddLine;
          TxtEd.ClearUndoHistory;
        end;
    end;
    Frm.ShowModal;
  finally
    Frm.Free;
  end;
end;

procedure TASTableEditor.TableDesignApply(Sender: TObject);
begin
  UpdateAll;
end;

procedure TASTableEditor.CreateParams(var Params: TCreateParams);
begin
  inherited;
  Params.Style := Params.Style or WS_HSCROLL or WS_VSCROLL;
  Params.WindowClass.style := Params.WindowClass.style and not CS_VREDRAW and not CS_HREDRAW;
end;

function TASTableEditor.CurrentCellText: string;
begin
  result := FTable[FActiveCell];
end;

procedure TASTableEditor.CutCurrentCellToClipboard;
begin
  CopyCurrentCellToClipboard;
  ClearCurrentCell;
end;

procedure TASTableEditor.CutToClipboard;
begin
  CopyToClipboard;
  ClearSelection;
end;

procedure TASTableEditor.DblClick;
begin
  inherited;
  with ScreenToClient(Mouse.CursorPos) do
    if not CellHitTest(X, Y) then
      Exit;

  if (not CanEdit(FActiveCell)) and ((FActiveCell.X = 0) or (FActiveCell.Y = 0)) then
  begin
    if PointsEqual(FActiveCell, Point(0, 0)) then
      SelectAll
    else if FActiveCell.Y = 0 then
      SelectColumn(FActiveCell.X)
    else if FActiveCell.X = 0 then
      SelectRow(FActiveCell.Y)
  end
  else
    ShowEditor;

  FDoubleClicked := true;
end;

procedure TASTableEditor.SelectAll;
begin
  if FSelectedAll then
  begin
    SetActiveCell(FOldSelAnchor, false, false);
    SetActiveCell(FOldActiveCell, true, false);
    Exit;
  end;

  FOldSelAnchor := FSelAnchor;
  FOldActiveCell := FActiveCell;
  SetActiveCell(Point(FTable.Width - 1, FTable.Height - 1), false, false);
  SetActiveCell(Point(0, 0), true, false);
  FSelectedAll := true;
end;

procedure TASTableEditor.SelectColumn(AIndex: integer);
begin
  SetActiveCell(Point(AIndex, FTable.Height - 1), false, false);
  SetActiveCell(Point(AIndex, 0), true, false);
end;

function TASTableEditor.SelectionText: string;
var
  P, Q: TPoint;

  function GetRow(Index: integer): string;
  var
    x: integer;
  begin
    result := FTable[Point(P.X, Index)];
    for x := P.X + 1 to Q.X do
      result := result + #9 + FTable[Point(x, Index)];
  end;

var
  y: integer;

begin
  P := Selection.TopLeft;
  Q := Selection.BottomRight;
  result := GetRow(P.Y);
  for y := P.Y + 1 to Q.Y do
    result := result + sLineBreak + GetRow(y);
end;

procedure TASTableEditor.SelectRow(AIndex: integer);
begin
  SetActiveCell(Point(FTable.Width - 1, AIndex), false, false);
  SetActiveCell(Point(0, AIndex), true, false);
end;

destructor TASTableEditor.Destroy;
begin
  FEditor.Free;
  FTable.Free;
  inherited;
end;

procedure TASTableEditor.DrawCell(const ACell: TPoint);
var
  Style: TCellStyle;
  Active: boolean;
  CellSelected: boolean;
  R: TRect;
  S: string;
  TF: TTextFormat;
begin

  if not FTable.CellExists(ACell) then
    Exit;

  Style := ApplyStyle(FTable.ComputedStyles[ACell]);
  Active := PointsEqual(ACell, FActiveCell) and not FNoSelection;
  CellSelected := Selected[ACell] and not PointsEqual(ACell, FActiveCell) and not FNoSelection;

  R := CellRect[ACell];
  S := FTable.Strings[ACell];

  // Background
  Canvas.Brush.Style := bsSolid;
  Canvas.Brush.Color := IfThen(CellSelected, InvertColor(Style.BackgroundColor), Style.BackgroundColor);
  Canvas.FillRect(R);

  // Border
  if Active then
  begin
    Style.BorderColor := clHighlight;
    Style.BorderWidth := Max(Style.BorderWidth, 5);
    InflateRect(R, (Style.BorderWidth + FVertGridWidth) div 2, (Style.BorderWidth + FHorGridWidth) div 2);
  end;

  if Style.BorderWidth > 0 then
  begin
    Canvas.Brush.Style := bsSolid;
    Canvas.Brush.Color := IfThen(CellSelected, InvertColor(Style.BorderColor), Style.BorderColor);
    Canvas.FillRect(Rect(R.Left, R.Top, R.Right, R.Top + Style.BorderWidth));
    Canvas.FillRect(Rect(R.Right - Style.BorderWidth, R.Top, R.Right, R.Bottom));
    Canvas.FillRect(Rect(R.Left, R.Bottom - Style.BorderWidth, R.Right, R.Bottom));
    Canvas.FillRect(Rect(R.Left, R.Top, R.Left + Style.BorderWidth, R.Bottom));
  end;

  if Active then
    InflateRect(R, -(Style.BorderWidth + FVertGridWidth) div 2, -(Style.BorderWidth + FHorGridWidth) div 2);

  // Text
  Canvas.Font.Name := Style.FontFamily;
  Canvas.Font.Size := Style.FontSize;
  Canvas.Font.Color := IfThen(CellSelected, InvertColor(Style.TextColor), Style.TextColor);
  Canvas.Font.Style := Style.FontStyles;

  TF := [tfNoPrefix, tfSingleLine, tfVerticalCenter, tfEndEllipsis];
  case Style.TextAlign of
    taLeftJustify:
      Include(TF, tfLeft);
    taRightJustify:
      Include(TF, tfRight);
    taCenter:
      Include(TF, tfCenter);
  end;

  if Style.NoEllipsis then
    Exclude(TF, tfEndEllipsis);

  InflateRect(R, -Style.Padding, 0);

  Canvas.Brush.Style := bsClear;
  Canvas.TextRect(R, S, TF);
  Canvas.Brush.Style := bsSolid;

end;

function TASTableEditor.GetCellAt(const X, Y: integer): TPoint;
var
  i: integer;
begin
  with Point(X + FScrollPos.X, Y + FScrollPos.Y) do
  begin
    result.X := 0;
    for i := 0 to FTable.Width - 1 do
      if X > FColLefts[i] then
        result.X := i;
    result.Y := EnsureRange((Y - FBorderWidth) div (FCellHeight + FHorGridWidth),
      0, FTable.Height - 1);
  end;
end;

procedure TASTableEditor.DrawBorder;
var
  vals: string;
  TableBorderColor: TColor;
  R: TRect;
begin
  if not (FTable.Style.TryGetValue('table-border-color', vals) and TryStrToColor(vals, TableBorderColor)) then
    TableBorderColor := Font.Color;
  Canvas.Brush.Color := TableBorderColor;
  Canvas.Brush.Style := bsSolid;
  R := TableRect;
  InflateRect(R, FBorderWidth, FBorderWidth);
  Canvas.FillRect(Rect(R.Left, R.Top, R.Right, R.Top + FBorderWidth));
  Canvas.FillRect(Rect(R.Right - FBorderWidth, R.Top, R.Right, R.Bottom));
  Canvas.FillRect(Rect(R.Left, R.Bottom - FBorderWidth, R.Right, R.Bottom));
  Canvas.FillRect(Rect(R.Left, R.Top, R.Left + FBorderWidth, R.Bottom));
end;

function TASTableEditor.GetCellRect(const ACell: TPoint): TRect;
begin
  result.Left := FColLefts[ACell.X];
  result.Top := FBorderWidth + (FCellHeight + FHorGridWidth) * ACell.y;
  result.Right := result.Left + FColWidths[ACell.X];
  result.Bottom := result.Top + FCellHeight;
  OffsetRect(result, -FScrollPos.X, -FScrollPos.Y);
end;

function TASTableEditor.GetIsSelected(const ACell: TPoint): boolean;
begin
  with Selection, ACell do
    result := InRange(X, Left, Right) and InRange(Y, Top, Bottom);
end;

function TASTableEditor.GetSelection(const APoint1, APoint2: TPoint): TRect;
begin
  result.Left := min(APoint1.X, APoint2.X);
  result.Right := max(APoint1.X, APoint2.X);
  result.Top := min(APoint1.Y, APoint2.Y);
  result.Bottom := max(APoint1.Y, APoint2.Y);
end;

function TASTableEditor.GetSelection: TRect;
begin
  result := GetSelection(FSelAnchor, FActiveCell);
end;

function TASTableEditor.GetSelRect(const ASel: TRect): TRect;
begin
  result.TopLeft := CellRect[ASel.TopLeft].TopLeft;
  result.BottomRight := CellRect[ASel.BottomRight].BottomRight;
end;

procedure TASTableEditor.SaveEdit(FillSel: boolean);
var
  R: TRect;
  y, x: integer;
begin
  if not (Assigned(FEditor) and FEditor.Visible) then
    Exit;

  if HasSelection and FillSel then
  begin
    R := GetSelection;
    for y := R.Top to R.Bottom do
      for x := R.Left to R.Right do
        if FTable.CellExists(Point(X, Y)) and CanEdit(Point(X, Y)) then
          FTable[Point(X, Y)] := FEditor.PlainText;
    InvalidateRect(Handle, SelRect[R], false);
  end
  else
    if CanEdit(FActiveCell) then
    begin
      FTable.Strings[FActiveCell] := FEditor.PlainText;
      DrawCell(FActiveCell);
    end;

end;

procedure TASTableEditor.SaveTableToFile;
begin
  with TSaveDialog.Create(nil) do
    try
      Filter := SSaveDialogFilter;
      DefaultExt := '.txt';
      Options := [ofOverwritePrompt, ofPathMustExist];
      if Execute then
        FTable.SaveTextToFile(FileName);
    finally
      Free;
    end;
end;

procedure TASTableEditor.UpdateAll;
begin
  UpdateMetricsFromStyle;
  FActiveCell := Point(0, 0);
  FSelAnchor := Point(0, 0);
  Invalidate;
end;

function TASTableEditor.HasSelection: boolean;
begin
  result := not PointsEqual(FSelAnchor, FActiveCell);
end;

procedure TASTableEditor.HideEditor;
begin
  if Assigned(FEditor) and FEditor.Visible then
  begin
    FEditor.Hide;
    if Self.CanFocus then Self.SetFocus;
  end;
end;

function TASTableEditor.Go(ADirection: TDirection; AJump: boolean = false): TPoint;
begin
  result := Go(FActiveCell, ADirection, AJump);
end;

function TASTableEditor.Go(const AOrigin: TPoint; ADirection: TDirection;
  AJump: boolean = false): TPoint;
var
  i: integer;
begin
  result := AOrigin;
  case ADirection of
    dirUp:
      if AJump then
        for i := AOrigin.Y - 1 downto 0 do
        begin
          if (FTable[Point(AOrigin.X, i)] <> '') or ((i = 0) and (FTable[Point(AOrigin.X, AOrigin.Y - 1)] = '')) then
            result.Y := i;
          if ((FTable[Point(AOrigin.X, i)] = '') <> (FTable[Point(AOrigin.X, AOrigin.Y - 1)] = '')) or ((FTable[AOrigin] = '') and (FTable[Point(AOrigin.X, i)] <> '')) then
            break;
        end
      else
        result.Y := Max(result.Y - 1, 0);
    dirRight:
      if AJump then
        for i := AOrigin.X + 1 to FTable.Width - 1 do
        begin
          if (FTable[Point(i, AOrigin.Y)] <> '') or ((i = FTable.Width - 1) and (FTable[Point(AOrigin.X + 1, AOrigin.Y)] = '')) then
            result.X := i;
          if ((FTable[Point(i, AOrigin.Y)] = '') <> (FTable[Point(AOrigin.X + 1, AOrigin.Y)] = '')) or ((FTable[AOrigin] = '') and (FTable[Point(i, AOrigin.Y)] <> '')) then
            break;
        end
      else
        result.X := Min(result.X + 1, FTable.Width - 1);
    dirDown:
      if AJump then
        for i := AOrigin.Y + 1 to FTable.Height - 1 do
        begin
          if (FTable[Point(AOrigin.X, i)] <> '') or ((i = FTable.Height - 1) and (FTable[Point(AOrigin.X, AOrigin.Y + 1)] = '')) then
            result.Y := i;
          if ((FTable[Point(AOrigin.X, i)] = '') <> (FTable[Point(AOrigin.X, AOrigin.Y + 1)] = '')) or ((FTable[AOrigin] = '') and (FTable[Point(AOrigin.X, i)] <> '')) then
            break;
        end
      else
        result.Y := Min(result.Y + 1, FTable.Height - 1);
    dirLeft:
      if AJump then
        for i := AOrigin.X - 1 downto 0 do
        begin
          if (FTable[Point(i, AOrigin.Y)] <> '') or ((i = 0) and (FTable[Point(AOrigin.X - 1, AOrigin.Y)] = '')) then
            result.X := i;
          if ((FTable[Point(i, AOrigin.Y)] = '') <> (FTable[Point(AOrigin.X - 1, AOrigin.Y)] = '')) or ((FTable[AOrigin] = '') and (FTable[Point(i, AOrigin.Y)] <> '')) then
            break;
        end
      else
        result.X := Max(result.X - 1, 0);
  end;
end;

procedure TASTableEditor.KeyDown(var Key: Word; Shift: TShiftState);
begin
  inherited;
  case Key of
    VK_UP:
      if ScrollMode then
        SetScrollPosY(FScrollPos.Y - IfThen(ssCtrl in Shift, 1, FCellHeight - FHorGridWidth))
      else
        SetActiveCell(Go(dirUp, ssCtrl in Shift), ssShift in Shift);
    VK_DOWN:
      if ScrollMode then
        SetScrollPosY(FScrollPos.Y + IfThen(ssCtrl in Shift, 1, FCellHeight + FHorGridWidth))
      else
        SetActiveCell(Go(dirDown, ssCtrl in Shift), ssShift in Shift);
    VK_LEFT:
      if ScrollMode then
        SetScrollPosX(FScrollPos.X - IfThen(ssCtrl in Shift, 1, FCellWidth - FVertGridWidth))
      else
        SetActiveCell(Go(dirLeft, ssCtrl in Shift), ssShift in Shift);
    VK_RIGHT:
      if ScrollMode then
        SetScrollPosX(FScrollPos.X + IfThen(ssCtrl in Shift, 1, FCellWidth + FVertGridWidth))
      else
        SetActiveCell(Go(dirRight, ssCtrl in Shift), ssShift in Shift);
    VK_TAB:
      if ssShift in Shift then
        ActiveCell := PrevCell(ActiveCell)
      else
        ActiveCell := NextCell(ActiveCell);
    VK_HOME:
      if ScrollMode then
        SetScrollPosY(0)
      else
        if ssCtrl in Shift then
          SetActiveCell(Point(0, 0), ssShift in Shift)
        else
          SetActiveCell(Point(0, FActiveCell.Y), ssShift in Shift);
    VK_END:
      if ScrollMode then
        SetScrollPosY(GetTotalVerticalExtent - ClientHeight)
      else
        if ssCtrl in Shift then
          SetActiveCell(Point(FTable.Width - 1, FTable.Height - 1), ssShift in Shift)
        else
          SetActiveCell(Point(FTable.Width - 1, FActiveCell.Y), ssShift in Shift);
    VK_PRIOR:
      if ScrollMode then
        SetScrollPosY(FScrollPos.Y - ClientHeight)
      else
        SetActiveCell(Point(FActiveCell.X, Max(0, FActiveCell.Y - ClientHeight div (FCellHeight + FHorGridWidth))), ssShift in Shift);
    VK_NEXT:
      if ScrollMode then
        SetScrollPosY(FScrollPos.Y + ClientHeight)
      else
        SetActiveCell(Point(FActiveCell.X, Min(FTable.Height - 1, FActiveCell.Y + ClientHeight div (FCellHeight + FHorGridWidth))), ssShift in Shift);
    VK_RETURN, VK_F2:
      ShowEditor;
    VK_DELETE:
      ClearSelection;
    VK_MENU, VK_SCROLL:
      ChangeCursor;
  end;
end;

procedure TASTableEditor.KeyPress(var Key: Char);
begin
  inherited;
  if Ord(Key) >= 32 then
  begin
    ShowEditor(Key);
    if Assigned(FEditor) then
      FEditor.EditorCommand(EDITOR_COMMAND_END);
  end
  else
    case Key of
      ^A:
        SelectAll;
      ^X:
        CutToClipboard;
      ^C:
        CopyToClipboard;
      ^V:
        PasteFromClipboard;
    end;
end;

procedure TASTableEditor.KeyUp(var Key: Word; Shift: TShiftState);
begin
  inherited;
  case Key of
    VK_MENU, VK_SCROLL:
      ChangeCursor;
  end;
end;

procedure TASTableEditor.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
var
  CellAt: TPoint;
begin
  inherited;
  if FDoubleClicked then
    Exit;
  FScrollAnchor := Point(X, Y);
  ChangeCursor;
  if ScrollMode(ssAlt in Shift) then
    Exit;
  CellAt := GetCellAt(X, Y);
  if (Button = mbLeft) or ((Button = mbRight) and not Selected[CellAt]) then
    if ssShift in Shift then
      SetActiveCell(CellAt, true)
    else
      ActiveCell := CellAt;
  if CanFocus then SetFocus;
end;

procedure TASTableEditor.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
  inherited;
  if FDoubleClicked then
    Exit;

  ChangeCursor;

  if csLButtonDown in ControlState then
    if ScrollMode(ssAlt in Shift) then
    begin
      SetScrollPosXY(FScrollPos.X - X + FScrollAnchor.X, FScrollPos.Y - Y + FScrollAnchor.Y);
      FScrollAnchor := Point(X, Y);
    end
    else
      SetActiveCell(GetCellAt(X, Y), true);
end;

procedure TASTableEditor.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
begin
  inherited;
  if FDoubleClicked then
  begin
    FDoubleClicked := false;
    Exit;
  end;
  ChangeCursor;
end;

function IsKeyDown(const AKey: integer): boolean;
begin
  result := GetKeyState(AKey) and $8000 <> 0;
end;

function IsKeyOn(const AKey: integer): boolean;
begin
  result := GetKeyState(AKey) and 1 = 1;
end;

function TASTableEditor.ScrollMode: boolean;
var
  AltDown, ScrlLockOn: boolean;
begin
  AltDown := IsKeyDown(VK_LMENU);
  ScrlLockOn := IsKeyOn(VK_SCROLL);
  result := (AltDown xor ScrlLockOn) and not IsKeyDown(VK_SHIFT);
end;

function TASTableEditor.ScrollMode(AAlt: boolean): boolean;
var
  AltDown, ScrlLockOn: boolean;
begin
  AltDown := AAlt;
  ScrlLockOn := IsKeyOn(VK_SCROLL);
  result := (AltDown xor ScrlLockOn) and not IsKeyDown(VK_SHIFT);
end;

function TASTableEditor.NextCell(const AFrom: TPoint): TPoint;
begin
  if AFrom.X < FTable.Width - 1 then
    result := Point(AFrom.X + 1, AFrom.Y)
  else if AFrom.Y < FTable.Height - 1 then
    result := Point(0, AFrom.Y + 1)
  else
    result := AFrom;
end;

procedure TASTableEditor.Paint;
var
  y: Integer;
  x: Integer;
  FVC, LVC: integer;
begin
  inherited;
  ClearClientArea;
  DrawGridLines;
  DrawBorder;

  FVC := GetFirstVisibleCol;
  LVC := GetLastVisibleCol;

  for y := GetFirstVisibleRow to GetLastVisibleRow do
    for x := FVC to LVC do
      DrawCell(Point(x, y));

  DrawCell(FActiveCell);
end;

function TASTableEditor.GetFirstVisibleRow: integer;
begin
  result := Max(0, FScrollPos.Y div (FCellHeight + FHorGridWidth) - 1);
end;

function TASTableEditor.GetLastVisibleRow: integer;
begin
  result := Min(GetFirstVisibleRow + Ceil(ClientHeight / (FCellHeight + FHorGridWidth)) + 2,
    FTable.Height - 1);
end;

function TASTableEditor.GetFirstVisibleCol: integer;
var
  i: Integer;
begin
  for i := 0 to high(FColLefts) do
    if FColLefts[i] + FColWidths[i] > FScrollPos.X then
      Exit(i);
  result := FTable.Width;
end;

function TASTableEditor.GetLastVisibleCol: integer;
var
  i: Integer;
begin
  for i := high(FColLefts) downto 0 do
    if FColLefts[i] < FScrollPos.X + ClientWidth then
      Exit(i);
  result := 0;
end;

procedure TASTableEditor.PasteFromClipboard;
var
  S: string;
  rows: DynStringArray;
  cells: DynStringArray;
  y, x: integer;
  maxwidth: integer;
begin
  if Clipboard.HasFormat(CF_TEXT) then
  begin
    S := Clipboard.AsText;
    if EndsStr(sLineBreak, S) then
      SetLength(S, Length(S) - Length(sLineBreak));
    rows := Split(S, sLineBreak);
    maxwidth := 1;
    for y := FActiveCell.Y to min(FActiveCell.Y + Length(rows), FTable.Height) - 1 do
    begin
      cells := Split(rows[y - FActiveCell.Y], #9);
      for x := FActiveCell.X to min(FActiveCell.X + Length(cells), FTable.Width) - 1 do
        if CanEdit(Point(x, y)) then
          FTable[Point(x, y)] := cells[x - FActiveCell.X];
      if Length(cells) > maxwidth then
        maxwidth := Length(cells);
    end;
    InvalidateRect(Handle,
      SelRect[Rect(FActiveCell.X,
        FActiveCell.Y,
        min(FActiveCell.X + maxwidth, FTable.Width) - 1,
        min(FActiveCell.Y + Length(rows), FTable.Height) - 1)],
      false);
  end;
end;

procedure TASTableEditor.PasteFromClipboardToCurrentCell;
begin
  if CanEdit(ActiveCell) then
  begin
    FTable[FActiveCell] := StringReplace(Clipboard.AsText, sLineBreak, #$21B2, [rfReplaceAll]);
    DrawCell(FActiveCell);
  end
  else
    Beep;
end;

function TASTableEditor.CellHitTest(const X, Y: integer): boolean;
begin
  with Point(X + FScrollPos.X, Y + FScrollPos.Y) do
    result :=
      InRange(X, FBorderWidth, FColLefts[high(FColLefts)] + FColWidths[high(FColWidths)])
      and
      InRange(Y, FBorderWidth, GetTotalVerticalExtent - FBorderWidth);
end;

procedure TASTableEditor.ClearClientArea;
begin
  Canvas.Brush.Color := Color;
  Canvas.Brush.Style := bsSolid;
  Canvas.FillRect(ClientRect);
end;

procedure TASTableEditor.UpdateMetricsFromStyle;
var
  vals: string;
  i: Integer;
begin
  if not (FTable.Style.TryGetValue('table-border-width', vals) and TryStrToInt(vals, FBorderWidth) and (FBorderWidth >= 0)) then
    FBorderWidth := 3;

  if not (FTable.Style.TryGetValue('horizontal-grid-width', vals) and TryStrToInt(vals, FHorGridWidth) and (FHorGridWidth >= 0)) then
    FHorGridWidth := 1;

  if not (FTable.Style.TryGetValue('vertical-grid-width', vals) and TryStrToInt(vals, FVertGridWidth) and (FVertGridWidth >= 0)) then
    FVertGridWidth := 1;

  if not (FTable.Style.TryGetValue('cell-width', vals) and TryStrToInt(vals, FCellWidth) and (FCellWidth >= 2)) then
    FCellWidth := 100;

  if not (FTable.Style.TryGetValue('cell-height', vals) and TryStrToInt(vals, FCellHeight) and (FCellHeight >= 2)) then
    FCellHeight := 32;

  SetLength(FColLefts, FTable.Width);
  SetLength(FColWidths, FTable.Width);

  if (FTable.Width > 0) and (FTable.Height > 0) then
  begin
    for i := 0 to FTable.Width - 1 do
      if not (FTable.ComputedStyles[Point(i, 0)].TryGetValue('column-width', vals) and TryStrToInt(vals, FColWidths[i]) and (FColWidths[i] >= 2)) then
        FColWidths[i] := FCellWidth;
    FColLefts[0] := FBorderWidth;
    for i := 1 to FTable.Width - 1 do
      FColLefts[i] := FColLefts[i - 1] + FColWidths[i - 1] + FVertGridWidth;
  end;

  UpdateScrollBars;
end;

function TASTableEditor.PrevCell(const AFrom: TPoint): TPoint;
begin
  if AFrom.X > 0 then
    result := Point(AFrom.X - 1, AFrom.Y)
  else if AFrom.Y > 0 then
    result := Point(FTable.Width - 1, AFrom.Y - 1)
  else
    result := AFrom;
end;

procedure TASTableEditor.Resize;
begin
  inherited;
  UpdateScrollBars;
end;

procedure TASTableEditor.DrawCellNbh(const ACell: TPoint);
var
  i, j: integer;
begin
  for i := ACell.X - 1 to ACell.X + 1 do
    for j := ACell.Y - 1 to ACell.Y + 1 do
      DrawCell(Point(i, j));
end;

procedure TASTableEditor.DrawGridLines;
var
  vals: string;
  TableGridColor: TColor;
  R: TRect;
  y: integer;
  x: integer;
begin
  if not (FTable.Style.TryGetValue('horizontal-grid-color', vals) and TryStrToColor(vals, TableGridColor)) then
    TableGridColor := clGray;
  Canvas.Brush.Color := TableGridColor;
  Canvas.Brush.Style := bsSolid;
  for y := Max(GetFirstVisibleRow - 1, 1) to Min(GetLastVisibleRow + 1, FTable.Height - 1) do
  begin
    R.Left := FBorderWidth;
    R.Right := FColLefts[high(FColLefts)] + FColWidths[high(FColWidths)];
    R.Top := FBorderWidth + y * FCellHeight + (y - 1) * FHorGridWidth;
    R.Bottom := R.Top + FHorGridWidth;
    OffsetRect(R, -FScrollPos.X, -FScrollPos.Y);
    Canvas.FillRect(R);
  end;
  if not (FTable.Style.TryGetValue('vertical-grid-color', vals) and TryStrToColor(vals, TableGridColor)) then
    TableGridColor := clGray;
  Canvas.Brush.Color := TableGridColor;
  for x := Max(GetFirstVisibleCol - 1, 1) to Min(GetLastVisibleCol + 1, FTable.Width - 1) do
  begin
    R.Top := FBorderWidth;
    R.Bottom := R.Top + FTable.Height * FCellHeight + (FTable.Height - 1) * FHorGridWidth;
    R.Left := FColLefts[x - 1] + FColWidths[x - 1];
    R.Right := R.Left + FVertGridWidth;
    OffsetRect(R, -FScrollPos.X, -FScrollPos.Y);
    Canvas.FillRect(R);
  end;
end;

procedure TASTableEditor.SetActiveCell(const Value: TPoint);
begin
  SetActiveCell(Value, false);
end;

procedure TASTableEditor.WMVScroll(var Message: TWMVScroll);
var
  ScrollInfo: TScrollInfo;
begin
  inherited;
  case Message.ScrollCode of
    SB_TOP:
      SetScrollPosY(0);
    SB_BOTTOM:
      SetScrollPosY(GetTotalVerticalExtent - ClientHeight);
    SB_LINEUP:
      SetScrollPosY(FScrollPos.Y - FCellHeight - FVertGridWidth);
    SB_LINEDOWN:
      SetScrollPosY(FScrollPos.Y + FCellHeight + FVertGridWidth);
    SB_PAGEUP:
      SetScrollPosY(FScrollPos.Y - ClientHeight);
    SB_PAGEDOWN:
      SetScrollPosY(FScrollPos.Y + ClientHeight);
    SB_THUMBTRACK:
      begin
        ScrollInfo.cbSize := sizeof(TScrollInfo);
        ScrollInfo.fMask := SIF_TRACKPOS;
        GetScrollInfo(Handle, SB_VERT, ScrollInfo);
        SetScrollPosY(ScrollInfo.nTrackPos);
      end;
  end;
end;

procedure TASTableEditor.SetScrollPosY(const Y: integer);
var
  OldScrollPos: integer;
begin
  OldScrollPos := FScrollPos.Y;
  FScrollPos.Y := EnsureRange(Y, 0, Max(0, GetTotalVerticalExtent - ClientHeight));
  ScrollWindowEx(Handle, 0, OldScrollPos - FScrollPos.Y, nil, nil, 0, nil, SW_INVALIDATE or SW_SCROLLCHILDREN {or SW_SMOOTHSCROLL or (200 shl 8)});
  UpdateScrollBars;
end;

procedure TASTableEditor.SetSelection(const Value: TRect);
begin
  FSelAnchor := Value.TopLeft;
  FActiveCell := Value.BottomRight;
  Invalidate;
end;

procedure TASTableEditor.WMHScroll(var Message: TWMHScroll);
var
  ScrollInfo: TScrollInfo;
begin
  inherited;
  case Message.ScrollCode of
    SB_LEFT:
      SetScrollPosX(0);
    SB_RIGHT:
      SetScrollPosX(GetTotalHorizontalExtent - ClientWidth);
    SB_LINELEFT:
      SetScrollPosX(FScrollPos.X - FCellWidth - FVertGridWidth);
    SB_LINERIGHT:
      SetScrollPosX(FScrollPos.X + FCellWidth + FVertGridWidth);
    SB_PAGELEFT:
      SetScrollPosX(FScrollPos.X - ClientWidth);
    SB_PAGERIGHT:
      SetScrollPosX(FScrollPos.X + ClientWidth);
    SB_THUMBTRACK:
      begin
        ScrollInfo.cbSize := sizeof(TScrollInfo);
        ScrollInfo.fMask := SIF_TRACKPOS;
        GetScrollInfo(Handle, SB_HORZ, ScrollInfo);
        SetScrollPosX(ScrollInfo.nTrackPos);
      end;
  end;
end;

procedure TASTableEditor.WMMouseHWheel(var Message: TWMMouseWheel);
begin
  if (Message.Keys and MK_SHIFT) <> 0 then
    SetScrollPosX(FScrollPos.X + Sign(Message.WheelDelta))
  else
    SetScrollPosX(FScrollPos.X + round((FCellWidth div 5) * Message.WheelDelta / WHEEL_DELTA));
end;

procedure TASTableEditor.WMMouseWheel(var Message: TWMMouseWheel);
begin
  if (Message.Keys and MK_SHIFT) <> 0 then
    SetScrollPosY(FScrollPos.Y - Sign(Message.WheelDelta))
  else
    if FSPIScrollLines >= 0 then
      SetScrollPosY(FScrollPos.Y - round((FCellHeight div 2) * FSPIScrollLines * Message.WheelDelta / WHEEL_DELTA))
    else if FSPIScrollLines = -1 then
      SetScrollPosY(FScrollPos.Y - round(ClientHeight * Message.WheelDelta / WHEEL_DELTA));

  Message.Result := 0;
end;

procedure TASTableEditor.SetScrollPosX(const X: integer);
var
  OldScrollPos: integer;
begin
  OldScrollPos := FScrollPos.X;
  FScrollPos.X := EnsureRange(X, 0, Max(0, GetTotalHorizontalExtent - ClientWidth));
  ScrollWindowEx(Handle, OldScrollPos - FScrollPos.X, 0, nil, nil, 0, nil, SW_INVALIDATE or SW_SCROLLCHILDREN{or SW_SMOOTHSCROLL or (200 shl 8)});
  UpdateScrollBars;
end;

procedure TASTableEditor.SetScrollPosXY(const X, Y: integer);
var
  OldScrollPosX, OldScrollPosY: integer;
begin
  OldScrollPosX := FScrollPos.X;
  OldScrollPosY := FScrollPos.Y;
  FScrollPos.X := EnsureRange(X, 0, Max(GetTotalHorizontalExtent - ClientWidth, 0));
  FScrollPos.Y := EnsureRange(Y, 0, Max(GetTotalVerticalExtent - ClientHeight, 0));
  ScrollWindowEx(Handle, OldScrollPosX - FScrollPos.X, OldScrollPosY - FScrollPos.Y, nil, nil, 0, nil, SW_INVALIDATE or SW_SCROLLCHILDREN{or SW_SMOOTHSCROLL or (200 shl 8)});
  UpdateScrollBars;
end;

procedure TASTableEditor.UpdateScrollBars;
var
  ScrollInfo: TScrollInfo;
begin

  ScrollInfo.cbSize := sizeof(TScrollInfo);
  ScrollInfo.fMask := SIF_ALL;
  ScrollInfo.nMin := 0;
  ScrollInfo.nMax := max(GetTotalVerticalExtent, FScrollPos.Y + ClientHeight) - 1;
  ScrollInfo.nPage := ClientHeight;
  ScrollInfo.nPos := FScrollPos.Y;
  SetScrollInfo(Handle, SB_VERT, ScrollInfo, true);

  ScrollInfo.cbSize := sizeof(TScrollInfo);
  ScrollInfo.fMask := SIF_ALL;
  ScrollInfo.nMin := 0;
  ScrollInfo.nMax := max(GetTotalHorizontalExtent, FScrollPos.X + ClientWidth) - 1;
  ScrollInfo.nPage := ClientWidth;
  ScrollInfo.nPos := FScrollPos.X;
  SetScrollInfo(Handle, SB_HORZ, ScrollInfo, true);

end;

procedure TASTableEditor.UpdateSPI;
begin
  SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, FSPIScrollLines, 0);
end;

function TASTableEditor.GetTotalVerticalExtent: integer;
begin
  result := FTable.Height * FCellHeight + (FTable.Height - 1) * FHorGridWidth + 2*FBorderWidth;
end;

function TASTableEditor.GetTotalHorizontalExtent: integer;
begin
  result := FColLefts[high(FColLefts)] + FColWidths[high(FColWidths)] + FBorderWidth;
end;

procedure TASTableEditor.SetActiveCell(const Value: TPoint; AExpandSel: boolean;
  AScrollToActive: boolean = true);
var
  OldActiveCell: TPoint;
  OldSelAnchor: TPoint;
  R: TRect;
  SelRepainted: boolean;
  x1, x2: integer;
  y1, y2: integer;
begin
  FSelectedAll := false;

  SaveEdit;
  HideEditor;

  if not FTable.CellExists(Value) then
    Exit;

  if PointsEqual(Value, FActiveCell) and not ((not PointsEqual(FSelAnchor, FActiveCell)) and (not AExpandSel)) then
    Exit;

  OldActiveCell := FActiveCell;
  OldSelAnchor := FSelAnchor;

  FActiveCell := Value;

  if not AExpandSel then
    FSelAnchor := Value;

  DrawBorder;
  DrawGridLines;

  // Selections
  if not (PointsEqual(OldActiveCell, OldSelAnchor) and PointsEqual(FActiveCell, FSelAnchor)) then
  begin

    SelRepainted := false;

    if PointsEqual(OldSelAnchor, FSelAnchor) then
    begin

      if OldActiveCell.Y = FActiveCell.Y then
      begin

        y1 := min(FSelAnchor.Y, FActiveCell.Y);
        y2 := max(FSelAnchor.Y, FActiveCell.Y);
        x1 := min(OldActiveCell.X, FActiveCell.X);
        x2 := max(OldActiveCell.X, FActiveCell.X);
        if x1 >= FSelAnchor.X then inc(x1);
        if x2 <= FSelAnchor.X then dec(x2);
        if x2 >= x1 then
          InvalidateRect(Handle, SelRect[Rect(x1, y1, x2, y2)], false);
        SelRepainted := true;

      end

      else if OldActiveCell.X = FActiveCell.X then
      begin

        x1 := min(FSelAnchor.X, FActiveCell.X);
        x2 := max(FSelAnchor.X, FActiveCell.X);
        y1 := min(OldActiveCell.Y, FActiveCell.Y);
        y2 := max(OldActiveCell.Y, FActiveCell.Y);
        if y1 >= FSelAnchor.Y then inc(y1);
        if y2 <= FSelAnchor.Y then dec(y2);
        if y2 >= y1 then
          InvalidateRect(Handle, SelRect[Rect(x1, y1, x2, y2)], false);
        SelRepainted := true;

      end

      else
      begin

        y1 := MinIntValue([FSelAnchor.Y, OldActiveCell.Y, FActiveCell.Y]);
        y2 := MaxIntValue([FSelAnchor.Y, OldActiveCell.Y, FActiveCell.Y]);
        x1 := min(OldActiveCell.X, FActiveCell.X);
        x2 := max(OldActiveCell.X, FActiveCell.X);
        if x1 >= FSelAnchor.X then inc(x1);
        if x2 <= FSelAnchor.X then dec(x2);
        if x2 >= x1 then
          InvalidateRect(Handle, SelRect[Rect(x1, y1, x2, y2)], false);

        x1 := MinIntValue([FSelAnchor.X, OldActiveCell.X, FActiveCell.X]);
        x2 := MaxIntValue([FSelAnchor.X, OldActiveCell.X, FActiveCell.X]);
        y1 := min(OldActiveCell.Y, FActiveCell.Y);
        y2 := max(OldActiveCell.Y, FActiveCell.Y);
        if y1 >= FSelAnchor.Y then inc(y1);
        if y2 <= FSelAnchor.Y then dec(y2);
        if y2 >= y1 then
          InvalidateRect(Handle, SelRect[Rect(x1, y1, x2, y2)], false);

        SelRepainted := true;

      end;

    end;

    if not SelRepainted then
    begin
      // Remove old selection
      if not PointsEqual(OldActiveCell, OldSelAnchor) then
        InvalidateRect(Handle, SelRect[GetSelection(OldActiveCell, OldSelAnchor)], false);

      // Draw new selection
      if not PointsEqual(FActiveCell, FSelAnchor) then
        InvalidateRect(Handle, SelRect[GetSelection(FActiveCell, FSelAnchor)], false);
    end;

  end;

  // Draw new active cell
  DrawCellNbh(OldActiveCell);
  DrawCell(FActiveCell);

  // Scroll to active cell
  if AScrollToActive then
  begin
    R := GetCellRect(Value);
    if R.Right > ClientWidth then
      SetScrollPosX(FScrollPos.X + R.Right - ClientWidth);
    if R.Left < 0 then
      SetScrollPosX(FScrollPos.X + R.Left);
    if R.Bottom > ClientHeight then
      SetScrollPosY(FScrollPos.Y + R.Bottom - ClientHeight);
    if R.Top < 0 then
      SetScrollPosY(FScrollPos.Y + R.Top);
  end;

end;

procedure TASTableEditor.SetCellHeight(const Value: integer);
begin
  if FCellHeight <> Value then
  begin
    FCellHeight := Value;
    Invalidate;
  end;
end;

procedure TASTableEditor.SetCellWidth(const Value: integer);
begin
  if FCellWidth <> Value then
  begin
    FCellWidth := Value;
    Invalidate;
  end;
end;

procedure TASTableEditor.SetNoSelection(const Value: boolean);
begin
  if FNoSelection <> Value then
  begin
    FNoSelection := Value;
    Invalidate;
  end;
end;

procedure TASTableEditor.SetTable(const Value: TASTable);
begin
  if Value <> FTable then
  begin
    FreeAndNil(FTable);
    FTable := TASTable.Create(Value);
    UpdateAll;
  end;
end;

procedure TASTableEditor.EditorExit(Sender: TObject);
begin
  SaveEdit;
  FEditor.Hide;
end;

procedure TASTableEditor.EditorKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  case Key of
    VK_RETURN:
      begin
        SaveEdit(ssCtrl in Shift);
        HideEditor;
        if not (ssCtrl in Shift) then
          if ssShift in Shift then
            SetActiveCell(Go(dirUp), false)
          else
            SetActiveCell(Go(dirDown), false);
        Key := 0;
      end;
    VK_TAB:
      begin
        SaveEdit(ssCtrl in Shift);
        HideEditor;
        if not (ssCtrl in Shift) then
          if ssShift in Shift then
            SetActiveCell(PrevCell(FActiveCell), false)
          else
            SetActiveCell(NextCell(FActiveCell), false);
        Key := 0;
      end;
    VK_ESCAPE:
      HideEditor;
  end;
end;

procedure TASTableEditor.ShowEditor(const AInitialText: string);
begin
  if ScrollMode or not CanEdit(FActiveCell) then
  begin
    Beep;
    Exit;
  end;

  if not Assigned(FEditor) then
    FEditor := TTextEditor.Create(Self);
  FEditor.Parent := Self;
  FEditor.OnKeyDown := EditorKeyDown;
  FEditor.OnExit := EditorExit;
  FEditor.AutoHeight := false;
  FEditor.SingleLine := true;
  FEditor.WantTab := true;
  FEditor.WantReturn := true;
  FEditor.BoundsRect := CellRect[FActiveCell];
  FEditor.PlainText := FTable.Strings[FActiveCell];
  FEditor.ClearUndoHistory;
  if AInitialText <> '' then
    FEditor.PlainText := AInitialText;
  FEditor.BorderType := btSimpleColor;
  FEditor.BorderColor := clHighlight;
  FEditor.BorderWidth := 3;
  FEditor.SelectAll;
  FEditor.EditorCommand(EDITOR_COMMAND_SET_SCROLL_POS);
  FEditor.Show;
  if FEditor.CanFocus then FEditor.SetFocus;
end;

function TASTableEditor.TableRect: TRect;
begin
  result.Left := FBorderWidth;
  result.Top := FBorderWidth;
  result.Right := FColLefts[high(FColLefts)] + FColWidths[high(FColWidths)];
  result.Bottom := FBorderWidth + FTable.Height * FCellHeight +
    Max(0, (FTable.Height - 1)) * FHorGridWidth;
  OffsetRect(result, -FScrollPos.X, -FScrollPos.Y);
end;

procedure TASTableEditor.WMContextMenu(var Message: TWMContextMenu);
var
  pm: TPopupMenu;
  p: TPoint;
  OnCell: boolean;
  i: Integer;
begin
  if Assigned(PopupMenu) then
    pm := PopupMenu
  else
    pm := FPopupMenu;

  if (Message.XPos = -1) and (Message.YPos = -1) then // menu key or Shift+F10
  begin
    p := GetCellRect(ActiveCell).TopLeft;
    inc(p.X, 2 * FCellWidth div 3);
    inc(p.Y, 2 * FCellHeight div 3);

    for i := 0 to FPopupMenu.Items.Count - 1 do
      case FPopupMenu.Items[i].Tag of
        MENU_EDIT:
          FPopupMenu.Items[i].Enabled := (HasSelection and not FReadOnly) or CanEdit(FActiveCell);
        MENU_CLEAR:
          FPopupMenu.Items[i].Enabled := (HasSelection and not FReadOnly) or CanEdit(FActiveCell);
        MENU_CUT:
          FPopupMenu.Items[i].Enabled := (HasSelection and not FReadOnly) or CanEdit(FActiveCell);
        MENU_COPY:
          FPopupMenu.Items[i].Enabled := true;
        MENU_PASTE:
          FPopupMenu.Items[i].Enabled := not FReadOnly;
        MENU_SELECT_ALL:
          FPopupMenu.Items[i].Enabled := true;
        MENU_SAVE:
          FPopupMenu.Items[i].Enabled := true;
        MENU_SET_SIZE:
          FPopupMenu.Items[i].Enabled := not FReadOnly;
        MENU_DESIGN:
          FPopupMenu.Items[i].Enabled := not FReadOnly;
        MENU_QUICK_DESIGN:
          FPopupMenu.Items[i].Enabled := not FReadOnly;
        MENU_CELL_STYLE:
          FPopupMenu.Items[i].Enabled := HasSelection or CanEdit(FActiveCell);
        MENU_TRANSFORM_SUBMENU:
          FPopupMenu.Items[i].Enabled := true;
        MENU_INSERT_SUBMENU, MENU_DELETE_SUBMENU:
          FPopupMenu.Items[i].Enabled := not FReadOnly;
      else
        FPopupMenu.Items[i].Enabled := false;
      end;

    with ClientToScreen(p) do
      pm.Popup(X, Y);
  end
  else
  begin
    p := ScreenToClient(SmallPointToPoint(Message.Pos));
    if (p.X >= ClientWidth) or (p.Y >= ClientHeight) then // scrollbar
    begin
      inherited;
      Exit;
    end;

    OnCell := CellHitTest(p.X, p.Y);
    for i := 0 to FPopupMenu.Items.Count - 1 do
      case FPopupMenu.Items[i].Tag of
        MENU_EDIT:
          FPopupMenu.Items[i].Enabled := (HasSelection and not FReadOnly) or (OnCell and CanEdit(FActiveCell));
        MENU_CLEAR:
          FPopupMenu.Items[i].Enabled := (HasSelection and not FReadOnly) or (OnCell and CanEdit(FActiveCell));
        MENU_CUT:
          FPopupMenu.Items[i].Enabled := (HasSelection and not FReadOnly) or (OnCell and CanEdit(FActiveCell));
        MENU_COPY:
          FPopupMenu.Items[i].Enabled := HasSelection or OnCell;
        MENU_PASTE:
          FPopupMenu.Items[i].Enabled := OnCell and not FReadOnly;
        MENU_SELECT_ALL:
          FPopupMenu.Items[i].Enabled := true;
        MENU_SAVE:
          FPopupMenu.Items[i].Enabled := true;
        MENU_SET_SIZE:
          FPopupMenu.Items[i].Enabled := not FReadOnly;
        MENU_DESIGN:
          FPopupMenu.Items[i].Enabled := not FReadOnly;
        MENU_QUICK_DESIGN:
          FPopupMenu.Items[i].Enabled := not FReadOnly;
        MENU_CELL_STYLE:
          FPopupMenu.Items[i].Enabled := (HasSelection and not FReadOnly) or (OnCell and CanEdit(FActiveCell));
        MENU_TRANSFORM_SUBMENU:
          FPopupMenu.Items[i].Enabled := true;
        MENU_INSERT_SUBMENU, MENU_DELETE_SUBMENU:
          FPopupMenu.Items[i].Enabled := not FReadOnly;
      else
        FPopupMenu.Items[i].Enabled := false;
      end;

    pm.Popup(Message.XPos, Message.YPos);
  end;

end;

function TASTableEditor.CanEdit(const ACell: TPoint): boolean;
var
  CellRes: boolean;
begin
  CellRes := true;
  if Assigned(FOnCanEdit) then
    FOnCanEdit(ACell.X, ACell.Y, CellRes);

  result := (not FReadOnly) and CellRes;
end;

procedure TASTableEditor.WndProc(var Message: TMessage);
begin
  inherited;
  case Message.Msg of
    WM_GETDLGCODE:
      Message.Result := Message.Result or DLGC_WANTCHARS or DLGC_WANTARROWS
        or DLGC_WANTTAB or DLGC_WANTALLKEYS;
    WM_CLEAR:
      ClearSelection;
    WM_CUT:
      CutToClipboard;
    WM_COPY:
      CopyToClipboard;
    WM_PASTE:
      PasteFromClipboard;
    WM_ERASEBKGND:
      Message.Result := 1;
  end;
end;

procedure TASTableEditor.ChangeCursor;
begin
  if ScrollMode then
    if csLButtonDown in ControlState then
      Cursor := crHandHold
    else
      Cursor := crHand
  else
    Cursor := crDefault;

  if csLButtonDown in ControlState then
    Screen.Cursor := Cursor
  else
    Screen.Cursor := crDefault;
end;

procedure TASTableEditor.InsertRow(AIndex: integer = -1);
var
  y, x: integer;
begin
  if FReadOnly then
    Exit;

  if AIndex = -1 then
    AIndex := FActiveCell.Y;

  AIndex := EnsureRange(AIndex, 0, FTable.Height - 1);

  FTable.SetSize(FTable.Width, FTable.Height + 1);

  for y := FTable.Height - 1 downto AIndex + 1 do
    for x := 0 to FTable.Width - 1 do
    begin
      FTable.Strings[Point(x, y)] := FTable.Strings[Point(x, y - 1)];
      FTable.CellStyles[Point(x, y)] := FTable.CellStyles[Point(x, y - 1)];
    end;

  for x := 0 to FTable.Width - 1 do
  begin
    FTable.Strings[Point(x, AIndex)] := '';
    FTable.CellStyles[Point(x, AIndex)].Clear;
  end;

  UpdateMetricsFromStyle;
  FActiveCell := Point(FActiveCell.X, AIndex);
  FSelAnchor := FActiveCell;
  Invalidate;
end;

procedure TASTableEditor.InsertCol(AIndex: integer = -1);
var
  y, x: integer;
begin
  if FReadOnly then
    Exit;

  if AIndex = -1 then
    AIndex := FActiveCell.X;

  AIndex := EnsureRange(AIndex, 0, FTable.Width - 1);

  FTable.SetSize(FTable.Width + 1, FTable.Height);

  for x := FTable.Width - 1 downto AIndex + 1 do
    for y := 0 to FTable.Height - 1 do
    begin
      FTable.Strings[Point(x, y)] := FTable.Strings[Point(x - 1, y)];
      FTable.CellStyles[Point(x, y)] := FTable.CellStyles[Point(x - 1, y)];
    end;

  for y := 0 to FTable.Height - 1 do
  begin
    FTable.Strings[Point(AIndex, y)] := '';
    FTable.CellStyles[Point(AIndex, y)].Clear;
  end;

  UpdateMetricsFromStyle;
  FActiveCell := Point(AIndex, FActiveCell.Y);
  FSelAnchor := FActiveCell;
  Invalidate;
end;

procedure TASTableEditor.DeleteRow(AIndex: integer = -1);
var
  y, x: integer;
begin
  if FReadOnly then
    Exit;

  if FTable.Height = 1 then
    Exit;

  if AIndex = -1 then
    AIndex := FActiveCell.Y;

  AIndex := EnsureRange(AIndex, 0, FTable.Height - 1);

  for y := AIndex to FTable.Height - 2 do
    for x := 0 to FTable.Width - 1 do
    begin
      FTable.Strings[Point(x, y)] := FTable.Strings[Point(x, y + 1)];
      FTable.CellStyles[Point(x, y)] := FTable.CellStyles[Point(x, y + 1)];
    end;

  FTable.SetSize(FTable.Width, FTable.Height - 1);

  UpdateMetricsFromStyle;
  FActiveCell := Point(FActiveCell.X, Min(AIndex, FTable.Height - 1));
  FSelAnchor := FActiveCell;
  Invalidate;
end;

procedure TASTableEditor.DeleteCol(AIndex: integer = -1);
var
  y, x: integer;
begin
  if FReadOnly then
    Exit;

  if FTable.Width = 1 then
    Exit;

  if AIndex = -1 then
    AIndex := FActiveCell.X;

  AIndex := EnsureRange(AIndex, 0, FTable.Width - 1);

  for x := AIndex to FTable.Width - 2 do
    for y := 0 to FTable.Height - 1 do
    begin
      FTable.Strings[Point(x, y)] := FTable.Strings[Point(x + 1, y)];
      FTable.CellStyles[Point(x, y)] := FTable.CellStyles[Point(x + 1, y)];
    end;

  FTable.SetSize(FTable.Width - 1, FTable.Height);

  UpdateMetricsFromStyle;
  FActiveCell := Point(Min(AIndex, FTable.Width - 1), FActiveCell.Y);
  FSelAnchor := FActiveCell;
  Invalidate;
end;

function EditTable(ATable: TASTable; AOwner: TCustomForm;
  const ACaption: string = ''): boolean;
resourcestring
  STableEditFrmCaption = 'Table Editor';
  SOk = 'OK';
  SCancel = 'Cancel';
var
  frm: TForm;
  editor: TASTableEditor;
  btnOk, btnCancel: TButton;
begin
  frm := TForm.Create(AOwner);
  try
    frm.BorderIcons := [biSystemMenu, biMaximize];
    frm.Position := poOwnerFormCenter;
    if ACaption <> '' then
      frm.Caption := ACaption
    else
      frm.Caption := STableEditFrmCaption;

    editor := TASTableEditor.Create(frm);
    editor.Parent := frm;
    editor.Table := TASTable.Create(ATable);
    editor.Color := clWindow;

    btnOk := TButton.Create(frm);
    btnOk.Parent := frm;
    btnOk.Default := true;
    btnOk.Caption := SOk;
    btnOk.ModalResult := mrOk;

    btnCancel := TButton.Create(frm);
    btnCancel.Parent := frm;
    btnCancel.Cancel := true;
    btnCancel.Caption := SCancel;
    btnCancel.ModalResult := mrCancel;

    btnCancel.Top := frm.ClientHeight - btnCancel.Height - 8;
    btnCancel.Left := frm.ClientWidth - btnCancel.Width - 8;
    btnOk.Top := btnCancel.Top;
    btnOk.Left := btnCancel.Left - btnOk.Width - 8;

    editor.Top := 0;
    editor.Left := 0;
    editor.Width := frm.ClientWidth;
    editor.Height := frm.ClientHeight - btnOk.Height - 16;

    editor.Anchors := [akLeft, akTop, akRight, akBottom];

    btnOk.Anchors := [akRight, akBottom];
    btnCancel.Anchors := [akRight, akBottom];

    frm.ClientWidth := Min(editor.TotalHorizontalExtent, 1024);
    frm.ClientHeight := Min(editor.TotalVerticalExtent, 768) + btnOk.Height + 16;;

    Result := frm.ShowModal = mrOk;
    if Result then
      ATable.Assign(editor.Table);

  finally
    frm.Free;
  end;
end;

procedure Register;
begin
  RegisterComponents('Rejbrand 2015', [TASTableEditor]);
end;

end.

