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