{

    "SHEdit" - Text editor with syntax highlighting
    Copyright (C) 1999-2000 by Sebastian Guenther (sg@freepascal.org)

    See the file COPYING.FPC, included in this distribution,
    for details about the copyright.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
}

// ===================================================================
//   Generic text editor widget with syntax highlighting capabilities
// ===================================================================

{$MODE objfpc}
{$H+}

unit shedit;

interface

uses
  Classes, doc_text;

type

  TSHTextEdit = class;
  TSHTextEditClass = class of TSHTextEdit;


// -------------------------------------------------------------------
//   Keyboard/action assignment handling
// -------------------------------------------------------------------

  TKeyboardActionProc = procedure of object;

  TSelectionAction = (selNothing,selExtend,selClear);

  TKeyboardActionDescr = class(TCollectionItem)
  public
    Descr: String;                      // Human readable description
    Method: TKeyboardActionProc;
    SelectionAction : TSelectionAction;
  end;

  TShortcut = class(TCollectionItem)
  public
    KeyCode: Integer;
    ShiftState: TShiftState;
    Action: TKeyboardActionDescr;
  end;


// -------------------------------------------------------------------
//   Undo/redo buffer stuff
// -------------------------------------------------------------------

  TUndoInfo = class;
  TUndoInfo = class
    Prev, Next: TUndoInfo;
    CursorX, CursorY: Integer;
    function Merge(AEdit: TSHTextEdit; AInfo: TUndoInfo): Boolean; virtual;
    procedure DoUndo(AEdit: TSHTextEdit); virtual; abstract;
  end;

  TUndoEdit = class(TUndoInfo)
    NumOfChars: Integer;
    constructor Create;
    constructor Create(ANumOfChars: Integer);
    function Merge(AEdit: TSHTextEdit; AInfo: TUndoInfo): Boolean; override;
    procedure DoUndo(AEdit: TSHTextEdit); override;
  end;

  TUndoDelLeft = class(TUndoInfo)
    DeletedString: String;
    constructor Create(const ADeletedString: String);
    function Merge(AEdit: TSHTextEdit; AInfo: TUndoInfo): Boolean; override;
    procedure DoUndo(AEdit: TSHTextEdit); override;
  end;

  TUndoDelRight = class(TUndoDelLeft)
    procedure DoUndo(AEdit: TSHTextEdit); override;
  end;


// -------------------------------------------------------------------
//   Selection support
// -------------------------------------------------------------------

  TSelection = class
  public
    constructor Create;
    procedure Clear;

    StartX, StartY, EndX, EndY: Integer;

    function IsValid: Boolean;
    function IsEmpty: Boolean;
    // Ordered coordinates: swaps start and end if necessary
    function OStartX: Integer;
    function OStartY: Integer;
    function OEndX: Integer;
    function OEndY: Integer;
  end;


// -------------------------------------------------------------------
//   SHWidget interface
// -------------------------------------------------------------------

  ISHWidget = class
    // Drawing
    procedure InvalidateRect(x, y, w, h: Integer); virtual; abstract;
    procedure ClearRect(x, y, w, h: Integer); virtual; abstract;
    procedure DrawTextLine(x1, x2, y: Integer; s: PChar); virtual; abstract;

    // Cursor placement
    procedure ShowCursor(x, y: Integer); virtual; abstract;
    procedure HideCursor(x, y: Integer); virtual; abstract;

    // Scrolling support
    function  GetHorzPos: Integer; virtual; abstract;
    procedure SetHorzPos(x: Integer); virtual; abstract;
    function  GetVertPos: Integer; virtual; abstract;
    procedure SetVertPos(y: Integer); virtual; abstract;
    function  GetPageWidth: Integer; virtual; abstract;
    function  GetPageHeight: Integer; virtual; abstract;
    function  GetLineWidth: Integer; virtual; abstract;
    procedure SetLineWidth(count: Integer); virtual; abstract;
    function  GetLineCount: Integer; virtual; abstract;
    procedure SetLineCount(count: Integer); virtual; abstract;

    // Clipboard support
    function  GetClipboard: String; virtual; abstract;
    procedure SetClipboard(Content: String); virtual; abstract;


    property  HorzPos: Integer read GetHorzPos write SetHorzPos;
    property  VertPos: Integer read GetVertPos write SetVertPos;
    property  PageWidth: Integer read GetPageWidth;
    property  PageHeight: Integer read GetPageHeight;
    property  LineWidth: Integer read GetLineWidth write SetLineWidth;
    property  LineCount: Integer read GetLineCount write SetLineCount;
    property  Clipboard: String read GetClipboard write SetClipboard;
  end;


