1 /*******************************************************************************
2  * Copyright (c) 2000, 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  *     Ralf M Petter<ralf.petter@gmail.com> - Bug 259846
14  *     Karsten Thoms<karsten.thoms@itemis.de> - Bug 521493
15  *******************************************************************************/
16 package org.eclipse.ui.internal.forms.widgets;
17 
18 import java.io.ByteArrayInputStream;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.nio.charset.StandardCharsets;
22 import java.util.Vector;
23 
24 import javax.xml.parsers.DocumentBuilder;
25 import javax.xml.parsers.DocumentBuilderFactory;
26 import javax.xml.parsers.ParserConfigurationException;
27 
28 import org.eclipse.swt.SWT;
29 import org.eclipse.ui.forms.HyperlinkSettings;
30 import org.w3c.dom.Document;
31 import org.w3c.dom.NamedNodeMap;
32 import org.w3c.dom.Node;
33 import org.w3c.dom.NodeList;
34 import org.xml.sax.ErrorHandler;
35 import org.xml.sax.InputSource;
36 import org.xml.sax.SAXException;
37 import org.xml.sax.SAXParseException;
38 
39 public class FormTextModel {
40 
41 	/*
42 	 * This class prevents parse errors from being written to standard output
43 	 */
44 	public static class ParseErrorHandler implements ErrorHandler {
45 
46 		@Override
error(SAXParseException arg0)47 		public void error(SAXParseException arg0) throws SAXException {
48 		}
49 
50 		@Override
fatalError(SAXParseException arg0)51 		public void fatalError(SAXParseException arg0) throws SAXException {
52 		}
53 
54 		@Override
warning(SAXParseException arg0)55 		public void warning(SAXParseException arg0) throws SAXException {
56 		}
57 	}
58 
59 	private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory
60 			.newInstance();
61 
62 	private boolean whitespaceNormalized = true;
63 
64 	private Vector<Paragraph> paragraphs;
65 
66 	private IFocusSelectable[] selectableSegments;
67 
68 	private int selectedSegmentIndex = -1;
69 
70 	private int savedSelectedLinkIndex = -1;
71 
72 	private HyperlinkSettings hyperlinkSettings;
73 
74 	public static final String BOLD_FONT_ID = "f.____bold"; //$NON-NLS-1$
75 
76 	//private static final int TEXT_ONLY_LINK = 1;
77 
78 	//private static final int IMG_ONLY_LINK = 2;
79 
80 	//private static final int TEXT_AND_IMAGES_LINK = 3;
81 
FormTextModel()82 	public FormTextModel() {
83 		reset();
84 	}
85 
86 	/*
87 	 * @see ITextModel#getParagraphs()
88 	 */
getParagraphs()89 	public Paragraph[] getParagraphs() {
90 		if (paragraphs == null)
91 			return new Paragraph[0];
92 		return paragraphs
93 				.toArray(new Paragraph[paragraphs.size()]);
94 	}
95 
getAccessibleText()96 	public String getAccessibleText() {
97 		if (paragraphs == null)
98 			return ""; //$NON-NLS-1$
99 		StringBuilder sbuf = new StringBuilder();
100 		for (Paragraph paragraph : paragraphs) {
101 			String text = paragraph.getAccessibleText();
102 			sbuf.append(text);
103 		}
104 		return sbuf.toString();
105 	}
106 
107 	/*
108 	 * @see ITextModel#parse(String)
109 	 */
parseTaggedText(String taggedText, boolean expandURLs)110 	public void parseTaggedText(String taggedText, boolean expandURLs) {
111 		if (taggedText == null) {
112 			reset();
113 			return;
114 		}
115 		taggedText = processAmpersandEscapes(taggedText);
116 		InputStream stream = new ByteArrayInputStream(taggedText.getBytes(StandardCharsets.UTF_8));
117 		parseInputStream(stream, expandURLs);
118 	}
119 
processAmpersandEscapes(String pTaggedText)120 	private String processAmpersandEscapes(String pTaggedText) {
121 		try {
122 			String taggedText = pTaggedText.replaceAll("&quot;", "&#034;"); //$NON-NLS-1$//$NON-NLS-2$
123 			taggedText = taggedText.replaceAll("&apos;", "&#039;"); //$NON-NLS-1$//$NON-NLS-2$
124 			taggedText = taggedText.replaceAll("&lt;", "&#060;"); //$NON-NLS-1$//$NON-NLS-2$
125 			taggedText = taggedText.replaceAll("&gt;", "&#062;"); //$NON-NLS-1$//$NON-NLS-2$
126 			taggedText = taggedText.replaceAll("&amp;", "&#038;"); //$NON-NLS-1$//$NON-NLS-2$
127 			return taggedText.replaceAll("&([^#])", "&#038;$1"); //$NON-NLS-1$//$NON-NLS-2$
128 		} catch (Exception e) {
129 			return pTaggedText;
130 		}
131 	}
132 
parseInputStream(InputStream is, boolean expandURLs)133 	public void parseInputStream(InputStream is, boolean expandURLs) {
134 
135 		documentBuilderFactory.setNamespaceAware(true);
136 		documentBuilderFactory.setIgnoringComments(true);
137 
138 		reset();
139 		try {
140 			DocumentBuilder parser = documentBuilderFactory
141 					.newDocumentBuilder();
142 			parser.setErrorHandler(new ParseErrorHandler());
143 			InputSource source = new InputSource(is);
144 			Document doc = parser.parse(source);
145 			processDocument(doc, expandURLs);
146 		} catch (ParserConfigurationException | SAXException e) {
147 			SWT.error(SWT.ERROR_INVALID_ARGUMENT, e, " " + e.getMessage()); //$NON-NLS-1$
148 		} catch (IOException e) {
149 			SWT.error(SWT.ERROR_IO, e);
150 		}
151 	}
152 
processDocument(Document doc, boolean expandURLs)153 	private void processDocument(Document doc, boolean expandURLs) {
154 		Node root = doc.getDocumentElement();
155 		NodeList children = root.getChildNodes();
156 		processSubnodes(paragraphs, children, expandURLs);
157 	}
158 
processSubnodes(Vector<Paragraph> plist, NodeList children, boolean expandURLs)159 	private void processSubnodes(Vector<Paragraph> plist, NodeList children, boolean expandURLs) {
160 		for (int i = 0; i < children.getLength(); i++) {
161 			Node child = children.item(i);
162 			if (child.getNodeType() == Node.TEXT_NODE) {
163 				// Make an implicit paragraph
164 				String text = getSingleNodeText(child);
165 				if (text != null && !isIgnorableWhiteSpace(text, true)) {
166 					Paragraph p = new Paragraph(true);
167 					p.parseRegularText(text, expandURLs, true,
168 							getHyperlinkSettings(), null);
169 					plist.add(p);
170 				}
171 			} else if (child.getNodeType() == Node.ELEMENT_NODE) {
172 				String tag = child.getNodeName().toLowerCase();
173 				if (tag.equals("p")) { //$NON-NLS-1$
174 					Paragraph p = processParagraph(child, expandURLs);
175 					if (p != null)
176 						plist.add(p);
177 				} else if (tag.equals("li")) { //$NON-NLS-1$
178 					Paragraph p = processListItem(child, expandURLs);
179 					if (p != null)
180 						plist.add(p);
181 				}
182 			}
183 		}
184 	}
185 
processParagraph(Node paragraph, boolean expandURLs)186 	private Paragraph processParagraph(Node paragraph, boolean expandURLs) {
187 		NodeList children = paragraph.getChildNodes();
188 		NamedNodeMap atts = paragraph.getAttributes();
189 		Node addSpaceAtt = atts.getNamedItem("addVerticalSpace"); //$NON-NLS-1$
190 		boolean addSpace = true;
191 
192 		if (addSpaceAtt == null)
193 			addSpaceAtt = atts.getNamedItem("vspace"); //$NON-NLS-1$
194 
195 		if (addSpaceAtt != null) {
196 			String value = addSpaceAtt.getNodeValue();
197 			addSpace = value.equalsIgnoreCase("true"); //$NON-NLS-1$
198 		}
199 		Paragraph p = new Paragraph(addSpace);
200 
201 		processSegments(p, children, expandURLs);
202 		return p;
203 	}
204 
processListItem(Node listItem, boolean expandURLs)205 	private Paragraph processListItem(Node listItem, boolean expandURLs) {
206 		NodeList children = listItem.getChildNodes();
207 		NamedNodeMap atts = listItem.getAttributes();
208 		Node addSpaceAtt = atts.getNamedItem("addVerticalSpace");//$NON-NLS-1$
209 		Node styleAtt = atts.getNamedItem("style");//$NON-NLS-1$
210 		Node valueAtt = atts.getNamedItem("value");//$NON-NLS-1$
211 		Node indentAtt = atts.getNamedItem("indent");//$NON-NLS-1$
212 		Node bindentAtt = atts.getNamedItem("bindent");//$NON-NLS-1$
213 		int style = BulletParagraph.CIRCLE;
214 		int indent = -1;
215 		int bindent = -1;
216 		String text = null;
217 		boolean addSpace = true;
218 
219 		if (addSpaceAtt != null) {
220 			String value = addSpaceAtt.getNodeValue();
221 			addSpace = value.equalsIgnoreCase("true"); //$NON-NLS-1$
222 		}
223 		if (styleAtt != null) {
224 			String value = styleAtt.getNodeValue();
225 			if (value.equalsIgnoreCase("text")) { //$NON-NLS-1$
226 				style = BulletParagraph.TEXT;
227 			} else if (value.equalsIgnoreCase("image")) { //$NON-NLS-1$
228 				style = BulletParagraph.IMAGE;
229 			} else if (value.equalsIgnoreCase("bullet")) { //$NON-NLS-1$
230 				style = BulletParagraph.CIRCLE;
231 			}
232 		}
233 		if (valueAtt != null) {
234 			text = valueAtt.getNodeValue();
235 			if (style == BulletParagraph.IMAGE)
236 				text = "i." + text; //$NON-NLS-1$
237 		}
238 		if (indentAtt != null) {
239 			String value = indentAtt.getNodeValue();
240 			try {
241 				indent = Integer.parseInt(value);
242 			} catch (NumberFormatException e) {
243 			}
244 		}
245 		if (bindentAtt != null) {
246 			String value = bindentAtt.getNodeValue();
247 			try {
248 				bindent = Integer.parseInt(value);
249 			} catch (NumberFormatException e) {
250 			}
251 		}
252 
253 		BulletParagraph p = new BulletParagraph(addSpace);
254 		p.setIndent(indent);
255 		p.setBulletIndent(bindent);
256 		p.setBulletStyle(style);
257 		p.setBulletText(text);
258 
259 		processSegments(p, children, expandURLs);
260 		return p;
261 	}
262 
processSegments(Paragraph p, NodeList children, boolean expandURLs)263 	private void processSegments(Paragraph p, NodeList children,
264 			boolean expandURLs) {
265 		for (int i = 0; i < children.getLength(); i++) {
266 			Node child = children.item(i);
267 			ParagraphSegment segment = null;
268 
269 			if (child.getNodeType() == Node.TEXT_NODE) {
270 				String value = getSingleNodeText(child);
271 
272 				if (value != null && !isIgnorableWhiteSpace(value, false)) {
273 					p.parseRegularText(value, expandURLs, true,
274 							getHyperlinkSettings(), null);
275 				}
276 			} else if (child.getNodeType() == Node.ELEMENT_NODE) {
277 				String name = child.getNodeName();
278 				if (name.equalsIgnoreCase("img")) { //$NON-NLS-1$
279 					segment = processImageSegment(child);
280 				} else if (name.equalsIgnoreCase("a")) { //$NON-NLS-1$
281 					segment = processHyperlinkSegment(child,
282 							getHyperlinkSettings());
283 				} else if (name.equalsIgnoreCase("span")) { //$NON-NLS-1$
284 					processTextSegment(p, expandURLs, child);
285 				} else if (name.equalsIgnoreCase("b")) { //$NON-NLS-1$
286 					String text = getNodeText(child);
287 					String fontId = BOLD_FONT_ID;
288 					p.parseRegularText(text, expandURLs, true,
289 							getHyperlinkSettings(), fontId);
290 				} else if (name.equalsIgnoreCase("br")) { //$NON-NLS-1$
291 					segment = new BreakSegment();
292 				} else if (name.equalsIgnoreCase("control")) { //$NON-NLS-1$
293 					segment = processControlSegment(child);
294 				}
295 			}
296 			if (segment != null) {
297 				p.addSegment(segment);
298 			}
299 		}
300 	}
301 
isIgnorableWhiteSpace(String text, boolean ignoreSpaces)302 	private boolean isIgnorableWhiteSpace(String text, boolean ignoreSpaces) {
303 		for (int i = 0; i < text.length(); i++) {
304 			char c = text.charAt(i);
305 			if (ignoreSpaces && c == ' ')
306 				continue;
307 			if (c == '\n' || c == '\r' || c == '\f')
308 				continue;
309 			return false;
310 		}
311 		return true;
312 	}
313 
processImageSegment(Node image)314 	private ImageSegment processImageSegment(Node image) {
315 		ImageSegment segment = new ImageSegment();
316 		processObjectSegment(segment, image, "i."); //$NON-NLS-1$
317 		return segment;
318 	}
319 
processControlSegment(Node control)320 	private ControlSegment processControlSegment(Node control) {
321 		ControlSegment segment = new ControlSegment();
322 		processObjectSegment(segment, control, "o."); //$NON-NLS-1$
323 		Node fill = control.getAttributes().getNamedItem("fill"); //$NON-NLS-1$
324 		if (fill!=null) {
325 			String value = fill.getNodeValue();
326 			boolean doFill = value.equalsIgnoreCase("true"); //$NON-NLS-1$
327 			segment.setFill(doFill);
328 		}
329 		try {
330 			Node width = control.getAttributes().getNamedItem("width"); //$NON-NLS-1$
331 			if (width!=null) {
332 				String value = width.getNodeValue();
333 				int doWidth = Integer.parseInt(value);
334 				segment.setWidth(doWidth);
335 			}
336 			Node height = control.getAttributes().getNamedItem("height"); //$NON-NLS-1$
337 			if (height!=null) {
338 				String value = height.getNodeValue();
339 				int doHeight = Integer.parseInt(value);
340 				segment.setHeight(doHeight);
341 			}
342 		}
343 		catch (NumberFormatException e) {
344 			// ignore invalid width or height
345 		}
346 		return segment;
347 	}
348 
processObjectSegment(ObjectSegment segment, Node object, String prefix)349 	private void processObjectSegment(ObjectSegment segment, Node object, String prefix) {
350 		NamedNodeMap atts = object.getAttributes();
351 		Node id = atts.getNamedItem("href"); //$NON-NLS-1$
352 		Node align = atts.getNamedItem("align"); //$NON-NLS-1$
353 		if (id != null) {
354 			String value = id.getNodeValue();
355 			segment.setObjectId(prefix + value);
356 		}
357 		if (align != null) {
358 			String value = align.getNodeValue().toLowerCase();
359 			switch (value) {
360 			case "top": //$NON-NLS-1$
361 				segment.setVerticalAlignment(ObjectSegment.TOP);
362 				break;
363 			case "middle": //$NON-NLS-1$
364 				segment.setVerticalAlignment(ObjectSegment.MIDDLE);
365 				break;
366 			case "bottom": //$NON-NLS-1$
367 				segment.setVerticalAlignment(ObjectSegment.BOTTOM);
368 				break;
369 			default:
370 				break;
371 			}
372 		}
373 	}
374 
appendText(String value, StringBuilder buf, int[] spaceCounter)375 	private void appendText(String value, StringBuilder buf, int[] spaceCounter) {
376 		if (!whitespaceNormalized)
377 			buf.append(value);
378 		else {
379 			for (int j = 0; j < value.length(); j++) {
380 				char c = value.charAt(j);
381 				switch (c) {
382 				case ' ':
383 				case '\t':
384 					// space
385 					if (++spaceCounter[0] == 1) {
386 						buf.append(c);
387 					}
388 					break;
389 				case '\n':
390 				case '\r':
391 				case '\f':
392 					// new line
393 					if (++spaceCounter[0] == 1) {
394 						buf.append(' ');
395 					}
396 					break;
397 				default:
398 					// other characters
399 					spaceCounter[0] = 0;
400 					buf.append(c);
401 					break;
402 				}
403 			}
404 		}
405 	}
406 
getNormalizedText(String text)407 	private String getNormalizedText(String text) {
408 		int[] spaceCounter = new int[1];
409 		StringBuilder buf = new StringBuilder();
410 
411 		if (text == null)
412 			return null;
413 		appendText(text, buf, spaceCounter);
414 		return buf.toString();
415 	}
416 
getSingleNodeText(Node node)417 	private String getSingleNodeText(Node node) {
418 		String text = getNormalizedText(node.getNodeValue());
419 		if (!whitespaceNormalized)
420 			return text;
421 		if (text.length() > 0 && node.getPreviousSibling() == null && isIgnorableWhiteSpace(text.substring(0, 1), true))
422 			return text.substring(1);
423 		if (text.length() > 1 && node.getNextSibling() == null
424 				&& isIgnorableWhiteSpace(text.substring(text.length() - 1), true))
425 			return text.substring(0, text.length() - 1);
426 		return text;
427 	}
428 
getNodeText(Node node)429 	private String getNodeText(Node node) {
430 		NodeList children = node.getChildNodes();
431 		StringBuilder buf = new StringBuilder();
432 		int[] spaceCounter = new int[1];
433 
434 		for (int i = 0; i < children.getLength(); i++) {
435 			Node child = children.item(i);
436 			if (child.getNodeType() == Node.TEXT_NODE) {
437 				String value = child.getNodeValue();
438 				appendText(value, buf, spaceCounter);
439 			}
440 		}
441 		if (whitespaceNormalized) {
442 			return buf.toString().trim();
443 		}
444 		return buf.toString();
445 	}
446 
processHyperlinkSegment(Node link, HyperlinkSettings settings)447 	private ParagraphSegment processHyperlinkSegment(Node link,
448 			HyperlinkSettings settings) {
449 		NamedNodeMap atts = link.getAttributes();
450 		String href = null;
451 		boolean wrapAllowed = true;
452 		String boldFontId = null;
453 
454 		Node hrefAtt = atts.getNamedItem("href"); //$NON-NLS-1$
455 		if (hrefAtt != null) {
456 			href = hrefAtt.getNodeValue();
457 		}
458 		Node boldAtt = atts.getNamedItem("bold"); //$NON-NLS-1$
459 		if (boldAtt != null) {
460 			boldFontId = BOLD_FONT_ID;
461 		}
462 		Node nowrap = atts.getNamedItem("nowrap"); //$NON-NLS-1$
463 		if (nowrap != null) {
464 			String value = nowrap.getNodeValue();
465 			if (value != null && value.equalsIgnoreCase("true")) //$NON-NLS-1$
466 				wrapAllowed = false;
467 		}
468 		Object status = checkChildren(link);
469 		if (status instanceof Node) {
470 			Node child = (Node)status;
471 			ImageHyperlinkSegment segment = new ImageHyperlinkSegment();
472 			segment.setHref(href);
473 			segment.setWordWrapAllowed(wrapAllowed);
474 			Node alt = child.getAttributes().getNamedItem("alt"); //$NON-NLS-1$
475 			if (alt!=null)
476 				segment.setTooltipText(alt.getNodeValue());
477 			Node text = child.getAttributes().getNamedItem("text"); //$NON-NLS-1$
478 			if (text!=null)
479 				segment.setText(text.getNodeValue());
480 			processObjectSegment(segment, child, "i."); //$NON-NLS-1$
481 			return segment;
482 		}  else if (status instanceof String) {
483 			String text = (String) status;
484 			TextHyperlinkSegment segment = new TextHyperlinkSegment(text,
485 					settings, null);
486 			segment.setHref(href);
487 			segment.setFontId(boldFontId);
488 			Node alt = atts.getNamedItem("alt"); //$NON-NLS-1$
489 			if (alt!=null)
490 				segment.setTooltipText(alt.getNodeValue());
491 			segment.setWordWrapAllowed(wrapAllowed);
492 			return segment;
493 		} else {
494 			AggregateHyperlinkSegment parent = new AggregateHyperlinkSegment();
495 			parent.setHref(href);
496 			NodeList children = link.getChildNodes();
497 			for (int i = 0; i < children.getLength(); i++) {
498 				Node child = children.item(i);
499 				if (child.getNodeType() == Node.TEXT_NODE) {
500 					String value = child.getNodeValue();
501 					TextHyperlinkSegment ts = new TextHyperlinkSegment(
502 							getNormalizedText(value), settings, null);
503 					Node alt = atts.getNamedItem("alt"); //$NON-NLS-1$
504 					if (alt!=null)
505 						ts.setTooltipText(alt.getNodeValue());
506 					ts.setWordWrapAllowed(wrapAllowed);
507 					parent.add(ts);
508 				} else if (child.getNodeType() == Node.ELEMENT_NODE) {
509 					String name = child.getNodeName();
510 					if (name.equalsIgnoreCase("img")) { //$NON-NLS-1$
511 						ImageHyperlinkSegment is = new ImageHyperlinkSegment();
512 						processObjectSegment(is, child, "i."); //$NON-NLS-1$
513 						Node alt = child.getAttributes().getNamedItem("alt"); //$NON-NLS-1$
514 						if (alt!=null)
515 							is.setTooltipText(alt.getNodeValue());
516 						parent.add(is);
517 						is.setWordWrapAllowed(wrapAllowed);
518 					}
519 				}
520 			}
521 			return parent;
522 		}
523 	}
524 
checkChildren(Node node)525 	private Object checkChildren(Node node) {
526 		boolean text = false;
527 		Node imgNode = null;
528 		//int status = 0;
529 
530 		NodeList children = node.getChildNodes();
531 		for (int i = 0; i < children.getLength(); i++) {
532 			Node child = children.item(i);
533 			if (child.getNodeType() == Node.TEXT_NODE)
534 				text = true;
535 			else if (child.getNodeType() == Node.ELEMENT_NODE
536 					&& child.getNodeName().equalsIgnoreCase("img")) { //$NON-NLS-1$
537 				imgNode = child;
538 			}
539 		}
540 		if (text && imgNode == null)
541 			return getNodeText(node);
542 		else if (!text && imgNode != null)
543 			return imgNode;
544 		else return null;
545 	}
546 
processTextSegment(Paragraph p, boolean expandURLs, Node textNode)547 	private void processTextSegment(Paragraph p, boolean expandURLs,
548 			Node textNode) {
549 		String text = getNodeText(textNode);
550 
551 		NamedNodeMap atts = textNode.getAttributes();
552 		Node font = atts.getNamedItem("font"); //$NON-NLS-1$
553 		Node color = atts.getNamedItem("color"); //$NON-NLS-1$
554 		boolean wrapAllowed=true;
555 		Node nowrap = atts.getNamedItem("nowrap"); //$NON-NLS-1$
556 		if (nowrap != null) {
557 			String value = nowrap.getNodeValue();
558 			if (value != null && value.equalsIgnoreCase("true")) //$NON-NLS-1$
559 				wrapAllowed = false;
560 		}
561 		String fontId = null;
562 		String colorId = null;
563 		if (font != null) {
564 			fontId = "f." + font.getNodeValue(); //$NON-NLS-1$
565 		}
566 		if (color != null) {
567 			colorId = "c." + color.getNodeValue(); //$NON-NLS-1$
568 		}
569 		p.parseRegularText(text, expandURLs, wrapAllowed, getHyperlinkSettings(), fontId,
570 				colorId);
571 	}
572 
parseRegularText(String regularText, boolean convertURLs)573 	public void parseRegularText(String regularText, boolean convertURLs) {
574 		reset();
575 
576 		if (regularText == null)
577 			return;
578 
579 		regularText = getNormalizedText(regularText);
580 
581 		Paragraph p = new Paragraph(true);
582 		paragraphs.add(p);
583 		int pstart = 0;
584 
585 		for (int i = 0; i < regularText.length(); i++) {
586 			char c = regularText.charAt(i);
587 			if (p == null) {
588 				p = new Paragraph(true);
589 				paragraphs.add(p);
590 			}
591 			if (c == '\n') {
592 				String text = regularText.substring(pstart, i);
593 				pstart = i + 1;
594 				p.parseRegularText(text, convertURLs, true, getHyperlinkSettings(),
595 						null);
596 				p = null;
597 			}
598 		}
599 		if (p != null) {
600 			// no new line
601 			String text = regularText.substring(pstart);
602 			p.parseRegularText(text, convertURLs, true, getHyperlinkSettings(), null);
603 		}
604 	}
605 
getHyperlinkSettings()606 	public HyperlinkSettings getHyperlinkSettings() {
607 		// #132723 cannot have null settings
608 		if (hyperlinkSettings==null)
609 			hyperlinkSettings = new HyperlinkSettings(SWTUtil.getStandardDisplay());
610 		return hyperlinkSettings;
611 	}
612 
setHyperlinkSettings(HyperlinkSettings settings)613 	public void setHyperlinkSettings(HyperlinkSettings settings) {
614 		this.hyperlinkSettings = settings;
615 	}
616 
reset()617 	private void reset() {
618 		if (paragraphs == null)
619 			paragraphs = new Vector<>();
620 		paragraphs.clear();
621 		selectedSegmentIndex = -1;
622 		savedSelectedLinkIndex = -1;
623 		selectableSegments = null;
624 	}
625 
getFocusSelectableSegments()626 	IFocusSelectable[] getFocusSelectableSegments() {
627 		if (selectableSegments != null || paragraphs == null)
628 			return selectableSegments;
629 		Vector<ParagraphSegment> result = new Vector<>();
630 		for (Paragraph paragraph : paragraphs) {
631 			ParagraphSegment[] segments = paragraph.getSegments();
632 			for (ParagraphSegment segment : segments) {
633 				if (segment instanceof IFocusSelectable)
634 					result.add(segment);
635 			}
636 		}
637 		selectableSegments = result
638 				.toArray(new IFocusSelectable[result.size()]);
639 		return selectableSegments;
640 	}
641 
getHyperlink(int index)642 	public IHyperlinkSegment getHyperlink(int index) {
643 		IFocusSelectable[] selectables = getFocusSelectableSegments();
644 		if (selectables.length>index) {
645 			IFocusSelectable link = selectables[index];
646 			if (link instanceof IHyperlinkSegment)
647 				return (IHyperlinkSegment)link;
648 		}
649 		return null;
650 	}
651 
findHyperlinkAt(int x, int y)652 	public IHyperlinkSegment findHyperlinkAt(int x, int y) {
653 		IFocusSelectable[] selectables = getFocusSelectableSegments();
654 		for (IFocusSelectable segment : selectables) {
655 			if (segment instanceof IHyperlinkSegment) {
656 				IHyperlinkSegment link = (IHyperlinkSegment)segment;
657 				if (link.contains(x, y))
658 					return link;
659 			}
660 		}
661 		return null;
662 	}
663 
getHyperlinkCount()664 	public int getHyperlinkCount() {
665 		return getFocusSelectableSegments().length;
666 	}
667 
indexOf(IHyperlinkSegment link)668 	public int indexOf(IHyperlinkSegment link) {
669 		IFocusSelectable[] selectables = getFocusSelectableSegments();
670 		for (int i = 0; i < selectables.length; i++) {
671 			IFocusSelectable segment = selectables[i];
672 			if (segment instanceof IHyperlinkSegment) {
673 				IHyperlinkSegment l = (IHyperlinkSegment)segment;
674 				if (link==l)
675 					return i;
676 			}
677 		}
678 		return -1;
679 	}
680 
findSegmentAt(int x, int y)681 	public ParagraphSegment findSegmentAt(int x, int y) {
682 		for (Paragraph paragraph : paragraphs) {
683 			ParagraphSegment segment = paragraph.findSegmentAt(x, y);
684 			if (segment != null)
685 				return segment;
686 		}
687 		return null;
688 	}
689 
clearCache(String fontId)690 	public void clearCache(String fontId) {
691 		for (Paragraph paragraph : paragraphs) {
692 			paragraph.clearCache(fontId);
693 		}
694 	}
695 
getSelectedSegment()696 	public IFocusSelectable getSelectedSegment() {
697 		if (selectableSegments==null || selectedSegmentIndex == -1)
698 			return null;
699 		return selectableSegments[selectedSegmentIndex];
700 	}
701 
getSelectedSegmentIndex()702 	public int getSelectedSegmentIndex() {
703 		return selectedSegmentIndex;
704 	}
705 
linkExists(IHyperlinkSegment link)706 	public boolean linkExists(IHyperlinkSegment link) {
707 		if (selectableSegments==null)
708 			return false;
709 		for (IFocusSelectable selectableSegment : selectableSegments) {
710 			if (selectableSegment==link)
711 				return true;
712 		}
713 		return false;
714 	}
715 
traverseFocusSelectableObjects(boolean next)716 	public boolean traverseFocusSelectableObjects(boolean next) {
717 		IFocusSelectable[] selectables = getFocusSelectableSegments();
718 		if (selectables == null)
719 			return false;
720 		int size = selectables.length;
721 		if (next) {
722 			selectedSegmentIndex++;
723 		} else
724 			selectedSegmentIndex--;
725 
726 		if (selectedSegmentIndex < 0 || selectedSegmentIndex > size - 1) {
727 			selectedSegmentIndex = -1;
728 		}
729 		return selectedSegmentIndex != -1;
730 	}
731 
getNextFocusSegment(boolean next)732 	public IFocusSelectable getNextFocusSegment(boolean next) {
733 		IFocusSelectable[] selectables = getFocusSelectableSegments();
734 		if (selectables == null)
735 			return null;
736 		int nextIndex = next?selectedSegmentIndex+1:selectedSegmentIndex-1;
737 
738 		if (nextIndex < 0 || nextIndex > selectables.length - 1) {
739 			return null;
740 		}
741 		return selectables[nextIndex];
742 	}
743 
restoreSavedLink()744 	public boolean restoreSavedLink() {
745 		if (savedSelectedLinkIndex!= -1) {
746 			selectedSegmentIndex = savedSelectedLinkIndex;
747 			return true;
748 		}
749 		return false;
750 	}
751 
selectLink(IHyperlinkSegment link)752 	public void selectLink(IHyperlinkSegment link) {
753 		if (link == null) {
754 			savedSelectedLinkIndex = selectedSegmentIndex;
755 			selectedSegmentIndex = -1;
756 		}
757 		else {
758 			select(link);
759 
760 		}
761 	}
762 
select(IFocusSelectable selectable)763 	public void select(IFocusSelectable selectable) {
764 		IFocusSelectable[] selectables = getFocusSelectableSegments();
765 		selectedSegmentIndex = -1;
766 		if (selectables == null)
767 			return;
768 		for (int i = 0; i < selectables.length; i++) {
769 			if (selectables[i].equals(selectable)) {
770 				selectedSegmentIndex = i;
771 				break;
772 			}
773 		}
774 	}
775 
hasFocusSegments()776 	public boolean hasFocusSegments() {
777 		IFocusSelectable[] segments = getFocusSelectableSegments();
778 		if (segments.length > 0)
779 			return true;
780 		return false;
781 	}
782 
dispose()783 	public void dispose() {
784 		paragraphs = null;
785 		selectedSegmentIndex = -1;
786 		savedSelectedLinkIndex = -1;
787 		selectableSegments = null;
788 	}
789 
790 	/**
791 	 * @return Returns the whitespaceNormalized.
792 	 */
isWhitespaceNormalized()793 	public boolean isWhitespaceNormalized() {
794 		return whitespaceNormalized;
795 	}
796 
797 	/**
798 	 * @param whitespaceNormalized
799 	 *            The whitespaceNormalized to set.
800 	 */
setWhitespaceNormalized(boolean whitespaceNormalized)801 	public void setWhitespaceNormalized(boolean whitespaceNormalized) {
802 		this.whitespaceNormalized = whitespaceNormalized;
803 	}
804 }
805