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, Generics.Defaults,
  Generics.Collections;

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 {invariant};
      BorderColor: TColor;
      FontFamily: string;
      FontSize: Integer {invariant};
      TextColor: TColor;
      FontStyles: TFontStyles;
      TextAlign: TAlignment;
      NoEllipsis: Boolean;
      Padding: Integer {invariant};
    end;
  const
    WM_MOUSEHWHEEL = $020E;
  var
    FTable: TASTable;
    FActiveCell: TPoint;
    FSelAnchor: TPoint;
    FSelectedAll: Boolean;
    FOldActiveCell: TPoint;
    FOldSelAnchor: TPoint;
    FCellWidth: Integer {invariant};
    FScaledCellWidth: Integer;
    FCellHeight: Integer {invariant};
    FScaledCellHeight: Integer;
    FBorderWidth: Integer {invariant};
    FScaledBorderWidth: Integer;
    FHorGridWidth, FVertGridWidth: Integer {invariant};
    FScaledHorGridWidth, FScaledVertGridWidth: Integer;
    FEditor: TTextEditor;
    FDoubleClicked: Boolean;
    FPopupMenu: TPopupMenu;
    FScrollPos: TPoint;
    FSPIScrollLines: Integer;
    FReadOnly: Boolean;
    FOnCanEdit: TCanEditCellQuery;
    FScrollAnchor: TPoint;
    FColLefts: array of Integer {invariant};
    FColWidths: array of Integer {invariant};
    FScaledColLefts: array of Integer;
    FScaledColWidths: array of Integer;
    FNoSelection: Boolean;
    FOnActiveCellChanged: TNotifyEvent;
    FUseRuxThemes: Boolean;
    FOrigin: TPoint;
    FZoomFactor: Double;
    FOnZoomChange: TNotifyEvent;
    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; overload;
    procedure ChangeCursor(X, Y: Integer); overload;
    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 TableDesignApply(Sender: TObject);
    procedure GUITransformResult(AType: Integer);
    procedure GUIQuickDesign;
    procedure SetNoSelection(const Value: Boolean);
    procedure SetUseRuxThemes(const Value: Boolean);
    procedure ScaleInvariants;
    procedure SetZoom(const Value: Double);
    procedure DoZoom;
    function GetZoomLevel: Integer;
    procedure SetZoomLevel(const Value: Integer);
    function GetEditorVisible: Boolean;
  strict private
    class var FInstances: TList<TASTableEditor>;
    class constructor ClassCreate;
    class destructor ClassDestroy;
    class procedure UxThemeUpdate; static;
    class var FOnAddInstance: TNotifyEvent;
  protected
    procedure DrawCell(const ACell: TPoint);
    procedure DrawCellNbh(const ACell: TPoint);
    procedure DrawBorder;
    procedure DrawGridLines;
    procedure ClearClientArea;
    procedure ClearOutskirts;
    procedure Paint; override;
    procedure CreateParams(var Params: TCreateParams); override;
    procedure WndProc(var Message: TMessage); 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 MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
      Y: Integer); override;
    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 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;
    procedure ChangeScale(M: Integer; D: Integer; isDpiChange: Boolean); override;
    procedure DoActiveCellChanged;
  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;
    procedure ClearSelectionStyles;
    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 GUISetDesign;
    procedure GUICellStyle;

    procedure RemoveCellStyle(x, y: Integer; const AName: string); overload;
    procedure RemoveCellStyle(const ACell: TPoint; const AName: string); overload;
    procedure SetCellStyle(x, y: Integer; const AName, AValue: string); overload;
    procedure SetCellStyle(const ACell: TPoint; const AName, AValue: string); overload;
    procedure FormatSelection(const AName, AValue: string);
    procedure FormatSelectionToggle(const AName, AValue: string);
    function SelectionHasConsistentFormat(const AName: string): Boolean; overload;
    function SelectionHasConsistentFormat(const AName, AValue: string): Boolean; overload;

    procedure LeftAlignSelection;
    procedure CenterSelection;
    procedure RightAlignSelection;

    procedure EmboldenSelection;
    procedure ItaliciseSelection;
    procedure UnderlineSelection;

    procedure UpdateMetricsFromStyle;

    procedure GUISetSize;

    property EditorVisible: Boolean read GetEditorVisible;

    class property OnAddInstance: TNotifyEvent read FOnAddInstance write FOnAddInstance;

  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 UseRuxThemes: Boolean read FUseRuxThemes write SetUseRuxThemes default False;
    property Visible;
    property ZoomFactor: Double read FZoomFactor write SetZoom;
    property ZoomLevel: Integer read GetZoomLevel write SetZoomLevel;
    property OnActiveCellChanged: TNotifyEvent read FOnActiveCellChanged write FOnActiveCellChanged; // on focused (active) cell coord changed OR focused (active) cell text changed
    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;
    property OnZoomChange: TNotifyEvent read FOnZoomChange write FOnZoomChange;
  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, Rux;