// -------------------------------------------------------------------
//   SHTextEdit: The main editor class
// -------------------------------------------------------------------

  TShortcutEvent = procedure of object;

  TEditLineEvent = procedure(Sender: TSHTextEdit; Line: Integer) of object;

  TSHTextEdit = class
  protected
    // ===== Internally used stuff
    ViewInfo: TViewInfo;                // Connection to document
    CursorVisible: Integer;
    OverwriteMode: Boolean;
    LastUndoInfo, LastRedoInfo: TUndoInfo;      // tails of double linked lists

    FSel: TSelection;
    OldSelValid: Boolean;
    OldSelStartX, OldSelStartY, OldSelEndX, OldSelEndY: Integer;

    // OnKeyPressed saves the cursor position before calling key handlers
    LastCursorX, LastCursorY: Integer;


    function  CalcSHFlags(FlagsIn: Byte; source: String): Byte;
    procedure ChangeInLine(line: Integer);  // Redraws screen where necessary
    procedure AddUndoInfo(AInfo: TUndoInfo; CanMerge: Boolean);

    // The default implementation does not perform any syntax highlighting:
    procedure DoHighlighting(var flags: Byte; source, dest: PChar); virtual;

    // ===== Properties

    FDoc: TTextDoc;                     // Document object for text
    FCursorX, FCursorY: Integer;        // 0/0 = upper left corner
    FOnModifiedChange: TNotifyEvent;
    FOnLineInsert, FOnLineRemove: TEditLineEvent;
    FWidget: ISHWidget;

    procedure SetCursorX(NewCursorX: Integer);
    procedure SetCursorY(NewCursorY: Integer);

    procedure DocumentCleared(Sender: TObject);
    procedure ModifiedChanged(Sender: TObject);
    procedure LineInserted(Sender: TTextDoc; Line: Integer); virtual;
    procedure LineRemoved(Sender: TTextDoc; Line: Integer); virtual;
    procedure LineChanged(Sender: TTextDoc; Line: Integer); virtual;

    function  ExecKey(Key: Char; BlockMode: Boolean): Boolean;
    procedure ExecKeys(Keys: String; BlockMode: Boolean);
    procedure MultiDelLeft(Count: Integer);

  public
    // Keyboard command handlers
    // Cursor movement
    procedure AdjustCursorToRange;
    procedure AdjustRangeToCursor;
    procedure CursorUp;
    procedure CursorDown;
    procedure CursorLeft;
    procedure CursorRight;
    procedure CursorHome;
    procedure CursorEnd;
    procedure CursorDocBegin;
    procedure CursorDocEnd;
    procedure CursorPageUp;
    procedure CursorPageDown;

    // Misc
    procedure ToggleOverwriteMode;
    procedure EditDelLeft;
    procedure EditDelRight;
    procedure EditDelLine;
    procedure EditUndo;
    procedure EditRedo;
    procedure ClipboardCut;
    procedure ClipboardCopy;
    procedure ClipboardPaste;

    // Customizable keyboard handlers
    procedure KeyReturn; virtual;

  public
    constructor Create(ADoc: TTextDoc; AWidget: ISHWidget); virtual;
    destructor Destroy; override;
    function  AddKeyboardAction(AMethod: TKeyboardActionProc;ASelectionAction:TSelectionAction;ADescr: String): TKeyboardActionDescr;
    function AddKeyboardAssignment(AKeyCode: Integer; AShiftState: TShiftState;
      AAction: TKeyboardActionDescr): TShortcut;
    procedure AddKeyDef(AMethod: TKeyboardActionProc; ASelectionAction:TSelectionAction; ADescr: String; AKeyCode: Integer; AShiftState: TShiftState);

    procedure HideCursor;
    procedure ShowCursor;
    procedure FocusIn;
    procedure FocusOut;
    procedure DrawContent(x, y, w, h: Integer);

    // Return value: True=Key has been pressed, False=Key has not been processed
    function  KeyPressed(KeyCode: LongWord; ShiftState: TShiftState): Boolean; virtual;

    procedure StartSelectionChange;
    procedure EndSelectionChange;

    KeyboardActions: TCollection;
    Shortcuts: TCollection;

    shDefault, shSelected: Integer;

    property Doc: TTextDoc read FDoc;
    property CursorX: Integer read FCursorX write SetCursorX;
    property CursorY: Integer read FCursorY write SetCursorY;
    property Selection: TSelection read FSel write FSel;
    property OnModifiedChange: TNotifyEvent read FOnModifiedChange write FOnModifiedChange;
    property OnLineInsert: TEditLineEvent read FOnLineInsert write FOnLineInsert;
    property OnLineRemove: TEditLineEvent read FOnLineRemove write FOnLineRemove;
    property Widget: ISHWidget read FWidget;
  end;



