1 /*******************************************************************************
2  * Copyright (c) 2006, 2018 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *     Tom Eicher (Avaloq Evolution AG) - block selection mode
14  *******************************************************************************/
15 package org.eclipse.jface.text;
16 
17 import org.eclipse.swt.custom.StyledText;
18 import org.eclipse.swt.graphics.GC;
19 import org.eclipse.swt.graphics.Point;
20 import org.eclipse.swt.graphics.Rectangle;
21 import org.eclipse.swt.widgets.Control;
22 import org.eclipse.swt.widgets.Display;
23 
24 import org.eclipse.jface.internal.text.SelectionProcessor;
25 
26 import org.eclipse.jface.text.source.ILineRange;
27 import org.eclipse.jface.text.source.LineRange;
28 
29 /**
30  * A collection of JFace Text functions.
31  * <p>
32  * This class is neither intended to be instantiated nor subclassed.
33  * </p>
34  *
35  * @since 3.3
36  * @noinstantiate This class is not intended to be instantiated by clients.
37  */
38 public final class JFaceTextUtil {
39 
JFaceTextUtil()40 	private JFaceTextUtil() {
41 		// Do not instantiate
42 	}
43 
44 	/**
45 	 * Computes the full line height for the text line corresponding to the given widget line,
46 	 * considering the possible line wrapping.
47 	 *
48 	 * @param styledText the widget
49 	 * @param widgetLine the widget line
50 	 * @return the full real height of the corresponding line of text (which might wrap to multiple
51 	 *         widget lines) in the widget
52 	 * @since 3.11
53 	 */
computeLineHeight(StyledText styledText, int widgetLine)54 	public static int computeLineHeight(StyledText styledText, int widgetLine) {
55 		boolean isWrapActive= styledText.getWordWrap();
56 		int lineHeight;
57 		int offset= styledText.getOffsetAtLine(widgetLine);
58 		if (!isWrapActive) {
59 			lineHeight= styledText.getLineHeight(offset);
60 		} else {
61 			int offsetEnd= offset + styledText.getLine(widgetLine).length();
62 			if (offsetEnd == styledText.getCharCount()) {
63 				lineHeight= styledText.getLineHeight(offset);
64 			} else {
65 				Rectangle textBounds= styledText.getTextBounds(offset, offsetEnd);
66 				lineHeight= textBounds.height;
67 			}
68 		}
69 		return lineHeight;
70 	}
71 
72 	/**
73 	 * Computes the line height for the given line range.
74 	 *
75 	 * @param textWidget the <code>StyledText</code> widget
76 	 * @param startLine the start line
77 	 * @param endLine the end line (exclusive)
78 	 * @param lineCount the line count used by the old API
79 	 * @return the height of all lines starting with <code>startLine</code> and ending above <code>endLime</code>
80 	 */
computeLineHeight(StyledText textWidget, int startLine, int endLine, int lineCount)81 	public static int computeLineHeight(StyledText textWidget, int startLine, int endLine, int lineCount) {
82 		return getLinePixel(textWidget, endLine) - getLinePixel(textWidget, startLine);
83 	}
84 
85 	/**
86 	 * Returns the last fully visible line of the widget. The exact semantics of "last fully visible
87 	 * line" are:
88 	 * <ul>
89 	 * <li>the last line of which the last pixel is visible, if any
90 	 * <li>otherwise, the only line that is partially visible
91 	 * </ul>
92 	 *
93 	 * @param widget the widget
94 	 * @return the last fully visible line
95 	 */
getBottomIndex(StyledText widget)96 	public static int getBottomIndex(StyledText widget) {
97 		int lastPixel= computeLastVisiblePixel(widget);
98 
99 		// bottom is in [0 .. lineCount - 1]
100 		int bottom= widget.getLineIndex(lastPixel);
101 
102 		// bottom is the first line - no more checking
103 		if (bottom == 0)
104 			return bottom;
105 
106 		int pixel= widget.getLinePixel(bottom);
107 		// bottom starts on or before the client area start - bottom is the only visible line
108 		if (pixel <= 0)
109 			return bottom;
110 
111 		int height= computeLineHeight(widget, bottom);
112 
113 		// bottom is not showing entirely - use the previous line
114 		if (pixel + height - 1 > lastPixel)
115 			return bottom - 1;
116 
117 		// bottom is fully visible and its last line is exactly the last pixel
118 		return bottom;
119 	}
120 
121 	/**
122 	 * Returns the index of the first (possibly only partially) visible line of the widget
123 	 *
124 	 * @param widget the widget
125 	 * @return the index of the first line of which a pixel is visible
126 	 */
getPartialTopIndex(StyledText widget)127 	public static int getPartialTopIndex(StyledText widget) {
128 		// see StyledText#getPartialTopIndex()
129 		int top= widget.getTopIndex();
130 		int pixels= widget.getLinePixel(top);
131 
132 		if (pixels > 0)
133 			top--;
134 
135 		return top;
136 	}
137 
138 	/**
139 	 * Returns the index of the last (possibly only partially) visible line of the widget
140 	 *
141 	 * @param widget the text widget
142 	 * @return the index of the last line of which a pixel is visible
143 	 */
getPartialBottomIndex(StyledText widget)144 	public static int getPartialBottomIndex(StyledText widget) {
145 		// @see StyledText#getPartialBottomIndex()
146 		int lastPixel= computeLastVisiblePixel(widget);
147 		int bottom= widget.getLineIndex(lastPixel);
148 		return bottom;
149 	}
150 
151 	/**
152 	 * Returns the last visible pixel in the widget's client area.
153 	 *
154 	 * @param widget the widget
155 	 * @return the last visible pixel in the widget's client area
156 	 */
computeLastVisiblePixel(StyledText widget)157 	private static int computeLastVisiblePixel(StyledText widget) {
158 		int caHeight= widget.getClientArea().height;
159 		int lastPixel= caHeight - 1;
160 		// XXX: what if there is a margin? can't take trim as this includes the scrollbars which are not part of the client area
161 //		if ((textWidget.getStyle() & SWT.BORDER) != 0)
162 //			lastPixel -= 4;
163 		return lastPixel;
164 	}
165 
166 	/**
167 	 * Returns the line index of the first visible model line in the viewer. The line may be only
168 	 * partially visible.
169 	 *
170 	 * @param viewer the text viewer
171 	 * @return the first line of which a pixel is visible, or -1 for no line
172 	 */
getPartialTopIndex(ITextViewer viewer)173 	public static int getPartialTopIndex(ITextViewer viewer) {
174 		StyledText widget= viewer.getTextWidget();
175 		int widgetTop= getPartialTopIndex(widget);
176 		return widgetLine2ModelLine(viewer, widgetTop);
177 	}
178 
179 	/**
180 	 * Returns the last, possibly partially, visible line in the view port.
181 	 *
182 	 * @param viewer the text viewer
183 	 * @return the last, possibly partially, visible line in the view port
184 	 */
getPartialBottomIndex(ITextViewer viewer)185 	public static int getPartialBottomIndex(ITextViewer viewer) {
186 		StyledText textWidget= viewer.getTextWidget();
187 		int widgetBottom= getPartialBottomIndex(textWidget);
188 		return widgetLine2ModelLine(viewer, widgetBottom);
189 	}
190 
191 	/**
192 	 * Returns the range of lines that is visible in the viewer, including any partially visible
193 	 * lines.
194 	 *
195 	 * @param viewer the viewer
196 	 * @return the range of lines that is visible in the viewer, <code>null</code> if no lines are
197 	 *         visible
198 	 */
getVisibleModelLines(ITextViewer viewer)199 	public static ILineRange getVisibleModelLines(ITextViewer viewer) {
200 		int top= getPartialTopIndex(viewer);
201 		int bottom= getPartialBottomIndex(viewer);
202 		if (top == -1 || bottom == -1)
203 			return null;
204 		return new LineRange(top, bottom - top + 1);
205 	}
206 
207 	/**
208 	 * Converts a widget line into a model (i.e. {@link IDocument}) line using the
209 	 * {@link ITextViewerExtension5} if available, otherwise by adapting the widget line to the
210 	 * viewer's {@link ITextViewer#getVisibleRegion() visible region}.
211 	 *
212 	 * @param viewer the viewer
213 	 * @param widgetLine the widget line to convert.
214 	 * @return the model line corresponding to <code>widgetLine</code> or -1 to signal that there
215 	 *         is no corresponding model line
216 	 */
widgetLine2ModelLine(ITextViewer viewer, int widgetLine)217 	public static int widgetLine2ModelLine(ITextViewer viewer, int widgetLine) {
218 		int modelLine;
219 		if (viewer instanceof ITextViewerExtension5) {
220 			ITextViewerExtension5 extension= (ITextViewerExtension5) viewer;
221 			modelLine= extension.widgetLine2ModelLine(widgetLine);
222 		} else {
223 			try {
224 				IRegion r= viewer.getVisibleRegion();
225 				IDocument d= viewer.getDocument();
226 				if (d == null)
227 					return -1;
228 				modelLine= widgetLine + d.getLineOfOffset(r.getOffset());
229 			} catch (BadLocationException x) {
230 				modelLine= widgetLine;
231 			}
232 		}
233 		return modelLine;
234 	}
235 
236 	/**
237 	 * Converts a model (i.e. {@link IDocument}) line into a widget line using the
238 	 * {@link ITextViewerExtension5} if available, otherwise by adapting the model line to the
239 	 * viewer's {@link ITextViewer#getVisibleRegion() visible region}.
240 	 *
241 	 * @param viewer the viewer
242 	 * @param modelLine the model line to convert.
243 	 * @return the widget line corresponding to <code>modelLine</code> or -1 to signal that there
244 	 *         is no corresponding widget line
245 	 */
modelLineToWidgetLine(ITextViewer viewer, final int modelLine)246 	public static int modelLineToWidgetLine(ITextViewer viewer, final int modelLine) {
247 		int widgetLine;
248 		if (viewer instanceof ITextViewerExtension5) {
249 			ITextViewerExtension5 extension= (ITextViewerExtension5) viewer;
250 			widgetLine= extension.modelLine2WidgetLine(modelLine);
251 		} else {
252 			IRegion region= viewer.getVisibleRegion();
253 			IDocument document= viewer.getDocument();
254 			if (document == null)
255 				return -1;
256 			try {
257 				int visibleStartLine= document.getLineOfOffset(region.getOffset());
258 				int visibleEndLine= document.getLineOfOffset(region.getOffset() + region.getLength());
259 				if (modelLine < visibleStartLine || modelLine > visibleEndLine)
260 					widgetLine= -1;
261 				else
262 				widgetLine= modelLine - visibleStartLine;
263 			} catch (BadLocationException x) {
264 				// ignore and return -1
265 				widgetLine= -1;
266 			}
267 		}
268 		return widgetLine;
269 	}
270 
271 
272 	/**
273 	 * Returns the number of hidden pixels of the first partially visible line. If there is no
274 	 * partially visible line, zero is returned.
275 	 *
276 	 * @param textWidget the widget
277 	 * @return the number of hidden pixels of the first partial line, always &gt;= 0
278 	 */
getHiddenTopLinePixels(StyledText textWidget)279 	public static int getHiddenTopLinePixels(StyledText textWidget) {
280 		int top= getPartialTopIndex(textWidget);
281 		return -textWidget.getLinePixel(top);
282 	}
283 
284 	/*
285 	 * @see StyledText#getLinePixel(int)
286 	 */
getLinePixel(StyledText textWidget, int line)287 	public static int getLinePixel(StyledText textWidget, int line) {
288 		return textWidget.getLinePixel(line);
289 	}
290 
291 	/*
292 	 * @see StyledText#getLineIndex(int)
293 	 */
getLineIndex(StyledText textWidget, int y)294 	public static int getLineIndex(StyledText textWidget, int y) {
295 		int lineIndex= textWidget.getLineIndex(y);
296 		return lineIndex;
297 	}
298 
299 	/**
300 	 * Returns <code>true</code> if the widget displays the entire contents, i.e. it cannot
301 	 * be vertically scrolled.
302 	 *
303 	 * @param widget the widget
304 	 * @return <code>true</code> if the widget displays the entire contents, i.e. it cannot
305 	 *         be vertically scrolled, <code>false</code> otherwise
306 	 */
isShowingEntireContents(StyledText widget)307 	public static boolean isShowingEntireContents(StyledText widget) {
308 		if (widget.getTopPixel() != 0) {
309 			// more efficient shortcut
310 			return false;
311 		}
312 
313 		int lastVisiblePixel= computeLastVisiblePixel(widget);
314 		int bottom= widget.getLineIndex(lastVisiblePixel);
315 		if (bottom + 1 < widget.getLineCount()) {
316 			// There's definitely more lines below
317 			return false;
318 		}
319 
320 		// Check whether the last line is fully visible
321 		int bottomLastPixel= getLinePixel(widget, bottom + 1) - 1;
322 		return bottomLastPixel <= lastVisiblePixel;
323 	}
324 
325 	/**
326 	 * Determines the graphical area covered by the given text region in
327 	 * the given viewer.
328 	 *
329 	 * @param region the region whose graphical extend must be computed
330 	 * @param textViewer the text viewer containing the region
331 	 * @return the graphical extend of the given region in the given viewer
332 	 *
333 	 * @since 3.4
334 	 */
computeArea(IRegion region, ITextViewer textViewer)335 	public static Rectangle computeArea(IRegion region, ITextViewer textViewer) {
336 		int start= 0;
337 		int end= 0;
338 		IRegion widgetRegion= modelRange2WidgetRange(region, textViewer);
339 		if (widgetRegion != null) {
340 			start= widgetRegion.getOffset();
341 			end= start + widgetRegion.getLength();
342 		}
343 
344 		StyledText styledText= textViewer.getTextWidget();
345 		Rectangle bounds;
346 		if (end > 0 && start < end)
347 			bounds= styledText.getTextBounds(start, end - 1);
348 		else {
349 			Point loc= styledText.getLocationAtOffset(start);
350 			bounds= new Rectangle(loc.x, loc.y, getAverageCharWidth(textViewer.getTextWidget()),
351 					computeLineHeight(styledText, styledText.getLineAtOffset(start)));
352 		}
353 
354 		return new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);
355 	}
356 
357 	/**
358 	 * Translates a given region of the text viewer's document into
359 	 * the corresponding region of the viewer's widget.
360 	 *
361 	 * @param region the document region
362 	 * @param textViewer the viewer containing the region
363 	 * @return the corresponding widget region
364 	 *
365 	 * @since 3.4
366 	 */
modelRange2WidgetRange(IRegion region, ITextViewer textViewer)367 	private static IRegion modelRange2WidgetRange(IRegion region, ITextViewer textViewer) {
368 		if (textViewer instanceof ITextViewerExtension5) {
369 			ITextViewerExtension5 extension= (ITextViewerExtension5) textViewer;
370 			return extension.modelRange2WidgetRange(region);
371 		}
372 
373 		IRegion visibleRegion= textViewer.getVisibleRegion();
374 		int start= region.getOffset() - visibleRegion.getOffset();
375 		int end= start + region.getLength();
376 		if (end > visibleRegion.getLength())
377 			end= visibleRegion.getLength();
378 
379 		return new Region(start, end - start);
380 	}
381 
382 	/**
383 	 * Returns the average character width of the given control's font.
384 	 *
385 	 * @param control the control to calculate the average char width for
386 	 * @return the average character width of the controls font
387 	 *
388 	 * @since 3.4
389 	 */
getAverageCharWidth(Control control)390 	public static int getAverageCharWidth(Control control) {
391 		GC gc= new GC(control);
392 		gc.setFont(control.getFont());
393 		double increment= gc.getFontMetrics().getAverageCharacterWidth();
394 		gc.dispose();
395 		return (int) increment;
396 	}
397 
398 	/**
399 	 * Returns <code>true</code> if the text covered by <code>selection</code> does not contain any
400 	 * characters in the given viewer. Note the difference to {@link ITextSelection#isEmpty()},
401 	 * which returns <code>true</code> only for invalid selections.
402 	 *
403 	 * @param viewer the viewer
404 	 * @param selection the selection
405 	 * @return <code>true</code> if <code>selection</code> does not contain any text,
406 	 *         <code>false</code> otherwise
407 	 * @throws BadLocationException if accessing the document failed
408 	 * @since 3.5
409 	 */
isEmpty(ITextViewer viewer, ITextSelection selection)410 	public static boolean isEmpty(ITextViewer viewer, ITextSelection selection) throws BadLocationException {
411 		return new SelectionProcessor(viewer).isEmpty(selection);
412 	}
413 
414 	/**
415 	 * Returns the text regions covered by the given selection in the given viewer.
416 	 *
417 	 * @param viewer the viewer
418 	 * @param selection the selection
419 	 * @return the text regions corresponding to <code>selection</code>
420 	 * @throws BadLocationException if accessing the document failed
421 	 * @since 3.5
422 	 */
getCoveredRanges(ITextViewer viewer, ITextSelection selection)423 	public static IRegion[] getCoveredRanges(ITextViewer viewer, ITextSelection selection) throws BadLocationException {
424 		return new SelectionProcessor(viewer).getRanges(selection);
425 	}
426 
427 	/**
428 	 * Returns the offset in the given viewer that corresponds to the current cursor location.
429 	 *
430 	 * @param viewer the viewer
431 	 * @return the offset for the current cursor location or -1 if not available
432 	 * @since 3.5
433 	 */
getOffsetForCursorLocation(ITextViewer viewer)434 	public static int getOffsetForCursorLocation(ITextViewer viewer) {
435 		try {
436 			StyledText text= viewer.getTextWidget();
437 			if (text == null || text.isDisposed()) {
438 				return -1;
439 			}
440 
441 			Display display= text.getDisplay();
442 			Point absolutePosition= display.getCursorLocation();
443 			Point relativePosition= text.toControl(absolutePosition);
444 
445 			int widgetOffset= text.getOffsetAtPoint(relativePosition);
446 			if (widgetOffset == -1) {
447 				return -1;
448 			}
449 			Point p= text.getLocationAtOffset(widgetOffset);
450 			if (p.x > relativePosition.x) {
451 				widgetOffset--;
452 			}
453 
454 			if (viewer instanceof ITextViewerExtension5) {
455 				ITextViewerExtension5 extension= (ITextViewerExtension5)viewer;
456 				return extension.widgetOffset2ModelOffset(widgetOffset);
457 			}
458 
459 			return widgetOffset + viewer.getVisibleRegion().getOffset();
460 		} catch (IllegalArgumentException e) {
461 			return -1;
462 		}
463 	}
464 }
465