function Blend(AFactor: Double; C1, C2: TColor): TColor;
begin
  C1 := ColorToRGB(C1);
  C2 := ColorToRGB(C2);
  const s = AFactor;
  const t = 1 - s;
  Result :=
    RGB(
      Round(s * GetRValue(C1) + t * GetRValue(C2)),
      Round(s * GetGValue(C1) + t * GetGValue(C2)),
      Round(s * GetBValue(C1) + t * GetBValue(C2))
    );
end;

{ 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);
    DoActiveCellChanged;
  end
  else
    Beep;
end;

procedure TASTableEditor.ClearOutskirts;
begin
  Canvas.Brush.Color := Color;
  Canvas.Brush.Style := bsSolid;
  var R := Rect(TotalHorizontalExtent - FScrollPos.X + FOrigin.X, 0, ClientWidth, ClientHeight);
  Canvas.FillRect(R);
  R := Rect(0, TotalVerticalExtent - FScrollPos.Y + FOrigin.Y, ClientWidth, ClientHeight);
  Canvas.FillRect(R);
  if FOrigin.X > 0 then
  begin
    R := Rect(0, 0, FOrigin.X, ClientHeight);
    Canvas.FillRect(R);
  end;
  if FOrigin.Y > 0 then
  begin
    R := Rect(0, 0, ClientWidth, FOrigin.Y);
    Canvas.FillRect(R);
  end;
end;

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

procedure TASTableEditor.ClearSelectionStyles;
begin
  var P := Selection.TopLeft;
  var Q := Selection.BottomRight;
  for var y := P.Y to Q.Y do
    for var x := P.X to Q.X do
      if CanEdit(Point(x, y)) then
        FTable.CellStyles[Point(x, y)] := EmptyAttributes;
  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...';
  SClearTableStyle = 'Clear table style';
  SClear = 'Clear';
  SClearStyles = 'Clear styles';
  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_CLEAR_TABLE_STYLE = 105;
  MENU_CLEAR_CELL_STYLES = 106;
  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 {invariant};
  FCellHeight := 32 {invariant};
  FZoomFactor := 1.0;

  Color := clWindow;

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

  FColLefts[0] := 0 {invariant};
  FColWidths[0] := 100 {invariant};

  TabStop := True;

  DoubleBuffered := 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);

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SClearTableStyle;
  mi.Tag := MENU_CLEAR_TABLE_STYLE;
  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);

  mi := TMenuItem.Create(FPopupMenu);
  mi.Caption := SClearStyles;
  mi.Tag := MENU_CLEAR_CELL_STYLES;
  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);

  ScaleInvariants;

  if Assigned(FInstances) then
    FInstances.Add(Self);

  if Assigned(FOnAddInstance) then
    FOnAddInstance(Self);

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_CLEAR_TABLE_STYLE:
        begin
          FTable.ClearTableStyles;
          UpdateMetricsFromStyle;
          Invalidate;
        end;
      MENU_CELL_STYLE:
        GUICellStyle;
      MENU_CLEAR_CELL_STYLES:
        ClearSelectionStyles;
      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;
begin
  const p = Pos('×', S.ToLower.Replace('x', '×'));
  if p = 0 then
    Exit(False);
  var s1 := Trim(Copy(S, 1, p - 1));
  var 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 CrossCharProc(var C: Char);
begin
  if (C = 'x') or (C = 'X') or (C = '*') then
    C := '×';
end;

procedure TASTableEditor.GUISetSize;
resourcestring
  SSetSizeTitle = 'Table size';
  SSetSizeText = 'Please enter the new size of the table:';
var
  w, h: Integer;
begin
  if FReadOnly then
    Exit;
  var S := Format('%d × %d', [FTable.Height, FTable.Width]);
  if
    TMultiInputBox.TextInputBoxEx(
      GetParentForm(Self),
      SSetSizeTitle,
      SSetSizeText,
      S,
      ecNormal,
      ValidSizeInput,
      0,
      CrossCharProc
    )
      and
    TryParseSizeStr(S, h, w)
  then
  begin
    FTable.SetSize(w, h);
    UpdateAll;
  end;
