1 // Scintilla source code edit control
2 /** @file CallTip.cxx
3 ** Code for displaying call tips.
4 **/
5 // Copyright 1998-2001 by Neil Hodgson <neilh@scintilla.org>
6 // The License.txt file describes the conditions under which this software may be distributed.
7
8 #include <cstddef>
9 #include <cstdlib>
10 #include <cassert>
11 #include <cstring>
12 #include <cstdio>
13 #include <cmath>
14
15 #include <stdexcept>
16 #include <string>
17 #include <vector>
18 #include <algorithm>
19 #include <memory>
20
21 #include "Platform.h"
22
23 #include "Scintilla.h"
24
25 #include "Position.h"
26 #include "IntegerRectangle.h"
27 #include "CallTip.h"
28
29 using namespace Scintilla;
30
Length() const31 size_t Chunk::Length() const noexcept {
32 return end - start;
33 }
34
CallTip()35 CallTip::CallTip() noexcept {
36 wCallTip = {};
37 inCallTipMode = false;
38 posStartCallTip = 0;
39 rectUp = PRectangle(0,0,0,0);
40 rectDown = PRectangle(0,0,0,0);
41 lineHeight = 1;
42 offsetMain = 0;
43 tabSize = 0;
44 above = false;
45 useStyleCallTip = false; // for backwards compatibility
46
47 insetX = 5;
48 widthArrow = 14;
49 borderHeight = 2; // Extra line for border and an empty line at top and bottom.
50 verticalOffset = 1;
51
52 #ifdef __APPLE__
53 // proper apple colours for the default
54 colourBG = ColourDesired(0xff, 0xff, 0xc6);
55 colourUnSel = ColourDesired(0, 0, 0);
56 #else
57 colourBG = ColourDesired(0xff, 0xff, 0xff);
58 colourUnSel = ColourDesired(0x80, 0x80, 0x80);
59 #endif
60 colourSel = ColourDesired(0, 0, 0x80);
61 colourShade = ColourDesired(0, 0, 0);
62 colourLight = ColourDesired(0xc0, 0xc0, 0xc0);
63 codePage = 0;
64 clickPlace = 0;
65 }
66
~CallTip()67 CallTip::~CallTip() {
68 font.Release();
69 wCallTip.Destroy();
70 }
71
72 // We ignore tabs unless a tab width has been set.
IsTabCharacter(char ch) const73 bool CallTip::IsTabCharacter(char ch) const noexcept {
74 return (tabSize > 0) && (ch == '\t');
75 }
76
NextTabPos(int x) const77 int CallTip::NextTabPos(int x) const noexcept {
78 if (tabSize > 0) { // paranoia... not called unless this is true
79 x -= insetX; // position relative to text
80 x = (x + tabSize) / tabSize; // tab "number"
81 return tabSize*x + insetX; // position of next tab
82 } else {
83 return x + 1; // arbitrary
84 }
85 }
86
87 namespace {
88
89 // Although this test includes 0, we should never see a \0 character.
IsArrowCharacter(char ch)90 constexpr bool IsArrowCharacter(char ch) noexcept {
91 return (ch == 0) || (ch == '\001') || (ch == '\002');
92 }
93
DrawArrow(Scintilla::Surface * surface,const PRectangle & rc,bool upArrow,ColourDesired colourBG,ColourDesired colourUnSel)94 void DrawArrow(Scintilla::Surface *surface, const PRectangle &rc, bool upArrow, ColourDesired colourBG, ColourDesired colourUnSel) {
95 surface->FillRectangle(rc, colourBG);
96 const int width = static_cast<int>(rc.Width());
97 const int halfWidth = width / 2 - 3;
98 const int quarterWidth = halfWidth / 2;
99 const int centreX = static_cast<int>(rc.left) + width / 2 - 1;
100 const int centreY = static_cast<int>(rc.top + rc.bottom) / 2;
101 const PRectangle rcClientInner(rc.left + 1, rc.top + 1, rc.right - 2, rc.bottom - 1);
102 surface->FillRectangle(rcClientInner, colourUnSel);
103
104 if (upArrow) { // Up arrow
105 Point pts[] = {
106 Point::FromInts(centreX - halfWidth, centreY + quarterWidth),
107 Point::FromInts(centreX + halfWidth, centreY + quarterWidth),
108 Point::FromInts(centreX, centreY - halfWidth + quarterWidth),
109 };
110 surface->Polygon(pts, Sci::size(pts), colourBG, colourBG);
111 } else { // Down arrow
112 Point pts[] = {
113 Point::FromInts(centreX - halfWidth, centreY - quarterWidth),
114 Point::FromInts(centreX + halfWidth, centreY - quarterWidth),
115 Point::FromInts(centreX, centreY + halfWidth - quarterWidth),
116 };
117 surface->Polygon(pts, Sci::size(pts), colourBG, colourBG);
118 }
119 }
120
121 }
122
123 // Draw a section of the call tip that does not include \n in one colour.
124 // The text may include tabs or arrow characters.
DrawChunk(Surface * surface,int x,const char * s,size_t len,int ytext,PRectangle rcClient,bool asHighlight,bool draw)125 int CallTip::DrawChunk(Surface *surface, int x, const char *s, size_t len,
126 int ytext, PRectangle rcClient, bool asHighlight, bool draw) {
127 if (len == 0) {
128 return x;
129 }
130
131 // Divide the text into sections that are all text, or that are
132 // single arrows or single tab characters (if tabSize > 0).
133 // Start with single element 0 to simplify append checks.
134 std::vector<size_t> ends(1);
135 for (size_t i=0; i<len; i++) {
136 if (IsArrowCharacter(s[i]) || IsTabCharacter(s[i])) {
137 if (ends.back() != i)
138 ends.push_back(i);
139 ends.push_back(i+1);
140 }
141 }
142 if (ends.back() != len)
143 ends.push_back(len);
144 ends.erase(ends.begin()); // Remove initial 0.
145
146 size_t startSeg = 0;
147 for (const size_t endSeg : ends) {
148 assert(endSeg > 0);
149 int xEnd;
150 if (IsArrowCharacter(s[startSeg])) {
151 xEnd = x + widthArrow;
152 const bool upArrow = s[startSeg] == '\001';
153 rcClient.left = static_cast<XYPOSITION>(x);
154 rcClient.right = static_cast<XYPOSITION>(xEnd);
155 if (draw) {
156 DrawArrow(surface, rcClient, upArrow, colourBG, colourUnSel);
157 }
158 offsetMain = xEnd;
159 if (upArrow) {
160 rectUp = rcClient;
161 } else {
162 rectDown = rcClient;
163 }
164 } else if (IsTabCharacter(s[startSeg])) {
165 xEnd = NextTabPos(x);
166 } else {
167 const char *segText = s + startSeg;
168 xEnd = x + static_cast<int>(Sci::lround(surface->WidthText(font, segText, endSeg - startSeg)));
169 if (draw) {
170 rcClient.left = static_cast<XYPOSITION>(x);
171 rcClient.right = static_cast<XYPOSITION>(xEnd);
172 surface->DrawTextTransparent(rcClient, font, static_cast<XYPOSITION>(ytext),
173 segText, endSeg - startSeg,
174 asHighlight ? colourSel : colourUnSel);
175 }
176 }
177 x = xEnd;
178 startSeg = endSeg;
179 }
180 return x;
181 }
182
PaintContents(Surface * surfaceWindow,bool draw)183 int CallTip::PaintContents(Surface *surfaceWindow, bool draw) {
184 const PRectangle rcClientPos = wCallTip.GetClientPosition();
185 const PRectangle rcClientSize(0.0f, 0.0f, rcClientPos.right - rcClientPos.left,
186 rcClientPos.bottom - rcClientPos.top);
187 PRectangle rcClient(1.0f, 1.0f, rcClientSize.right - 1, rcClientSize.bottom - 1);
188
189 // To make a nice small call tip window, it is only sized to fit most normal characters without accents
190 const int ascent = static_cast<int>(Sci::round(surfaceWindow->Ascent(font) - surfaceWindow->InternalLeading(font)));
191
192 // For each line...
193 // Draw the definition in three parts: before highlight, highlighted, after highlight
194 int ytext = static_cast<int>(rcClient.top) + ascent + 1;
195 rcClient.bottom = ytext + surfaceWindow->Descent(font) + 1;
196 const char *remaining = val.c_str();
197 int maxWidth = 0;
198 size_t lineStart = 0;
199 while (*remaining) {
200 const char *chunkVal = remaining;
201 const char *chunkEnd = strchr(remaining, '\n');
202 if (!chunkEnd) {
203 chunkEnd = chunkVal + strlen(chunkVal);
204 }
205 const size_t chunkLength = static_cast<size_t>(chunkEnd - chunkVal);
206 remaining += chunkLength;
207 if (*remaining) {
208 remaining++; // Skip \n
209 }
210
211 const Chunk chunkLine(lineStart, lineStart + chunkLength);
212 Chunk chunkHighlight(
213 Sci::clamp(highlight.start, chunkLine.start, chunkLine.end),
214 Sci::clamp(highlight.end, chunkLine.start, chunkLine.end)
215 );
216 chunkHighlight.start -= lineStart;
217 chunkHighlight.end -= lineStart;
218
219 rcClient.top = static_cast<XYPOSITION>(ytext - ascent - 1);
220
221 int x = insetX; // start each line at this inset
222
223 x = DrawChunk(surfaceWindow, x, chunkVal, chunkHighlight.start,
224 ytext, rcClient, false, draw);
225 x = DrawChunk(surfaceWindow, x, chunkVal + chunkHighlight.start, chunkHighlight.Length(),
226 ytext, rcClient, true, draw);
227 x = DrawChunk(surfaceWindow, x, chunkVal + chunkHighlight.end, chunkLength - chunkHighlight.end,
228 ytext, rcClient, false, draw);
229
230 ytext += lineHeight;
231 rcClient.bottom += lineHeight;
232 maxWidth = std::max(maxWidth, x);
233 lineStart += chunkLength + 1;
234 }
235 return maxWidth;
236 }
237
PaintCT(Surface * surfaceWindow)238 void CallTip::PaintCT(Surface *surfaceWindow) {
239 if (val.empty())
240 return;
241 const PRectangle rcClientPos = wCallTip.GetClientPosition();
242 const PRectangle rcClientSize(0.0f, 0.0f, rcClientPos.right - rcClientPos.left,
243 rcClientPos.bottom - rcClientPos.top);
244 const PRectangle rcClient(1.0f, 1.0f, rcClientSize.right - 1, rcClientSize.bottom - 1);
245
246 surfaceWindow->FillRectangle(rcClient, colourBG);
247
248 offsetMain = insetX; // initial alignment assuming no arrows
249 PaintContents(surfaceWindow, true);
250
251 #ifndef __APPLE__
252 // OSX doesn't put borders on "help tags"
253 // Draw a raised border around the edges of the window
254 const IntegerRectangle ircClientSize(rcClientSize);
255 surfaceWindow->MoveTo(0, ircClientSize.bottom - 1);
256 surfaceWindow->PenColour(colourShade);
257 surfaceWindow->LineTo(ircClientSize.right - 1, ircClientSize.bottom - 1);
258 surfaceWindow->LineTo(ircClientSize.right - 1, 0);
259 surfaceWindow->PenColour(colourLight);
260 surfaceWindow->LineTo(0, 0);
261 surfaceWindow->LineTo(0, ircClientSize.bottom - 1);
262 #endif
263 }
264
MouseClick(Point pt)265 void CallTip::MouseClick(Point pt) noexcept {
266 clickPlace = 0;
267 if (rectUp.Contains(pt))
268 clickPlace = 1;
269 if (rectDown.Contains(pt))
270 clickPlace = 2;
271 }
272
CallTipStart(Sci::Position pos,Point pt,int textHeight,const char * defn,const char * faceName,int size,int codePage_,int characterSet,int technology,const Window & wParent)273 PRectangle CallTip::CallTipStart(Sci::Position pos, Point pt, int textHeight, const char *defn,
274 const char *faceName, int size,
275 int codePage_, int characterSet,
276 int technology, const Window &wParent) {
277 clickPlace = 0;
278 val = defn;
279 codePage = codePage_;
280 std::unique_ptr<Surface> surfaceMeasure(Surface::Allocate(technology));
281 surfaceMeasure->Init(wParent.GetID());
282 surfaceMeasure->SetUnicodeMode(SC_CP_UTF8 == codePage);
283 surfaceMeasure->SetDBCSMode(codePage);
284 highlight = Chunk();
285 inCallTipMode = true;
286 posStartCallTip = pos;
287 const XYPOSITION deviceHeight = static_cast<XYPOSITION>(surfaceMeasure->DeviceHeightFont(size));
288 const FontParameters fp(faceName, deviceHeight / SC_FONT_SIZE_MULTIPLIER, SC_WEIGHT_NORMAL, false, 0, technology, characterSet);
289 font.Create(fp);
290 // Look for multiple lines in the text
291 // Only support \n here - simply means container must avoid \r!
292 const int numLines = 1 + static_cast<int>(std::count(val.begin(), val.end(), '\n'));
293 rectUp = PRectangle(0,0,0,0);
294 rectDown = PRectangle(0,0,0,0);
295 offsetMain = insetX; // changed to right edge of any arrows
296 const int width = PaintContents(surfaceMeasure.get(), false) + insetX;
297 lineHeight = static_cast<int>(Sci::lround(surfaceMeasure->Height(font)));
298
299 // The returned
300 // rectangle is aligned to the right edge of the last arrow encountered in
301 // the tip text, else to the tip text left edge.
302 const int height = lineHeight * numLines - static_cast<int>(surfaceMeasure->InternalLeading(font)) + borderHeight * 2;
303 if (above) {
304 return PRectangle(pt.x - offsetMain, pt.y - verticalOffset - height, pt.x + width - offsetMain, pt.y - verticalOffset);
305 } else {
306 return PRectangle(pt.x - offsetMain, pt.y + verticalOffset + textHeight, pt.x + width - offsetMain, pt.y + verticalOffset + textHeight + height);
307 }
308 }
309
CallTipCancel()310 void CallTip::CallTipCancel() {
311 inCallTipMode = false;
312 if (wCallTip.Created()) {
313 wCallTip.Destroy();
314 }
315 }
316
SetHighlight(size_t start,size_t end)317 void CallTip::SetHighlight(size_t start, size_t end) {
318 // Avoid flashing by checking something has really changed
319 if ((start != highlight.start) || (end != highlight.end)) {
320 highlight.start = start;
321 highlight.end = (end > start) ? end : start;
322 if (wCallTip.Created()) {
323 wCallTip.InvalidateAll();
324 }
325 }
326 }
327
328 // Set the tab size (sizes > 0 enable the use of tabs). This also enables the
329 // use of the STYLE_CALLTIP.
SetTabSize(int tabSz)330 void CallTip::SetTabSize(int tabSz) noexcept {
331 tabSize = tabSz;
332 useStyleCallTip = true;
333 }
334
335 // Set the calltip position, below the text by default or if above is false
336 // else above the text.
SetPosition(bool aboveText)337 void CallTip::SetPosition(bool aboveText) noexcept {
338 above = aboveText;
339 }
340
UseStyleCallTip() const341 bool CallTip::UseStyleCallTip() const noexcept {
342 return useStyleCallTip;
343 }
344
345 // It might be better to have two access functions for this and to use
346 // them for all settings of colours.
SetForeBack(const ColourDesired & fore,const ColourDesired & back)347 void CallTip::SetForeBack(const ColourDesired &fore, const ColourDesired &back) noexcept {
348 colourBG = back;
349 colourUnSel = fore;
350 }
351