-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathColorPreview.Notifier.pas
More file actions
244 lines (223 loc) · 8.09 KB
/
Copy pathColorPreview.Notifier.pas
File metadata and controls
244 lines (223 loc) · 8.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
unit ColorPreview.Notifier;
{ Code-editor events notifier that paints a color swatch in the LEFT gutter
(the breakpoint column) for every color literal found on a visible line, and
opens the custom color picker on Shift+click, rewriting the literal in place
(with IDE undo support).
Painting is done once per repaint at the pgsEndPaint gutter stage: that stage
runs after the whole gutter is drawn, with a clip covering the entire gutter,
so the swatch can sit at the far left on every line (with or without an actual
breakpoint). Geometry comes from each line's own GutterRect, so there is no
column-to-pixel math and the swatch never drifts.
The set of recognized literals and the byte order of bare hex follow the
global switch in ColorPreview.Settings. }
interface
uses
System.Classes,
System.Types,
System.UITypes,
System.Generics.Collections,
Vcl.Controls,
Vcl.Graphics,
ToolsAPI,
ToolsAPI.Editor,
ColorPreview.Parser;
type
/// <summary>
/// Draws color swatches in the IDE code-editor gutter and opens the color
/// picker when a swatch is Shift+clicked.
/// </summary>
TColorPreviewNotifier = class(TNTACodeEditorNotifier)
private
type
TSwatch = record
Area : TRect; // client-coordinate hit area of the swatch
Token : TColorToken; // the literal this swatch represents
Line : Integer; // 1-based logical line
end;
var
FSwatches : TList<TSwatch>;
FCurrentRgbOrder : Boolean; // effective 6-digit hex order for this repaint
FDetectedFile : string; // file name behind the cached FFileIsFmx
FFileIsFmx : Boolean; // does the current unit use the FMX framework
function CurrentFileIsFmx: Boolean;
procedure DrawLineSwatches(aCanvas: TCanvas; const aLineState: INTACodeEditorLineState);
procedure ApplyColor(const aSwatch: TSwatch);
{ Handlers wired to the base TNTACodeEditorNotifier.On* events (the paint /
mouse methods are not virtual, so they are consumed as events). }
procedure HandlePaintGutter(const aRect: TRect; const aStage: TPaintGutterStage;
const aBeforeEvent: Boolean; var aAllowDefaultPainting: Boolean;
const aContext: INTACodeEditorPaintContext);
procedure HandleMouseDown(const aEditor: TWinControl; aButton: TMouseButton;
aShift: TShiftState; aX, aY: Integer);
protected
function AllowedEvents: TCodeEditorEvents; override;
function AllowedGutterStages: TPaintGutterStages; override;
public
constructor Create;
destructor Destroy; override;
end;
implementation
uses
System.SysUtils,
ColorPreview.Render,
ColorPreview.Settings,
ColorPreview.PickerForm;
const
SWATCH_LEFT = 2; // px offset from the left edge of the gutter
SWATCH_MARGIN = 2; // px vertical inset within the gutter line
SWATCH_WIDTH = 20; // swatch width (wider than tall for visibility)
MAX_SWATCH_HEIGHT = 14; // cap the height so it stays a neat marker
DETECT_SCAN_BYTES = 16384; // bytes scanned from the top to detect an FMX unit
{ Builds the swatch rect whose left edge is aLeftEdge, vertically centered
inside the given gutter line rect. }
function MakeSwatchRect(const aGutter: TRect; aLeftEdge, aWidth, aHeight: Integer): TRect;
var
LTop: Integer;
begin
LTop := aGutter.Top + (aGutter.Height - aHeight) div 2;
Result := TRect.Create(aLeftEdge, LTop, aLeftEdge + aWidth, LTop + aHeight);
end;
{ TColorPreviewNotifier }
constructor TColorPreviewNotifier.Create;
begin
inherited Create;
FSwatches := TList<TSwatch>.Create;
OnEditorPaintGutter := HandlePaintGutter;
OnEditorMouseDown := HandleMouseDown;
end;
destructor TColorPreviewNotifier.Destroy;
begin
FSwatches.Free;
inherited Destroy;
end;
function TColorPreviewNotifier.AllowedEvents: TCodeEditorEvents;
begin
Result := [cevPaintGutterEvents, cevMouseEvents];
end;
function TColorPreviewNotifier.AllowedGutterStages: TPaintGutterStages;
begin
Result := [pgsEndPaint]; // fires once, after the whole gutter is painted
end;
procedure TColorPreviewNotifier.HandlePaintGutter(const aRect: TRect;
const aStage: TPaintGutterStage; const aBeforeEvent: Boolean;
var aAllowDefaultPainting: Boolean; const aContext: INTACodeEditorPaintContext);
var
LState : INTACodeEditorState;
LLineState : INTACodeEditorLineState;
LVisLine : Integer;
begin
if aBeforeEvent or (aStage <> pgsEndPaint) or (not Assigned(aContext)) then
Exit;
LState := aContext.EditorState;
if not Assigned(LState) then
Exit;
FCurrentRgbOrder := EffectiveRgbOrder(GetByteOrderMode, CurrentFileIsFmx);
FSwatches.Clear;
for LVisLine := LState.TopLine to LState.BottomLine do
begin
LLineState := LState.LineState[LVisLine];
if Assigned(LLineState) then
DrawLineSwatches(aContext.Canvas, LLineState);
end;
end;
procedure TColorPreviewNotifier.DrawLineSwatches(aCanvas: TCanvas;
const aLineState: INTACodeEditorLineState);
var
LTokens : TColorTokens;
LSwatch : TSwatch;
LGutter : TRect;
LHeight : Integer;
LWidth : Integer;
LLeft : Integer;
LIndex : Integer;
begin
LTokens := FindColorTokens(aLineState.Text, FCurrentRgbOrder);
if Length(LTokens) = 0 then
Exit;
LGutter := aLineState.GutterRect; // leftmost area, where breakpoints appear
LHeight := LGutter.Height - SWATCH_MARGIN * 2;
if LHeight > MAX_SWATCH_HEIGHT then
LHeight := MAX_SWATCH_HEIGHT;
LWidth := SWATCH_WIDTH;
if LWidth > LGutter.Width - SWATCH_LEFT * 2 then
LWidth := LGutter.Width - SWATCH_LEFT * 2; // keep it inside the gutter column
LLeft := LGutter.Left + SWATCH_LEFT;
for LIndex := 0 to High(LTokens) do
begin
LSwatch.Area := MakeSwatchRect(LGutter, LLeft + LIndex * (LWidth + 1), LWidth, LHeight);
LSwatch.Token := LTokens[LIndex];
LSwatch.Line := aLineState.LogicalLineNum;
FSwatches.Add(LSwatch);
DrawColorPreview(aCanvas, LSwatch.Area, LTokens[LIndex].Color, LTokens[LIndex].Alpha);
end;
end;
procedure TColorPreviewNotifier.HandleMouseDown(const aEditor: TWinControl;
aButton: TMouseButton; aShift: TShiftState; aX, aY: Integer);
var
LSwatch: TSwatch;
begin
// Picker on Shift+click, so a plain click stays free for breakpoints.
if (aButton <> TMouseButton.mbLeft) or (not (ssShift in aShift)) then
Exit;
for LSwatch in FSwatches do
if LSwatch.Area.Contains(TPoint.Create(aX, aY)) then
begin
ApplyColor(LSwatch);
Break;
end;
end;
procedure TColorPreviewNotifier.ApplyColor(const aSwatch: TSwatch);
var
LView : IOTAEditView;
LPos : IOTAEditPosition;
LToken : TColorToken;
LColor : TColor;
LAlpha : Byte;
LWriteRgb : Boolean;
begin
LView := (BorlandIDEServices as IOTAEditorServices).TopView;
if not Assigned(LView) then
Exit;
LToken := aSwatch.Token;
LColor := ColorToRGB(LToken.Color);
LAlpha := LToken.Alpha;
if not EditColor(LToken, CurrentFileIsFmx, LColor, LAlpha, LWriteRgb) then
Exit;
LToken.Color := LColor;
LToken.Alpha := LAlpha;
LPos := LView.Buffer.EditPosition;
LPos.Move(aSwatch.Line, aSwatch.Token.StartCol);
LPos.Delete(aSwatch.Token.Length);
LPos.InsertText(FormatColorLiteral(LToken, LWriteRgb));
(BorlandIDEServices as INTACodeEditorServices).InvalidateTopEditor;
end;
{ Detects whether the active unit uses the FMX framework by scanning the top of
its buffer for an 'FMX.' unit reference. Cached per file name so repaints are
cheap; refreshed when the active file changes. }
function TColorPreviewNotifier.CurrentFileIsFmx: Boolean;
var
LBuffer : IOTAEditBuffer;
LReader : IOTAEditReader;
LText : AnsiString;
LName : string;
LRead : Integer;
begin
LBuffer := (BorlandIDEServices as IOTAEditorServices).TopBuffer;
if not Assigned(LBuffer) then
Exit(False);
LName := LBuffer.FileName;
if SameText(LName, FDetectedFile) then
Exit(FFileIsFmx);
FDetectedFile := LName;
FFileIsFmx := False;
LReader := LBuffer.CreateReader;
if Assigned(LReader) then
begin
SetLength(LText, DETECT_SCAN_BYTES);
LRead := LReader.GetText(0, PAnsiChar(LText), DETECT_SCAN_BYTES);
SetLength(LText, LRead);
FFileIsFmx := Pos(AnsiString('FMX.'), LText) > 0;
end;
Result := FFileIsFmx;
end;
end.