end;

procedure TASTableEditor.GUISetDesign;
begin

  if FReadOnly then
    Exit;

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

end;


procedure TASTableEditor.GUIQuickDesign;
begin

  if FReadOnly then
    Exit;

  with TTableQuickDesignFrm.Create(GetParentForm(Self)) 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;
begin
  var LStyle := FTable.CellStyles[FActiveCell].Clone;
  if StyleDialog(GetParentForm(Self), '', LStyle) then
  begin
    var Sel := GetSelection;
    for var y := Sel.Top to Sel.Bottom do
      for var x := Sel.Left to Sel.Right do
        FTable.CellStyles[Point(x, y)] := LStyle.Clone;
    InvalidateRect(Handle, SelRect[Sel], False);
  end;
end;

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

          var Tbl := TASTable.Create;
          try
            Tbl.SetSize(RectHeight(Sel) + 1, RectWidth(Sel) + 1);
            for var y := 0 to Tbl.Height - 1 do
              for var 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
          var TxtEd := TTextEditor.Create(Frm);
          TxtEd.Parent := Frm;
          TxtEd.Align := alClient;
          TxtEd.BorderType := btNone;

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

          TxtEd.TextFile.BeginAddLine;
          try
            for var y := Sel.Top to Sel.Bottom do
              for var x := Sel.Left to Sel.Right do
                TxtEd.AddLine(FTable.Strings[Point(x, y)]);
          finally
            TxtEd.TextFile.EndAddLine;
          end;
          TxtEd.ClearUndoHistory;
          TxtEd.EditMode := emReadOnly;
          TxtEd.TextFile.StrictReadOnly := True;
        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.SelectionHasConsistentFormat(const AName,
  AValue: string): Boolean;
begin
  var P := Selection.TopLeft;
  var Q := Selection.BottomRight;
  for var y := P.Y to Q.Y do
    for var x := P.X to Q.X do
      if FTable.CellStyles[Point(x, y)][AName] <> AValue then
        Exit(False);
  Result := True;
end;

function TASTableEditor.SelectionHasConsistentFormat(
  const AName: string): Boolean;
begin
  if not HasSelection then
    Exit(True);
  var P := Selection.TopLeft;
  var Q := Selection.BottomRight;
  var LIsFirstValue := True;
  var LFirstValue: string;
  for var y := P.Y to Q.Y do
    for var x := P.X to Q.X do
    begin
      if LIsFirstValue then
      begin
        LFirstValue := FTable.CellStyles[Point(x, y)][AName];
        LIsFirstValue := False;
      end
      else if FTable.CellStyles[Point(x, y)][AName] <> LFirstValue then
        Exit(False);
    end;
  Result := True;
end;

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

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

begin
  P := Selection.TopLeft;
  Q := Selection.BottomRight;
  Result := GetRow(P.Y);
  for var 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
  if Assigned(FInstances) then
    FInstances.Remove(Self);
  FreeAndNil(FEditor);
  FreeAndNil(FTable);
  inherited;
end;

procedure TASTableEditor.DoActiveCellChanged;
begin
  if Assigned(FOnActiveCellChanged) then
    FOnActiveCellChanged(Self);
end;

procedure TASTableEditor.DoZoom;
begin
  if Assigned(FOnZoomChange) then
    FOnZoomChange(Self);
end;

procedure TASTableEditor.DrawCell(const ACell: TPoint);
begin

  if not FTable.CellExists(ACell) then
    Exit;

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

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

  // Background
  Canvas.Brush.Style := bsSolid;

  if CellSelected then
    if FUseRuxThemes then
      Canvas.Brush.Color := Blend(0.5, Style.BackgroundColor, TUx.ThemeData.InactiveCaptionColor)
    else
      Canvas.Brush.Color := InvertColor(Style.BackgroundColor)
  else
    Canvas.Brush.Color := Style.BackgroundColor;

  Canvas.FillRect(R);

  var LScaledBorderWidth := Round(ScaleValue(Style.BorderWidth) * FZoomFactor);

  var LOldR := R;

  // Border
  if Active then
  begin
    Style.BorderColor := IfThen(FUseRuxThemes, TUx.ThemeData.ActiveCaptionColor, clHighlight);
    Style.BorderWidth := Max(Style.BorderWidth, 5);
    LScaledBorderWidth := Round(ScaleValue(Style.BorderWidth) * FZoomFactor);
    InflateRect(
      R,
      (LScaledBorderWidth + FScaledVertGridWidth) div 2,
      (LScaledBorderWidth + FScaledHorGridWidth) div 2
    );
  end;

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

  R := LOldR;

  // Text
  Canvas.Font.Name := Style.FontFamily;
  Canvas.Font.Size := Round(Style.FontSize * FZoomFactor);
  if CellSelected then
    if FUseRuxThemes then
      Canvas.Font.Color := Style.TextColor
    else
      Canvas.Font.Color := InvertColor(Style.TextColor)
  else
    Canvas.Font.Color := Style.TextColor;
  Canvas.Font.Style := Style.FontStyles;

  var TF: TTextFormat := [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, -Round(ScaleValue(Style.Padding) * FZoomFactor), 0);

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