implementation

uses
  SysUtils;


{$INCLUDE undo.inc}
{$INCLUDE keys.inc}
{$INCLUDE drawing.inc}


constructor TSelection.Create;
begin
  inherited Create;
  Clear;
end;

function TSelection.IsValid: Boolean;
begin
  Result := StartX <> -1;
end;

function TSelection.IsEmpty: Boolean;
begin
  Result := (StartX = EndX) and (StartY = EndY);
end;

function TSelection.OStartX: Integer;
begin
  if (StartY > EndY) or ((StartY = EndY) and (StartX > EndX)) then
    Result := EndX
  else
    Result := StartX;
end;

function TSelection.OStartY: Integer;
begin
  if (StartY > EndY) or ((StartY = EndY) and (StartX > EndX)) then
    Result := EndY
  else
    Result := StartY;
end;

function TSelection.OEndX: Integer;
begin
  if (StartY > EndY) or ((StartY = EndY) and (StartX > EndX)) then
    Result := StartX
  else
    Result := EndX;
end;

function TSelection.OEndY: Integer;
begin
  if (StartY > EndY) or ((StartY = EndY) and (StartX > EndX)) then
    Result := StartY
  else
    Result := EndY;
end;



procedure TSelection.Clear;
begin
  StartX := -1;
  StartY := -1;
  EndX := -1;
  EndY := -1;
end;


constructor TSHTextEdit.Create(ADoc: TTextDoc; AWidget: ISHWidget);
var
  i: Integer;
begin
  ASSERT(Assigned(ADoc) and Assigned(AWidget));

  FDoc := ADoc;

  ViewInfo := TViewInfo(FDoc.ViewInfos.Add);
  ViewInfo.OnLineInsert := @LineInserted;
  ViewInfo.OnLineRemove := @LineRemoved;
  ViewInfo.OnLineChange := @LineChanged;
  ViewInfo.OnModifiedChange := @ModifiedChanged;
  ViewInfo.OnClearDocument := @DocumentCleared;

  FWidget := AWidget;

  FSel := TSelection.Create;

  KeyboardActions := TCollection.Create(TKeyboardActionDescr);
  Shortcuts := TCollection.Create(TShortcut);

  Widget.LineCount := FDoc.LineCount;
  Widget.LineWidth := FDoc.LineWidth;
  CursorX:=0;
  CursorY:=0;
end;

destructor TSHTextEdit.Destroy;
var
  buf, prev: TUndoInfo;
begin
  ViewInfo.Free;
  FDoc.Release;
  KeyboardActions.Free;
  Shortcuts.Free;
  FSel.Free;

  buf := LastUndoInfo;
  while Assigned(buf) do begin
    prev := buf.prev;
    buf.Free;
    buf := prev;
  end;

  buf := LastRedoInfo;
  while Assigned(buf) do begin
    prev := buf.prev;
    buf.Free;
    buf := prev;
  end;

  inherited Destroy;
end;

procedure TSHTextEdit.DocumentCleared(Sender: TObject);
begin
  FCursorX := 0;
  FCursorY := 0;
  FSel.Clear;
  AdjustRangeToCursor;
  Widget.ClearRect(0, 0, Widget.PageWidth, Widget.PageHeight);
end;

procedure TSHTextEdit.ModifiedChanged(Sender: TObject);
begin
  if Assigned(OnModifiedChange) then
    OnModifiedChange(Self);
end;

procedure TSHTextEdit.FocusIn;
begin
  CursorVisible := 0;
  ShowCursor;
end;

procedure TSHTextEdit.FocusOut;
begin
  HideCursor;
end;


procedure TSHTextEdit.SetCursorX(NewCursorX: Integer);
begin
  HideCursor;
  if NewCursorX >= 0 then
    FCursorX := NewCursorX
  else
    FCursorX := 0;
  if FCursorX > FDoc.LineWidth then
    Widget.LineWidth := FCursorX + 4
  else
    Widget.LineWidth := FDoc.LineWidth + 4;
  ShowCursor;
  AdjustRangeToCursor;
end;

procedure TSHTextEdit.SetCursorY(NewCursorY: Integer);
begin
  HideCursor;
  if NewCursorY >= 0 then
    FCursorY := NewCursorY
  else
    FCursorY := 0;
  ShowCursor;
  AdjustRangeToCursor;
end;

procedure TSHTextEdit.LineInserted(Sender: TTextDoc; Line: Integer);
begin
  Widget.LineCount := FDoc.LineCount;
  if FCursorX > FDoc.LineWidth then
    Widget.LineWidth := FCursorX + 4
  else
    Widget.LineWidth := FDoc.LineWidth + 4;
  if Assigned(FOnLineInsert) then
    FOnLineInsert(Self, Line);
  ChangeInLine(Line);
