lazarus/components/synedit/synguttermarks.pp
2024-10-01 22:02:25 +02:00

473 lines
14 KiB
ObjectPascal

unit SynGutterMarks;
{$I synedit.inc}
interface
uses
Classes, SysUtils, Graphics, LCLType, LCLIntf, Controls, ImgList, FPCanvas,
SynGutterBase, SynEditMiscClasses, SynEditMarks, LazSynEditText,
SynEditMiscProcs;
type
TSynGutterMarksOption = (
sgmoDeDuplicateMarks, // don't show consecutive marks with same icon
sgmoDeDuplicateMarksKeepTwo, // show max 2 consecutive, if not limited by MaxMarksCount
sgmoDeDuplicateMarksOnOverflow // remove consecutive dups, until there are more than MaxMarksCount marks
);
TSynGutterMarksOptions = set of TSynGutterMarksOption;
{ TSynGutterMarks }
TSynGutterMarks = class(TSynGutterPartBase)
private
FColumnCount: Integer;
FMaxExtraMarksColums: Integer;
FOptions: TSynGutterMarksOptions;
FWantedColumns: integer;
FColumnWidth, FUserColumnWidth: Integer;
FDebugMarksImageIndex: Integer;
FInternalImage: TSynInternalImage;
FNoInternalImage: Boolean;
function GetColumnWidth: Integer; inline;
function GetColumnWidthFromImgList: Integer; inline;
procedure SetColumnWidth(AValue: Integer);
procedure SetColumnCount(AValue: Integer);
protected type
TSynEditMarkDrawInfo = record
Mark: TSynEditMark;
Images: TCustomImageList;
IconIdx: integer;
end;
TSynEditMarkDrawInfoArray = array of TSynEditMarkDrawInfo;
protected
FBookMarkOpt: TSynBookMarkOpt;
FTempDrawInfo: TSynEditMarkDrawInfoArray;
procedure SetAutoSize(const AValue: boolean); override;
procedure Init; override;
function PreferedWidth: Integer; override;
function LeftMarginAtCurrentPPI: Integer;
function GetImgListRes(const ACanvas: TCanvas; const AImages: TCustomImageList): TScaledImageListResolution; virtual;
function MarksToDrawInfo(AMLine: TSynEditMarkLine; var ADrawInfo: TSynEditMarkDrawInfoArray;
AMaxEntries: integer;
out aFirstCustomColumnIdx: integer; out AHasNonBookmark: boolean): integer; virtual;
// PaintMarks: True, if it has any Mark, that is *not* a bookmark
function PaintMarks(aScreenLine: Integer; Canvas : TCanvas; AClip : TRect;
var aFirstCustomColumnIdx: integer): Boolean;
Procedure PaintLine(aScreenLine: Integer; Canvas : TCanvas; AClip : TRect); virtual;
property WantedColumns: integer read FWantedColumns;
property UserColumnWidth: integer read FUserColumnWidth;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Paint(Canvas: TCanvas; AClip: TRect; FirstLine, LastLine: integer); override;
property DebugMarksImageIndex: Integer read FDebugMarksImageIndex write FDebugMarksImageIndex;
property ColumnWidth: Integer read FColumnWidth write SetColumnWidth; // zero for auto-size by bookmarks
property ColumnCount: Integer read FColumnCount write SetColumnCount;
published
// Max amount of marks show in addition to ColumnCount
property MaxExtraMarksColums: Integer read FMaxExtraMarksColums write FMaxExtraMarksColums;
property Options: TSynGutterMarksOptions read FOptions write FOptions;
property MarkupInfoCurrentLine;
end;
implementation
{ TSynGutterMarks }
constructor TSynGutterMarks.Create(AOwner: TComponent);
begin
FInternalImage := nil;
FDebugMarksImageIndex := -1;
FNoInternalImage := False;
FOptions := [sgmoDeDuplicateMarksOnOverflow];
inherited Create(AOwner);
end;
procedure TSynGutterMarks.SetColumnWidth(AValue: Integer);
begin
if FColumnWidth = AValue then
Exit;
if AValue < 0 then
AValue := 0;
FUserColumnWidth := AValue;
FColumnWidth := GetColumnWidth;
if FColumnWidth = 0 then
FColumnCount := 0
else
FColumnCount := Max((Width+1) div FColumnWidth, 1); // full columns
end;
function TSynGutterMarks.GetColumnWidth: Integer;
begin
Result := FUserColumnWidth;
if Result > 0 then
exit;
Result := GetColumnWidthFromImgList;
if Result < 0 then
Result := Width;
end;
function TSynGutterMarks.GetColumnWidthFromImgList: Integer;
begin
Result := -1;
if assigned(FBookMarkOpt) and assigned(FBookMarkOpt.BookmarkImages) and
assigned(FriendEdit) and FriendEdit.HandleAllocated and
(FColumnWidth > 0)
then
Result := GetImgListRes(FriendEdit.Canvas, FBookMarkOpt.BookmarkImages).Width;
end;
procedure TSynGutterMarks.Init;
begin
inherited Init;
FBookMarkOpt := SynEdit.BookMarkOptions;
end;
function TSynGutterMarks.PreferedWidth: Integer;
var
tmp: Integer;
begin
if FUserColumnWidth > 0 then begin
Result := FUserColumnWidth;
end
else
if FWantedColumns > 0 then begin
tmp := FColumnWidth;
FColumnWidth := 22;
Result := GetColumnWidthFromImgList;
FColumnWidth := tmp;
if Result < 0 then begin
Result := 22;
end;
end
else
Result := 22;
if FWantedColumns > 0 then
Result := Result * FWantedColumns;
Result := Result + FBookMarkOpt.LeftMargin;
end;
function TSynGutterMarks.LeftMarginAtCurrentPPI: Integer;
begin
Result := Scale96ToFont(FBookMarkOpt.LeftMargin);
end;
destructor TSynGutterMarks.Destroy;
begin
FreeAndNil(FInternalImage);
inherited Destroy;
end;
function TSynGutterMarks.GetImgListRes(const ACanvas: TCanvas; const AImages: TCustomImageList
): TScaledImageListResolution;
var
Scale: Double;
PPI: Integer;
begin
if ACanvas is TControlCanvas then
begin
Scale := TControlCanvas(ACanvas).Control.GetCanvasScaleFactor;
PPI := TControlCanvas(ACanvas).Control.Font.PixelsPerInch;
end else
begin
Scale := 1;
PPI := ACanvas.Font.PixelsPerInch;
end;
Result := AImages.ResolutionForPPI[0, PPI, Scale];
end;
function TSynGutterMarks.MarksToDrawInfo(AMLine: TSynEditMarkLine;
var ADrawInfo: TSynEditMarkDrawInfoArray; AMaxEntries: integer; out
aFirstCustomColumnIdx: integer; out AHasNonBookmark: boolean): integer;
var
i, CntUniq, CntRep2, CntKeepRep2, CntDel3: Integer;
prev_iidx, pprev_iidx: LongInt;
begin
Result := AMLine.VisibleCount;
if (FOptions * [sgmoDeDuplicateMarks, sgmoDeDuplicateMarksKeepTwo] <> []) or
((sgmoDeDuplicateMarksOnOverflow in FOptions) and (Result > ColumnCount))
then begin
CntUniq := 0;
CntRep2 := 0;
prev_iidx := low(integer);
pprev_iidx := low(integer);
for i := 0 to AMLine.Count - 1 do begin
if (not AMLine[i].Visible) or
(AMLine[i].IsBookmark and (not FBookMarkOpt.GlyphsVisible))
then
continue;
if AMLine[i].ImageIndex <> prev_iidx then
inc(CntUniq) // Uniq
else
if (i=1) or (AMLine[i].ImageIndex <> pprev_iidx) then
inc(CntRep2); // sgmoDeDuplicateMarksKeepTwo
pprev_iidx := prev_iidx;
prev_iidx := AMLine[i].ImageIndex;
end;
if (sgmoDeDuplicateMarks in FOptions) then begin
Result := Min(cntUniq, AMaxEntries);
CntKeepRep2 := 0;
end
else
if (sgmoDeDuplicateMarksKeepTwo in FOptions) then begin
CntKeepRep2 := Min(CntRep2, Max(0, AMaxEntries- cntUniq));
Result := Min(cntUniq+CntKeepRep2, AMaxEntries);
end
else begin
// only dedup for MaxExtraMarksColums
CntKeepRep2 := Min(CntRep2, Max(0, AMaxEntries - cntUniq));
Result := Min(cntUniq+CntKeepRep2, AMaxEntries);
end;
CntDel3 := AMLine.VisibleCount - Result;
end
else begin
Result := Min(AMLine.VisibleCount, AMaxEntries);
CntKeepRep2 := MaxInt; // keep duplicate if exactly 2nd
CntDel3 := 0; // del 3rd or later
end;
if Length(ADrawInfo) < Result then
SetLength(ADrawInfo, AMaxEntries); // Expand to max needed (for further runs)
aFirstCustomColumnIdx := 0;
AHasNonBookmark := False;
prev_iidx := low(integer);
pprev_iidx := low(integer);
for i := 0 to AMLine.Count - 1 do begin
if (not AMLine[i].Visible) or
(AMLine[i].IsBookmark and (not FBookMarkOpt.GlyphsVisible))
then
continue;
if (i = 0) and FBookMarkOpt.DrawBookmarksFirst and
(Result < ColumnCount) and (not AMLine[i].IsBookmark)
then begin
ADrawInfo[aFirstCustomColumnIdx].Mark := nil;
ADrawInfo[aFirstCustomColumnIdx].IconIdx := 0;
ADrawInfo[aFirstCustomColumnIdx].Images := nil;
inc(Result);
if Length(ADrawInfo) < Result then
SetLength(ADrawInfo, Result); // Expand to max needed (for further runs)
inc(aFirstCustomColumnIdx);
end;
if AMLine[i].ImageIndex = prev_iidx then begin
if (AMLine[i].ImageIndex = pprev_iidx) and (CntDel3 > 0) then begin
dec(CntDel3);
continue;
end;
if CntKeepRep2 = 0 then
Continue;
dec(CntKeepRep2);
end;
AHasNonBookmark := AHasNonBookmark or (not AMLine[i].IsBookmark); // Line has a none-bookmark glyph
ADrawInfo[aFirstCustomColumnIdx].Mark := AMLine[i];
ADrawInfo[aFirstCustomColumnIdx].IconIdx := 0; //AMLine[i].ImageIndex;
ADrawInfo[aFirstCustomColumnIdx].Images := nil; //AMLine[i].ImageList;
inc(aFirstCustomColumnIdx);
if aFirstCustomColumnIdx >= AMaxEntries then
break;
pprev_iidx := prev_iidx;
prev_iidx := AMLine[i].ImageIndex;
end;
end;
function TSynGutterMarks.PaintMarks(aScreenLine: Integer; Canvas : TCanvas;
AClip : TRect; var aFirstCustomColumnIdx: integer): Boolean;
var
LineHeight: Integer;
BkMkOptImg: TScaledImageListResolution;
BkMkOptImgDone: Boolean;
procedure DoPaintMark(const CurMarkInfo: TSynEditMarkDrawInfo; aRect: TRect);
var
img: TScaledImageListResolution;
CurMark: TSynEditMark;
idx: Integer;
begin
CurMark := CurMarkInfo.Mark;
if (CurMark <> nil) and
( CurMark.InternalImage or
( (not assigned(FBookMarkOpt.BookmarkImages)) and
(not assigned(CurMark.ImageList)) )
)
then begin
// draw internal image
if CurMark.ImageIndex in [0..9] then
begin
try
if (not Assigned(FInternalImage)) and (not FNoInternalImage) then
FInternalImage := TSynInternalImage.Create('SynEditInternalImages',10);
except
FNoInternalImage := True;
end;
if Assigned(FInternalImage) then
FInternalImage.DrawMark(Canvas, CurMark.ImageIndex, aRect.Left, aRect.Top,
LineHeight);
end;
end
else begin
// draw from ImageList
if CurMark = nil then begin
if CurMarkInfo.Images = nil then
exit;
img := GetImgListRes(Canvas, CurMarkInfo.Images);
idx := CurMarkInfo.IconIdx;
end
else
if assigned(CurMark.ImageList) then begin
img := GetImgListRes(Canvas, CurMark.ImageList);
idx := CurMark.ImageIndex;
end
else begin
if not BkMkOptImgDone then begin
BkMkOptImg := GetImgListRes(Canvas, FBookMarkOpt.BookmarkImages);
BkMkOptImgDone := True;
end;
img := BkMkOptImg;
idx := CurMark.ImageIndex;
end;
if (idx <= img.Count) and (idx >= 0) then begin
if LineHeight > img.Height then
aRect.Top := (aRect.Top + aRect.Bottom - img.Height) div 2;
img.Draw(Canvas, aRect.Left, aRect.Top, idx, True);
end;
end
end;
var
j, lm, StoredColumnWidth, lx, vcnt: Integer;
MLine: TSynEditMarkLine;
MarkRect: TRect;
iRange: TLineRange;
begin
Result := False;
aFirstCustomColumnIdx := 0;
if (FBookMarkOpt.DrawBookmarksFirst) then
aFirstCustomColumnIdx := 1;
aScreenLine := aScreenLine + ToIdx(GutterArea.TextArea.TopLine);
j := ViewedTextBuffer.DisplayView.ViewToTextIndexEx(aScreenLine, iRange);
if aScreenLine <> iRange.Top then
exit;
if (j < 0) or (j >= SynEdit.Lines.Count) then
exit;
MLine := (SynEdit.Marks as TSynEditMarkList).Line[j + 1];
if MLine = nil then
exit;
if FBookMarkOpt.DrawBookmarksFirst then
MLine.Sort(smsoBookmarkFirst, smsoPriority)
else
MLine.Sort(smsoBookMarkLast, smsoPriority);
vcnt := MarksToDrawInfo(MLine, FTempDrawInfo, ColumnCount + MaxExtraMarksColums, aFirstCustomColumnIdx, Result);
//Gutter.Paint always supplies AClip.Left = GutterPart.Left
BkMkOptImgDone := False;
LineHeight := SynEdit.LineHeight;
StoredColumnWidth := FColumnWidth;
try
lm := LeftMarginAtCurrentPPI;
lx := 0;
if vcnt > ColumnCount then begin
lx := FColumnWidth;
FColumnWidth := Min(lx, Max(2, (Width + vcnt - 1) div vcnt));
lx := (lx - FColumnWidth - 1) div 2;
end;
MarkRect := Rect(AClip.Left + lm - lx,
AClip.Top,
AClip.Left + lm - lx + FColumnWidth,
AClip.Top + LineHeight);
for j := 0 to vcnt - 1 do begin
DoPaintMark(FTempDrawInfo[j], MarkRect);
MarkRect.Left := MarkRect.Right;
MarkRect.Right := Min(MarkRect.Right + FColumnWidth, AClip.Right);
end;
finally
FColumnWidth := StoredColumnWidth;
end;
end;
procedure TSynGutterMarks.PaintLine(aScreenLine: Integer; Canvas: TCanvas; AClip: TRect);
var
aGutterOffs: Integer;
begin
aGutterOffs := 0;
PaintMarks(aScreenLine, Canvas, AClip, aGutterOffs);
end;
procedure TSynGutterMarks.Paint(Canvas : TCanvas; AClip : TRect; FirstLine, LastLine : integer);
var
i: integer;
LineHeight: Integer;
rcLine, clpr: TRect;
clp: Boolean;
begin
if not Visible then exit;
PaintBackground(Canvas, AClip);
FColumnWidth := GetColumnWidth;
if FColumnWidth = 0 then
FColumnCount := 0
else
FColumnCount := Max((Width+1) div FColumnWidth, 1); // full columns
if FBookMarkOpt.GlyphsVisible and (LastLine >= FirstLine) then
begin
clp := Canvas.Clipping;
clpr := Canvas.ClipRect;
try
rcLine := AClip;
Canvas.ClipRect := rcLine;
Canvas.Clipping := True;
LineHeight := SynEdit.LineHeight;
rcLine := AClip;
rcLine.Bottom := rcLine.Top;
for i := FirstLine to LastLine do begin
rcLine.Top := rcLine.Bottom;
rcLine.Bottom := Min(AClip.Bottom, rcLine.Top + LineHeight);
PaintLine(i, Canvas, rcLine);
end;
finally
Canvas.ClipRect := clpr;
Canvas.Clipping := clp;
end;
end;
end;
procedure TSynGutterMarks.SetColumnCount(AValue: Integer);
begin
if FWantedColumns = AValue then
exit;
FWantedColumns := AValue;
if not AutoSize then
AutoSize := True
else
DoAutoSize;
end;
procedure TSynGutterMarks.SetAutoSize(const AValue: boolean);
begin
inherited SetAutoSize(AValue);
if not AValue then
FWantedColumns := 0;
end;
end.