end;

function TASTableEditor.GetCellAt(const X, Y: Integer): TPoint;
begin
  with Point(X, Y) + FScrollPos - FOrigin do
  begin
    Result.X := 0;
    for var i := 0 to FTable.Width - 1 do
      if X > FScaledColLefts[i] then
        Result.X := i;
    Result.Y := EnsureRange((Y - FScaledBorderWidth) div (FScaledCellHeight + FScaledHorGridWidth),
      0, FTable.Height - 1);
  end;
end;

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

function TASTableEditor.GetCellRect(const ACell: TPoint): TRect;
begin
  Result.Left := FScaledColLefts[ACell.X];
  Result.Top := FScaledBorderWidth + (FScaledCellHeight + FScaledHorGridWidth) * ACell.y;
  Result.Right := Result.Left + FScaledColWidths[ACell.X];
  Result.Bottom := Result.Top + FScaledCellHeight;
  OffsetRect(Result, -FScrollPos.X, -FScrollPos.Y);
  Result.Offset(FOrigin);
end;

function TASTableEditor.GetEditorVisible: Boolean;
begin
  Result := Assigned(FEditor) and FEditor.Visible;
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);
begin

  if not (Assigned(FEditor) and FEditor.Visible) then
    Exit;

  if HasSelection and FillSel then
  begin
    var R := GetSelection;
    for var y := R.Top to R.Bottom do
      for var 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.UnderlineSelection;
begin
  FormatSelectionToggle('underline', 'true');
end;

procedure TASTableEditor.UpdateAll;
begin
  UpdateMetricsFromStyle;
  FActiveCell := Point(0, 0);
  FSelAnchor := Point(0, 0);
  Invalidate;
  DoActiveCellChanged;
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;
begin
  Result := AOrigin;
  case ADirection of
    dirUp:
      if AJump then
        for var 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 var 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 var 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 var 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, FScaledCellHeight - FScaledHorGridWidth))
      else
        SetActiveCell(Go(dirUp, ssCtrl in Shift), ssShift in Shift);
    VK_DOWN:
      if ScrollMode then
        SetScrollPosY(FScrollPos.Y + IfThen(ssCtrl in Shift, 1, FScaledCellHeight + FScaledHorGridWidth))
      else
        SetActiveCell(Go(dirDown, ssCtrl in Shift), ssShift in Shift);
    VK_LEFT:
      if ScrollMode then
        SetScrollPosX(FScrollPos.X - IfThen(ssCtrl in Shift, 1, FScaledCellWidth - FScaledVertGridWidth))
      else
        SetActiveCell(Go(dirLeft, ssCtrl in Shift), ssShift in Shift);
    VK_RIGHT:
      if ScrollMode then
        SetScrollPosX(FScrollPos.X + IfThen(ssCtrl in Shift, 1, FScaledCellWidth + FScaledVertGridWidth))
      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 (FScaledCellHeight + FScaledHorGridWidth))), 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 (FScaledCellHeight + FScaledHorGridWidth))), ssShift in Shift);
    VK_RETURN, VK_F2:
      ShowEditor;
    VK_DELETE:
      ClearSelection;
    VK_MENU, VK_SCROLL:
      ChangeCursor;
    VK_ADD, VK_OEM_PLUS:
      if ssCtrl in Shift then
        ZoomFactor := ZoomFactor + 0.1;
    VK_SUBTRACT, VK_OEM_MINUS:
      if ssCtrl in Shift then
        ZoomFactor := Max(0.1, ZoomFactor - 0.1);
    Ord('0'), VK_NUMPAD0:
      if ssCtrl in Shift then
        ZoomFactor := 1.0;
  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.LeftAlignSelection;
begin
  FormatSelection('text-align', 'left');
end;

