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