end;

procedure TSHTextEdit.LineRemoved(Sender: TTextDoc; Line: Integer);
begin
  Widget.LineCount := FDoc.LineCount;
  if FCursorX > FDoc.LineWidth then
    Widget.LineWidth := FCursorX + 4
  else
    Widget.LineWidth := FDoc.LineWidth + 4;
  if Assigned(FOnLineRemove) then
    FOnLineRemove(Self, Line);
  ChangeInLine(Line);
end;

procedure TSHTextEdit.LineChanged(Sender: TTextDoc; Line: Integer);
begin
  if FCursorX > FDoc.LineWidth then
    Widget.LineWidth := FCursorX + 4
  else
    Widget.LineWidth := FDoc.LineWidth + 4;
  ChangeInLine(Line);
end;

procedure TSHTextEdit.StartSelectionChange;
begin
  HideCursor;

  LastCursorX := FCursorX;
  LastCursorY := FCursorY;
  OldSelValid := FSel.IsValid;
  if OldSelValid then begin
    OldSelStartX := FSel.OStartX;
    OldSelStartY := FSel.OStartY;
    OldSelEndX := FSel.OEndX;
    OldSelEndY := FSel.OEndY;
  end;
end;

procedure TSHTextEdit.EndSelectionChange;

  procedure RedrawArea(x1, y1, x2, y2: Integer);
  begin
    //WriteLn('Redraw: ', x1, '/', y1, ' - ', x2, '/', y2);
    if y1 = y2 then
      FWidget.InvalidateRect(x1, y1, (x2 - x1) + 1, (y2 - y1) + 1)
    else begin
      FWidget.InvalidateRect(x1, y1, FWidget.PageWidth + FWidget.HorzPos, 1);
      if y1 < y2 - 1 then
        FWidget.InvalidateRect(0, y1 + 1, FWidget.PageWidth + FWidget.HorzPos, (y2 - y1) - 1);
      FWidget.InvalidateRect(0, y2, x2, 1);
    end;
  end;

begin
//WriteLn('=> TSHTextEdit.EndSelectionChange');
  if not OldSelValid then begin
    if FSel.IsValid then
      RedrawArea(FSel.OStartX, FSel.OStartY, FSel.OEndX, FSel.OEndY);
  end else begin
    //WriteLn('Old selection: ', OldSelStartX, '/', OldSelStartY, ' - ', OldSelEndX, '/', OldSelEndY);
    if not FSel.IsValid then begin
      //WriteLn('No new selection');
      RedrawArea(OldSelStartX, OldSelStartY, OldSelEndX, OldSelEndY);
    end else begin
      //WriteLn('New selection: ', FSel.OStartX, '/', FSel.OStartY, ' - ', FSel.OEndX, '/', FSel.OEndY);
      // Do OldSel and FSel intersect?
      if (OldSelEndY < FSel.OStartY) or (OldSelStartY > FSel.OEndY) or
         ((OldSelEndY = FSel.OStartY) and (OldSelEndX <= FSel.OStartX)) or
         ((OldSelStartY = FSel.OEndY) and (OldSelStartX >= FSel.OEndX)) then
      begin
        RedrawArea(OldSelStartX, OldSelStartY, OldSelEndX, OldSelEndY);
        RedrawArea(FSel.OStartX, FSel.OStartY, FSel.OEndX, FSel.OEndY);
      end else begin
        // Intersection => determine smallest possible area(s) to redraw
        // 1. Check if the start position has changed
        if (OldSelStartX <> FSel.OStartX) or (OldSelStartY <> FSel.OStartY) then
          if (OldSelStartY < FSel.OStartY) or ((OldSelStartY = FSel.OStartY) and
             (OldSelStartX < FSel.OStartX)) then
            RedrawArea(OldSelStartX, OldSelStartY, FSel.OStartX, FSel.OStartY)
          else
            RedrawArea(FSel.OStartX, FSel.OStartY, OldSelStartX, OldSelStartY);
          // 2. Check if end position has changed
          if (OldSelEndX <> FSel.OEndX) or (OldSelEndY <> FSel.OEndY) then
            if (OldSelEndY < FSel.OEndY) or ((OldSelEndY = FSel.OEndY) and
               (OldSelEndX < FSel.OEndX)) then
              RedrawArea(OldSelEndX, OldSelEndY, FSel.OEndX, FSel.OEndY)
            else
              RedrawArea(FSel.OEndX, FSel.OEndY, OldSelEndX, OldSelEndY);
      end;
    end;
  end;
  ShowCursor;
end;


end.