procedure TASTableEditor.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
begin
  inherited;
  if FDoubleClicked then
    Exit;
  FScrollAnchor := Point(X, Y);
  ChangeCursor(X, Y);
  if ScrollMode(ssAlt in Shift) then
    Exit;
  var CellAt := GetCellAt(X, Y);
  if (FOrigin.X > 0) and (FScrollPos.X = 0) and (X < FOrigin.X) then
    SelectRow(CellAt.Y)
  else
    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(X, Y);

  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(X, Y);
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;
begin
  var AltDown := IsKeyDown(VK_LMENU);
  var ScrlLockOn := IsKeyOn(VK_SCROLL);
  Result := (AltDown xor ScrlLockOn) and not IsKeyDown(VK_SHIFT);
end;

procedure TASTableEditor.ScaleInvariants;
begin

  FScaledCellWidth := Round(ScaleValue(FCellWidth) * FZoomFactor);
  FScaledCellHeight := Round(ScaleValue(FCellHeight) * FZoomFactor);
  FScaledBorderWidth := Round(ScaleValue(FBorderWidth) * FZoomFactor);
  FScaledHorGridWidth := Round(ScaleValue(FHorGridWidth) * FZoomFactor);
  FScaledVertGridWidth := Round(ScaleValue(FVertGridWidth) * FZoomFactor);
  SetLength(FScaledColWidths, Length(FColWidths));
  for var i := 0 to High(FColWidths) do
    FScaledColWidths[i] := Round(ScaleValue(FColWidths[i]) * FZoomFactor);
  SetLength(FScaledColLefts, Length(FColLefts));
  FScaledColLefts[0] := FScaledBorderWidth;
  for var i := 1 to FTable.Width - 1 do
    FScaledColLefts[i] := FScaledColLefts[i - 1] + FScaledColWidths[i - 1] + FScaledVertGridWidth;

  if HandleAllocated then
  begin
    FOrigin.X := Max(0, (ClientWidth - GetTotalHorizontalExtent) div 2);
    FOrigin.Y := Max(0, (ClientHeight - GetTotalVerticalExtent) div 2);
  end;

end;

function TASTableEditor.ScrollMode(AAlt: Boolean): Boolean;
begin
  var AltDown := AAlt;
  var 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;
begin

  inherited;

  ClearClientArea;
  DrawGridLines;
  DrawBorder;

  var FVC := GetFirstVisibleCol;
  var LVC := GetLastVisibleCol;

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

  DrawCell(FActiveCell);

end;

function TASTableEditor.GetFirstVisibleRow: Integer;
begin
  Result := Max(0, FScrollPos.Y div (FScaledCellHeight + FScaledHorGridWidth) - 1);
end;

function TASTableEditor.GetLastVisibleRow: Integer;
begin
  Result := Min(GetFirstVisibleRow + Ceil(ClientHeight / (FScaledCellHeight + FScaledHorGridWidth)) + 2,
    FTable.Height - 1);
end;

function TASTableEditor.GetFirstVisibleCol: Integer;
begin
  for var i := 0 to High(FColLefts) do
    if FScaledColLefts[i] + FScaledColWidths[i] > FScrollPos.X then
      Exit(i);
  Result := FTable.Width;
end;

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

procedure TASTableEditor.PasteFromClipboard;
var
  rows: DynStringArray;
  cells: DynStringArray;
begin
  if Clipboard.HasFormat(CF_TEXT) then
  begin
    var S := Clipboard.AsText;
    if EndsStr(sLineBreak, S) then
      SetLength(S, Length(S) - Length(sLineBreak));
    rows := Split(S, sLineBreak);
    var maxwidth := 1;
    for var y := FActiveCell.Y to Min(FActiveCell.Y + Length(rows), FTable.Height) - 1 do
    begin
      cells := Split(rows[y - FActiveCell.Y], #9);
      for var 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);
    DoActiveCellChanged;
  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, Y) + FScrollPos - FOrigin do
    Result :=
      InRange(X, FScaledBorderWidth, FScaledColLefts[High(FScaledColLefts)] + FScaledColWidths[High(FScaledColWidths)])
        and
      InRange(Y, FScaledBorderWidth, GetTotalVerticalExtent - FScaledBorderWidth);
end;

procedure TASTableEditor.CenterSelection;
begin
  FormatSelection('text-align', 'center');
end;

class constructor TASTableEditor.ClassCreate;
begin
  FInstances := TList<TASTableEditor>.Create;
  TUx.RegisterCallback(UxThemeUpdate);
end;

class destructor TASTableEditor.ClassDestroy;
begin
  FreeAndNil(FInstances);
end;

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

procedure TASTableEditor.UpdateMetricsFromStyle;
var
  vals: string;
