lazarus/components/synedit/syngutter.pp

492 lines
15 KiB
ObjectPascal

unit SynGutter;
{$I synedit.inc}
interface
uses
SysUtils, Classes, Controls, Graphics, LCLType, LCLIntf, Menus,
SynEditMarks, SynEditTypes, SynEditMiscClasses, SynEditMiscProcs, LazSynTextArea,
SynTextDrawer, SynGutterBase, SynGutterLineNumber, SynGutterCodeFolding,
SynGutterMarks, SynGutterChanges, SynEditMouseCmds, SynGutterLineOverview;
type
TSynGutterSeparator = class;
TLazSynGutterArea = class;
{ TSynGutter }
TSynGutter = class(TSynGutterBase)
private
FCursor: TCursor;
FOnGutterClick: TGutterClickEvent;
FMouseDownPart: Integer;
function PixelToPartIndex(X: Integer): Integer;
protected
procedure DoDefaultGutterClick(Sender: TObject; X, Y, Line: integer;
mark: TSynEditMark); override;
function CreatePartList: TSynGutterPartListBase; override;
procedure CreateDefaultGutterParts; virtual;
function CreateMouseActions: TSynEditMouseInternalActions; override;
property GutterArea;
public
constructor Create(AOwner : TSynEditBase; ASide: TSynGutterSide;
ATextDrawer: TheTextDrawer);
destructor Destroy; override;
procedure Paint(Canvas: TCanvas; Surface:TLazSynGutterArea; AClip: TRect; FirstLine, LastLine: integer);
function HasCustomPopupMenu(out PopMenu: TPopupMenu): Boolean;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
procedure MouseMove(Shift: TShiftState; X, Y: Integer);
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
function MaybeHandleMouseAction(var AnInfo: TSynEditMouseActionInfo;
HandleActionProc: TSynEditMouseActionHandler): Boolean; override;
function DoHandleMouseAction(AnAction: TSynEditMouseAction;
var AnInfo: TSynEditMouseActionInfo): Boolean;
procedure ResetMouseActions; override; // set mouse-actions according to current Options / may clear them
procedure DoOnGutterClick(X, Y: integer);
property OnGutterClick: TGutterClickEvent
read FOnGutterClick write FOnGutterClick;
public
// Access to well known parts
Function LineNumberPart(Index: Integer = 0): TSynGutterLineNumber;
Function CodeFoldPart(Index: Integer = 0): TSynGutterCodeFolding;
Function ChangesPart(Index: Integer = 0): TSynGutterChanges;
Function MarksPart(Index: Integer = 0): TSynGutterMarks;
Function SeparatorPart(Index: Integer = 0): TSynGutterSeparator;
Function LineOverviewPart(Index: Integer = 0): TSynGutterLineOverview;
published
property AutoSize;
property Color;
property CurrentLineColor;
property Cursor: TCursor read FCursor write FCursor default crDefault;
property LeftOffset;
property RightOffset;
property Visible;
property Width;
property Parts;
property MouseActions;
property OnResize;
property OnChange;
end;
{ TSynGutterSeparator }
TSynGutterSeparator = class(TSynGutterPartBase)
private
FLineOffset: Integer;
FLineOnRight: Boolean;
FLineWidth: Integer;
procedure SetLineOffset(const AValue: Integer);
procedure SetLineOnRight(const AValue: Boolean);
procedure SetLineWidth(const AValue: Integer);
protected
function PreferedWidth: Integer; override;
public
constructor Create(AOwner: TComponent); override;
procedure Paint(Canvas: TCanvas; AClip: TRect; FirstLine, LastLine: integer); override;
published
property LineWidth: Integer read FLineWidth write SetLineWidth default 1;
property LineOffset: Integer read FLineOffset write SetLineOffset default 0;
property LineOnRight: Boolean read FLineOnRight write SetLineOnRight default True;
property MarkupInfo;
property MarkupInfoCurrentLine;
end;
{ TSynEditMouseActionsGutter }
TSynEditMouseActionsGutter = class(TSynEditMouseInternalActions)
protected
procedure InitForOptions(AnOptions: TSynEditorMouseOptions); override;
end;
{ TLazSynGutterArea }
TLazSynGutterArea = class(TLazSynSurfaceWithText)
private
FGutter: TSynGutter;
function GetTextBounds: TRect;
procedure SetGutter(AValue: TSynGutter);
protected
procedure DoPaint(ACanvas: TCanvas; AClip: TRect); override;
public
procedure InvalidateLines(FirstTextLine, LastTextLine: TLineIdx; AScreenLineOffset: Integer = 0); override;
procedure Assign(Src: TLazSynSurface); override;
property Gutter: TSynGutter read FGutter write SetGutter;
property TextBounds: TRect read GetTextBounds;
end;
implementation
{ TLazSynGutterArea }
function TLazSynGutterArea.GetTextBounds: TRect;
begin
Result := TextArea.TextBounds;
end;
procedure TLazSynGutterArea.SetGutter(AValue: TSynGutter);
begin
if FGutter = AValue then Exit;
if FGutter <> nil then
FGutter.GutterArea := nil;
FGutter := AValue;
if FGutter <> nil then
FGutter.GutterArea := Self;
end;
procedure TLazSynGutterArea.DoPaint(ACanvas: TCanvas; AClip: TRect);
var
ScreenRow1, ScreenRow2: integer;
begin
// TODO TextArea top/bottom padding;
ScreenRow1 := Max((AClip.Top - Bounds.Top) div TextArea.LineHeight, 0);
ScreenRow2 := Min((AClip.Bottom-1 - Bounds.Top) div TextArea.LineHeight, TextArea.LinesInWindow + 1);
FGutter.Paint(ACanvas, Self, AClip, ScreenRow1, ScreenRow2);
end;
procedure TLazSynGutterArea.InvalidateLines(FirstTextLine,
LastTextLine: TLineIdx; AScreenLineOffset: Integer);
var
rcInval: TRect;
begin
rcInval := Bounds;
if (FirstTextLine >= 0) then
rcInval.Top := Max(TextArea.TextBounds.Top,
TextArea.TextBounds.Top
+ (DisplayView.TextToViewIndex(FirstTextLine).Top + AScreenLineOffset
- TextArea.TopLine + 1) * TextArea.LineHeight);
if (LastTextLine >= 0) then
rcInval.Bottom := Min(TextArea.TextBounds.Bottom,
TextArea.TextBounds.Top
+ (DisplayView.TextToViewIndex(LastTextLine).Bottom + AScreenLineOffset
- TextArea.TopLine + 2) * TextArea.LineHeight);
{$IFDEF VerboseSynEditInvalidate}
DebugLn(['TCustomSynEdit.InvalidateGutterLines ',DbgSName(self), ' FirstLine=',FirstTextLine, ' LastLine=',LastTextLine, ' rect=',dbgs(rcInval)]);
{$ENDIF}
if (rcInval.Top < rcInval.Bottom) and (rcInval.Left < rcInval.Right) then
InvalidateRect(Handle, @rcInval, FALSE);
end;
procedure TLazSynGutterArea.Assign(Src: TLazSynSurface);
begin
inherited Assign(Src);
FGutter := TLazSynGutterArea(Src).FGutter;
end;
{ TSynGutter }
constructor TSynGutter.Create(AOwner: TSynEditBase; ASide: TSynGutterSide;
ATextDrawer: TheTextDrawer);
begin
inherited;
if not(csLoading in AOwner.ComponentState) then
CreateDefaultGutterParts;
end;
destructor TSynGutter.Destroy;
begin
OnChange := nil;
OnResize := nil;
inherited Destroy;
end;
procedure TSynGutter.CreateDefaultGutterParts;
begin
if Side <> gsLeft then
exit;
// Todo: currently there is only one Gutter so names can be fixed
with TSynGutterMarks.Create(Parts) do
Name := 'SynGutterMarks1';
with TSynGutterLineNumber.Create(Parts) do
Name := 'SynGutterLineNumber1';
with TSynGutterChanges.Create(Parts) do
Name := 'SynGutterChanges1';
with TSynGutterSeparator.Create(Parts) do
Name := 'SynGutterSeparator1';
with TSynGutterCodeFolding.Create(Parts) do
Name := 'SynGutterCodeFolding1';
end;
function TSynGutter.CreateMouseActions: TSynEditMouseInternalActions;
begin
Result := TSynEditMouseActionsGutter.Create(self);
end;
function TSynGutter.PixelToPartIndex(X: Integer): Integer;
begin
Result := 0;
x := x - Left - LeftOffset;
while Result < PartCount-1 do begin
if Parts[Result].Visible then begin
if x >= Parts[Result].FullWidth then
x := x - Parts[Result].FullWidth
else
break;
end;
inc(Result)
end;
end;
procedure TSynGutter.DoDefaultGutterClick(Sender: TObject; X, Y, Line: integer;
mark: TSynEditMark);
begin
end;
function TSynGutter.CreatePartList: TSynGutterPartListBase;
begin
case Side of
gsLeft:
begin
Result := TSynGutterPartList.Create(SynEdit, self); //left side gutter
Result.Name := 'SynLeftGutterPartList1';
end;
gsRight:
begin
Result := TSynRightGutterPartList.Create(SynEdit, self);
Result.Name := 'SynRightGutterPartList1';
end;
end;
end;
procedure TSynGutter.DoOnGutterClick(X, Y: integer);
begin
Parts[PixelToPartIndex(X)].DoOnGutterClick(X, Y);
end;
procedure TSynGutter.Paint(Canvas: TCanvas; Surface:TLazSynGutterArea; AClip: TRect; FirstLine, LastLine: integer);
var
aCaretRow: LongInt;
i, t: integer;
rcLine, rcClip: TRect;
dc: HDC;
begin
aCaretRow := ToIdx(SynEdit.TextXYToScreenXY(SynEdit.CaretXY).Y);
if (aCaretRow < FirstLine) or (aCaretRow > LastLine) then
aCaretRow := -1;
FCaretRow := aCaretRow;
Canvas.Brush.Color := Color;
dc := Canvas.Handle;
LCLIntf.SetBkColor(dc, TColorRef(Canvas.Brush.Color));
rcClip := AClip;
t := Surface.TextBounds.Top;
// Clear all
TextDrawer.BeginDrawing(dc);
TextDrawer.SetBackColor(Color);
TextDrawer.SetForeColor(SynEdit.Font.Color);
TextDrawer.SetFrameColor(clNone);
if aCaretRow >= 0 then
rcClip.Bottom := t + aCaretRow * SynEdit.LineHeight;
with rcClip do
TextDrawer.ExtTextOut(Left, Top, ETO_OPAQUE, rcClip, nil, 0);
if aCaretRow >= 0 then begin
rcClip.top := rcClip.Bottom + SynEdit.LineHeight;
rcClip.Bottom := AClip.Bottom;
with rcClip do
TextDrawer.ExtTextOut(Left, Top, ETO_OPAQUE, rcClip, nil, 0);
rcClip.Bottom := rcClip.Top;
rcClip.top := rcClip.Top - SynEdit.LineHeight;
TextDrawer.SetBackColor(MarkupInfoCurLineMerged.Background);
with rcClip do
TextDrawer.ExtTextOut(Left, Top, ETO_OPAQUE, rcClip, nil, 0);
end;
TextDrawer.EndDrawing;
AClip.Left := Surface.Left + LeftOffset;
AClip.Top := t + FirstLine * SynEdit.LineHeight;
rcLine := AClip;
rcLine.Right := rcLine.Left;
for i := 0 to PartCount -1 do
begin
if rcLine.Right >= AClip.Right then break;
if Parts[i].Visible then
begin
rcLine.Left := rcLine.Right;
rcLine.Right := min(rcLine.Left + Parts[i].FullWidth, AClip.Right);
Parts[i].PaintAll(Canvas, rcLine, FirstLine, LastLine);
end;
end;
end;
function TSynGutter.HasCustomPopupMenu(out PopMenu: TPopupMenu): Boolean;
begin
Result := Parts[FMouseDownPart].HasCustomPopupMenu(PopMenu);
end;
procedure TSynGutter.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
FMouseDownPart := PixelToPartIndex(X);
Parts[FMouseDownPart].MouseDown(Button, Shift, X, Y);
if (Button=Controls.mbLeft) then
DoOnGutterClick(X, Y);
end;
procedure TSynGutter.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
Parts[FMouseDownPart].MouseMove(Shift, X, Y);
end;
procedure TSynGutter.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
Parts[FMouseDownPart].MouseUp(Button, Shift, X, Y);
end;
function TSynGutter.MaybeHandleMouseAction(var AnInfo: TSynEditMouseActionInfo;
HandleActionProc: TSynEditMouseActionHandler): Boolean;
var
MouseDownPart: LongInt;
begin
MouseDownPart := PixelToPartIndex(AnInfo.MouseX);
if MouseDownPart < PartCount then
Result := Parts[MouseDownPart].MaybeHandleMouseAction(AnInfo, HandleActionProc)
else
Result := False;
if not Result then
Result := inherited MaybeHandleMouseAction(AnInfo, HandleActionProc);
end;
function TSynGutter.DoHandleMouseAction(AnAction: TSynEditMouseAction;
var AnInfo: TSynEditMouseActionInfo): Boolean;
var
i: Integer;
ACommand: Word;
begin
Result := False;
for i := 0 to Parts.Count - 1 do begin
Result := Parts[i].DoHandleMouseAction(AnAction, AnInfo);
if Result then exit;;
end;
if AnAction = nil then exit;
ACommand := AnAction.Command;
if (ACommand = emcNone) then exit;
case ACommand of
emcOnMainGutterClick:
begin
if Assigned(FOnGutterClick) then begin
FOnGutterClick(Self, AnInfo.MouseX, AnInfo.MouseY, AnInfo.NewCaret.LinePos, nil);
Result := True;
end;
end;
end;
end;
procedure TSynGutter.ResetMouseActions;
var
i: integer;
begin
for i := 0 to Parts.Count - 1 do
Parts[i].ResetMouseActions;
inherited;
end;
function TSynGutter.LineNumberPart(Index: Integer = 0): TSynGutterLineNumber;
begin
Result := TSynGutterLineNumber(Parts.ByClass[TSynGutterLineNumber, Index]);
end;
function TSynGutter.CodeFoldPart(Index: Integer = 0): TSynGutterCodeFolding;
begin
Result := TSynGutterCodeFolding(Parts.ByClass[TSynGutterCodeFolding, Index]);
end;
function TSynGutter.ChangesPart(Index: Integer = 0): TSynGutterChanges;
begin
Result := TSynGutterChanges(Parts.ByClass[TSynGutterChanges, Index]);
end;
function TSynGutter.MarksPart(Index: Integer = 0): TSynGutterMarks;
begin
Result := TSynGutterMarks(Parts.ByClass[TSynGutterMarks, Index]);
end;
function TSynGutter.SeparatorPart(Index: Integer = 0): TSynGutterSeparator;
begin
Result := TSynGutterSeparator(Parts.ByClass[TSynGutterSeparator, Index]);
end;
function TSynGutter.LineOverviewPart(Index: Integer): TSynGutterLineOverview;
begin
Result := TSynGutterLineOverview(Parts.ByClass[TSynGutterLineOverview, Index]);
end;
{ TSynGutterSeparator }
procedure TSynGutterSeparator.SetLineWidth(const AValue: Integer);
begin
if FLineWidth = AValue then exit;
FLineWidth := AValue;
DoChange(Self);
end;
procedure TSynGutterSeparator.SetLineOffset(const AValue: Integer);
begin
if FLineOffset = AValue then exit;
FLineOffset := AValue;
DoChange(Self);
end;
procedure TSynGutterSeparator.SetLineOnRight(const AValue: Boolean);
begin
if FLineOnRight = AValue then exit;
FLineOnRight := AValue;
DoChange(Self);
end;
function TSynGutterSeparator.PreferedWidth: Integer;
begin
Result := 2;
end;
constructor TSynGutterSeparator.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
MarkupInfo.Background := clWhite;
MarkupInfo.Foreground := clDkGray;
FLineWidth := 1;
FLineOffset := 0;
FLineOnRight := True;
end;
procedure TSynGutterSeparator.Paint(Canvas: TCanvas; AClip: TRect; FirstLine, LastLine: integer);
begin
PaintBackground(Canvas, AClip);
if FLineOnRight then begin
AClip.Right := Min(AClip.Right, Left + LeftOffset + Width - FLineOffset);
AClip.Left := Max(AClip.Left, Left + LeftOffset + Width - FLineOffset - FLineWidth);
end else begin
AClip.Left := Max(AClip.Left, Left + LeftOffset + FLineOffset);
AClip.Right := Min(AClip.Right, Left + LeftOffset + FLineOffset + FLineWidth);
end;
if AClip.Right > AClip.Left then begin
Canvas.Brush.Color := MarkupInfo.Foreground;
Canvas.FillRect(AClip);
end;
end;
{ TSynEditMouseActionsGutter }
procedure TSynEditMouseActionsGutter.InitForOptions(AnOptions: TSynEditorMouseOptions);
var
rmc: Boolean;
begin
Clear;
rmc := (emRightMouseMovesCursor in AnOptions);
AddCommand(emcOnMainGutterClick, False, mbXLeft, ccAny, cdDown, [], []);
AddCommand(emcContextMenu, rmc, mbXRight, ccSingle, cdUp, [], []);
end;
end.