begin

  if not (FTable.Style.TryGetValue('table-border-width', vals) and TryStrToInt(vals, FBorderWidth) and (FBorderWidth >= 0)) then
    FBorderWidth := 1;

  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 var 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 var i := 1 to FTable.Width - 1 do
      FColLefts[i] := FColLefts[i - 1] + FColWidths[i - 1] + FVertGridWidth;
  end;

  ScaleInvariants;

  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.RemoveCellStyle(const ACell: TPoint;
  const AName: string);
begin
  var LStyles := FTable.CellStyles[ACell];
  LStyles.Remove(AName);
  FTable.CellStyles[ACell] := LStyles;
end;

procedure TASTableEditor.RemoveCellStyle(x, y: Integer; const AName: string);
begin
  RemoveCellStyle(Point(x, y), AName);
end;

procedure TASTableEditor.Resize;
begin
  inherited;
  FOrigin.X := Max(0, (ClientWidth - GetTotalHorizontalExtent) div 2);
  FOrigin.Y := Max(0, (ClientHeight - GetTotalVerticalExtent) div 2);
  if FOrigin.X > 0 then
    FScrollPos.X := 0;
  if FOrigin.Y > 0 then
    FScrollPos.Y := 0;
  if not FOrigin.IsZero then
    Invalidate;
  UpdateScrollBars;
end;

procedure TASTableEditor.RightAlignSelection;
begin
  FormatSelection('text-align', 'right');
end;

procedure TASTableEditor.DrawCellNbh(const ACell: TPoint);
begin
  for var i := ACell.X - 1 to ACell.X + 1 do
    for var j := ACell.Y - 1 to ACell.Y + 1 do
      DrawCell(Point(i, j));
  if (ACell.X <= 0) or (ACell.Y <= 0) or (ACell.X >= FTable.Width - 1) or (ACell.Y >= FTable.Height - 1) then
    ClearOutskirts;
end;

procedure TASTableEditor.DrawGridLines;
var
  vals: string;
  TableGridColor: TColor;
  R: TRect;
begin
  if not (FTable.Style.TryGetValue('horizontal-grid-color', vals) and TryStrToColor(vals, TableGridColor)) then
    TableGridColor := IfThen(FUseRuxThemes, TUx.ThemeData.InactiveCaptionColor, clGray);
  Canvas.Brush.Color := TableGridColor;
  Canvas.Brush.Style := bsSolid;
  for var y := Max(GetFirstVisibleRow - 1, 1) to Min(GetLastVisibleRow + 1, FTable.Height - 1) do
  begin
    R.Left := FScaledBorderWidth;
    R.Right := FScaledColLefts[High(FScaledColLefts)] + FScaledColWidths[High(FScaledColWidths)];
    R.Top := FScaledBorderWidth + y * FScaledCellHeight + (y - 1) * FScaledHorGridWidth;
    R.Bottom := R.Top + FScaledHorGridWidth;
    OffsetRect(R, -FScrollPos.X, -FScrollPos.Y);
    R.Offset(FOrigin);
    Canvas.FillRect(R);
  end;
  if not (FTable.Style.TryGetValue('vertical-grid-color', vals) and TryStrToColor(vals, TableGridColor)) then
    TableGridColor := IfThen(FUseRuxThemes, TUx.ThemeData.InactiveCaptionColor, clGray);
  Canvas.Brush.Color := TableGridColor;
  for var x := Max(GetFirstVisibleCol - 1, 1) to Min(GetLastVisibleCol + 1, FTable.Width - 1) do
  begin
    R.Top := FScaledBorderWidth;
    R.Bottom := R.Top + FTable.Height * FScaledCellHeight + (FTable.Height - 1) * FScaledHorGridWidth;
    R.Left := FScaledColLefts[x - 1] + FScaledColWidths[x - 1];
    R.Right := R.Left + FScaledVertGridWidth;
    OffsetRect(R, -FScrollPos.X, -FScrollPos.Y);
    R.Offset(FOrigin);
    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 - FScaledCellHeight - FScaledVertGridWidth);
    SB_LINEDOWN:
      SetScrollPosY(FScrollPos.Y + FScaledCellHeight + FScaledVertGridWidth);
    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);
begin
  var 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;
  DoActiveCellChanged;
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 - FScaledCellWidth - FScaledVertGridWidth);
    SB_LINERIGHT:
      SetScrollPosX(FScrollPos.X + FScaledCellWidth + FScaledVertGridWidth);
    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((FScaledCellWidth div 5) * Message.WheelDelta / WHEEL_DELTA));
end;

procedure TASTableEditor.WMMouseWheel(var Message: TWMMouseWheel);
begin

  if Message.Keys and MK_CONTROL <> 0 then
  begin
    if Message.WheelDelta > 0 then
      ZoomFactor := ZoomFactor + 0.1
    else
      ZoomFactor := Max(0.1, ZoomFactor - 0.1);
  end
  else if Message.Keys and MK_SHIFT <> 0 then
    SetScrollPosY(FScrollPos.Y - Sign(Message.WheelDelta))
  else
    if FSPIScrollLines >= 0 then
      SetScrollPosY(FScrollPos.Y - Round((FScaledCellHeight 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);
begin
  var 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);
begin
  var OldScrollPosX := FScrollPos.X;
  var 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;
begin

  var ScrollInfo := Default(TScrollInfo);

  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;

class procedure TASTableEditor.UxThemeUpdate;
begin
  if Assigned(FInstances) then
    for var Editor in FInstances do
      if Editor.UseRuxThemes then
        Editor.Invalidate;
end;

function TASTableEditor.GetTotalVerticalExtent: Integer;
begin
  Result := FTable.Height * FScaledCellHeight + (FTable.Height - 1) * FScaledHorGridWidth + 2*FScaledBorderWidth;
end;

function TASTableEditor.GetZoomLevel: Integer;
begin
  Result := Round(100 * FZoomFactor);
end;

function TASTableEditor.GetTotalHorizontalExtent: Integer;
begin
  Result := FScaledColLefts[High(FScaledColLefts)] + FScaledColWidths[High(FScaledColWidths)] + FScaledBorderWidth;
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;

  DoActiveCellChanged;

end;

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

procedure TASTableEditor.SetCellStyle(const ACell: TPoint; const AName,
  AValue: string);
begin
  var LStyles := FTable.CellStyles[ACell];
  LStyles[AName] := AValue;
  FTable.CellStyles[ACell] := LStyles;
end;

procedure TASTableEditor.SetCellStyle(x, y: Integer; const AName,
  AValue: string);
begin
  SetCellStyle(Point(x, y), AName, AValue);
end;

procedure TASTableEditor.SetCellWidth(const Value: Integer);
begin
  if FCellWidth <> Value then
  begin
    FCellWidth := Value;
    ScaleInvariants;
    Invalidate;
    UpdateScrollBars;
  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.SetUseRuxThemes(const Value: Boolean);
begin
  if FUseRuxThemes <> Value then
  begin
    FUseRuxThemes := Value;
    Invalidate;
  end;
end;

procedure TASTableEditor.SetZoom(const Value: Double);
begin
  const LValue = EnsureRange(Value, 0.01, 100);
  if not SameValue(FZoomFactor, LValue) then
  begin
    HideEditor;
    FZoomFactor := LValue;
    ScaleInvariants;
    if FOrigin.X > 0 then
      FScrollPos.X := 0;
    if FOrigin.Y > 0 then
      FScrollPos.Y := 0;
    UpdateScrollBars;
    Invalidate;
    DoZoom;
  end;
end;

procedure TASTableEditor.SetZoomLevel(const Value: Integer);
begin
  ZoomFactor := Value / 100;
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)
        else
          DoActiveCellChanged;
        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.EmboldenSelection;
begin
  FormatSelectionToggle('bold', 'true');
end;

procedure TASTableEditor.FormatSelection(const AName, AValue: string);
begin
  var P := Selection.TopLeft;
  var Q := Selection.BottomRight;
  for var y := P.Y to Q.Y do
    for var x := P.X to Q.X do
      if CanEdit(Point(x, y)) then
        SetCellStyle(x, y, AName, AValue);
  InvalidateRect(Handle, GetSelRect(Selection), False);
end;

procedure TASTableEditor.FormatSelectionToggle(const AName, AValue: string);
begin
  const LOldState = FTable.CellStyles[ActiveCell][AName] = AValue;
  var P := Selection.TopLeft;
  var Q := Selection.BottomRight;
  for var y := P.Y to Q.Y do
    for var x := P.X to Q.X do
      if CanEdit(Point(x, y)) then
        if LOldState then
          RemoveCellStyle(x, y, AName)
        else
          SetCellStyle(x, y, AName, AValue);
  InvalidateRect(Handle, GetSelRect(Selection), False);
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.AutoReplace := True;
  FEditor.ClearUndoHistory;
  if AInitialText <> '' then
    FEditor.PlainText := AInitialText;
  FEditor.BorderType := btSimpleColor;
  FEditor.BorderColor := IfThen(FUseRuxThemes, TUx.ThemeData.ActiveCaptionColor, clHighlight);
  FEditor.BorderWidth := 3;
  FEditor.Zoom := Self.ZoomLevel;
  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 := FScaledBorderWidth;
  Result.Top := FScaledBorderWidth;
  Result.Right := FScaledColLefts[High(FScaledColLefts)] + FScaledColWidths[High(FScaledColWidths)];
  Result.Bottom := FScaledBorderWidth + FTable.Height * FScaledCellHeight +
    Max(0, (FTable.Height - 1)) * FScaledHorGridWidth;
  OffsetRect(Result, -FScrollPos.X, -FScrollPos.Y);
  Result.Offset(FOrigin);
end;

procedure TASTableEditor.WMContextMenu(var Message: TWMContextMenu);
var
  pm: TPopupMenu;
  OnCell: Boolean;
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

    var p := GetCellRect(ActiveCell).TopLeft;
    Inc(p.X, 2 * FScaledCellWidth div 3);
    Inc(p.Y, 2 * FScaledCellHeight div 3);

    for var 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 and Clipboard.HasFormat(CF_TEXT);
        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_CLEAR_TABLE_STYLE:
          FPopupMenu.Items[i].Enabled := True;
        MENU_CELL_STYLE:
          FPopupMenu.Items[i].Enabled := HasSelection or CanEdit(FActiveCell);
        MENU_CLEAR_CELL_STYLES:
          FPopupMenu.Items[i].Enabled := True;
        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

    var 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 var 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 and Clipboard.HasFormat(CF_TEXT);
        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_CLEAR_TABLE_STYLE:
          FPopupMenu.Items[i].Enabled := True;
        MENU_CELL_STYLE:
          FPopupMenu.Items[i].Enabled := (HasSelection and not FReadOnly) or (OnCell and CanEdit(FActiveCell));
        MENU_CLEAR_CELL_STYLES:
          FPopupMenu.Items[i].Enabled := True;
        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;
begin

  var 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(X, Y: Integer);
begin

  if ScrollMode then
    if csLButtonDown in ControlState then
      Cursor := crHandHold
    else
      Cursor := crHand
  else
    if (FOrigin.X > 0) and (FScrollPos.X = 0) and (X < FOrigin.X) then
      Cursor :=  crLineSel
    else
      Cursor := crDefault;

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

end;

procedure TASTableEditor.ChangeCursor;
begin
  with ScreenToClient(Mouse.CursorPos) do
    ChangeCursor(X, Y);
end;

procedure TASTableEditor.ChangeScale(M, D: Integer; isDpiChange: Boolean);
begin
  inherited;
  ScaleInvariants;
  Invalidate;
  UpdateScrollBars;
end;

procedure TASTableEditor.InsertRow(AIndex: Integer = -1);
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 var y := FTable.Height - 1 downto AIndex + 1 do
    for var 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 var 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;
  DoActiveCellChanged;

end;

procedure TASTableEditor.ItaliciseSelection;
begin
  FormatSelectionToggle('italic', 'true');
end;

procedure TASTableEditor.InsertCol(AIndex: Integer = -1);
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 var x := FTable.Width - 1 downto AIndex + 1 do
    for var 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 var 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;
  DoActiveCellChanged;

end;

procedure TASTableEditor.DeleteRow(AIndex: Integer = -1);
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 var y := AIndex to FTable.Height - 2 do
    for var 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;
  DoActiveCellChanged;

end;

procedure TASTableEditor.DeleteCol(AIndex: Integer = -1);
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 var x := AIndex to FTable.Width - 2 do
    for var 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;
  DoActiveCellChanged;

end;

function EditTable(ATable: TASTable; AOwner: TCustomForm;
  const ACaption: string = ''): Boolean;
resourcestring
  STableEditFrmCaption = 'Table Editor';
  SOk = 'OK';
  SCancel = 'Cancel';
begin

  var frm := TForm.Create(AOwner);
  try
    frm.BorderIcons := [biSystemMenu, biMaximize];
    frm.Position := poOwnerFormCenter;
    if ACaption <> '' then
      frm.Caption := ACaption
    else
      frm.Caption := STableEditFrmCaption;

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

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

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

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

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

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

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

    frm.ClientWidth := Min(editor.TotalHorizontalExtent, frm.ScaleValue(1024));
    frm.ClientHeight := Min(editor.TotalVerticalExtent, frm.ScaleValue(768)) + btnOk.Height + frm.ScaleValue(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.

