1 /*******************************************************************************
2  * Copyright (c) 2000, 2020 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  *******************************************************************************/
14 package org.eclipse.help.ui.internal.views;
15 
16 import java.net.URL;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 
20 import org.eclipse.core.runtime.IStatus;
21 import org.eclipse.core.runtime.Platform;
22 import org.eclipse.help.IHelpResource;
23 import org.eclipse.help.internal.base.BaseHelpSystem;
24 import org.eclipse.help.internal.base.HelpBasePlugin;
25 import org.eclipse.help.internal.search.SearchHit;
26 import org.eclipse.help.search.ISearchEngineResult;
27 import org.eclipse.help.search.ISearchEngineResult2;
28 import org.eclipse.help.ui.internal.HelpUIResources;
29 import org.eclipse.help.ui.internal.IHelpUIConstants;
30 import org.eclipse.help.ui.internal.Messages;
31 import org.eclipse.help.ui.internal.util.EscapeUtils;
32 import org.eclipse.osgi.util.NLS;
33 import org.eclipse.swt.SWT;
34 import org.eclipse.swt.custom.BusyIndicator;
35 import org.eclipse.swt.graphics.Image;
36 import org.eclipse.swt.graphics.ImageData;
37 import org.eclipse.swt.layout.GridData;
38 import org.eclipse.swt.layout.GridLayout;
39 import org.eclipse.swt.widgets.Composite;
40 import org.eclipse.swt.widgets.Control;
41 import org.eclipse.swt.widgets.Label;
42 import org.eclipse.swt.widgets.Menu;
43 import org.eclipse.ui.ISharedImages;
44 import org.eclipse.ui.IWorkbenchPage;
45 import org.eclipse.ui.IWorkbenchWindow;
46 import org.eclipse.ui.PartInitException;
47 import org.eclipse.ui.PlatformUI;
48 import org.eclipse.ui.forms.IFormColors;
49 import org.eclipse.ui.forms.events.ExpansionAdapter;
50 import org.eclipse.ui.forms.events.ExpansionEvent;
51 import org.eclipse.ui.forms.events.HyperlinkAdapter;
52 import org.eclipse.ui.forms.events.HyperlinkEvent;
53 import org.eclipse.ui.forms.events.IHyperlinkListener;
54 import org.eclipse.ui.forms.widgets.FormText;
55 import org.eclipse.ui.forms.widgets.FormToolkit;
56 import org.eclipse.ui.forms.widgets.ImageHyperlink;
57 import org.eclipse.ui.forms.widgets.Section;
58 import org.eclipse.ui.forms.widgets.TableWrapData;
59 import org.eclipse.ui.forms.widgets.TableWrapLayout;
60 import org.osgi.framework.Bundle;
61 
62 public class EngineResultSection {
63 
64 	private static final String KEY_PREFIX_GRAYED = "grayed:"; //$NON-NLS-1$
65 
66 	private static final String CAT_HEADING_PREFIX = "catheading:"; //$NON-NLS-1$
67 
68 	private SearchResultsPart part;
69 
70 	private EngineDescriptor desc;
71 
72 	private IStatus errorStatus;
73 
74 	private ArrayList<ISearchEngineResult> hits;
75 
76 	private Section section;
77 
78 	private Composite container;
79 
80 	private FormText searchResults;
81 
82 	private ImageHyperlink prevLink;
83 
84 	private ImageHyperlink nextLink;
85 
86 	private boolean needsUpdating;
87 
88 	private FederatedSearchSorter sorter;
89 
90 	private int HITS_PER_PAGE = 10;
91 
92 	private static final String HREF_PROGRESS = "__progress__"; //$NON-NLS-1$
93 
94 	private static final String PROGRESS_VIEW = "org.eclipse.ui.views.ProgressView"; //$NON-NLS-1$
95 
96 	private int resultOffset = 0;
97 
EngineResultSection(SearchResultsPart part, EngineDescriptor desc)98 	public EngineResultSection(SearchResultsPart part, EngineDescriptor desc) {
99 		this.part = part;
100 		this.desc = desc;
101 		hits = new ArrayList<>();
102 		sorter = new FederatedSearchSorter();
103 	}
104 
hasControl(Control control)105 	public boolean hasControl(Control control) {
106 		return searchResults.equals(control);
107 	}
108 
matches(EngineDescriptor desc)109 	public boolean matches(EngineDescriptor desc) {
110 		return this.desc == desc;
111 	}
112 
createControl(Composite parent, final FormToolkit toolkit)113 	public Control createControl(Composite parent, final FormToolkit toolkit) {
114 		section = toolkit.createSection(parent, Section.SHORT_TITLE_BAR | Section.COMPACT | Section.TWISTIE
115 				| Section.EXPANDED | Section.LEFT_TEXT_CLIENT_ALIGNMENT);
116 		// section.marginHeight = 10;
117 		container = toolkit.createComposite(section);
118 		TableWrapLayout layout = new TableWrapLayout();
119 		layout.topMargin = 0;
120 		layout.bottomMargin = 0;
121 		layout.leftMargin = 0;
122 		layout.rightMargin = 0;
123 		layout.verticalSpacing = 0;
124 		container.setLayout(layout);
125 		createFormText(container, toolkit);
126 		searchResults.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB));
127 		searchResults.setColor("summary", parent.getDisplay().getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW)); //$NON-NLS-1$
128 		section.setClient(container);
129 		updateSectionTitle(0);
130 		section.addExpansionListener(new ExpansionAdapter() {
131 
132 			@Override
133 			public void expansionStateChanging(ExpansionEvent e) {
134 				if (needsUpdating)
135 					asyncUpdateResults(true, false);
136 			}
137 		});
138 		return section;
139 	}
140 
createFormText(Composite parent, FormToolkit toolkit)141 	private void createFormText(Composite parent, FormToolkit toolkit) {
142 		searchResults = toolkit.createFormText(parent, false);
143 		searchResults.setColor(IFormColors.TITLE, toolkit.getColors().getColor(IFormColors.TITLE));
144 		searchResults.marginHeight = 5;
145 		String topicKey = IHelpUIConstants.IMAGE_FILE_F1TOPIC;
146 		String searchKey = IHelpUIConstants.IMAGE_HELP_SEARCH;
147 		searchResults.setImage(topicKey, HelpUIResources.getImage(topicKey));
148 		searchResults.setImage(searchKey, HelpUIResources.getImage(searchKey));
149 		searchResults.setColor("summary", parent.getDisplay().getSystemColor( //$NON-NLS-1$
150 				SWT.COLOR_WIDGET_DARK_SHADOW));
151 		searchResults.setImage(ISharedImages.IMG_TOOL_FORWARD, PlatformUI.getWorkbench().getSharedImages()
152 				.getImage(ISharedImages.IMG_TOOL_FORWARD));
153 		searchResults.setImage(ISharedImages.IMG_TOOL_BACK, PlatformUI.getWorkbench().getSharedImages()
154 				.getImage(ISharedImages.IMG_TOOL_BACK));
155 		searchResults.setImage(ISharedImages.IMG_OBJS_ERROR_TSK, PlatformUI.getWorkbench().getSharedImages()
156 				.getImage(ISharedImages.IMG_OBJS_ERROR_TSK));
157 		searchResults.setImage(desc.getId(), desc.getIconImage());
158 		searchResults.setImage(KEY_PREFIX_GRAYED + desc.getId(), getGrayedImage(desc.getIconImage()));
159 		searchResults.addHyperlinkListener(new IHyperlinkListener() {
160 
161 			@Override
162 			public void linkActivated(HyperlinkEvent e) {
163 				Object href = e.getHref();
164 				String shref = (String) href;
165 				if (HREF_PROGRESS.equals(href)) {
166 					showProgressView();
167 				} else if (shref.startsWith("bmk:")) { //$NON-NLS-1$
168 					doBookmark(e.getLabel(), shref);
169 				} else if (shref.startsWith(CAT_HEADING_PREFIX)) {
170 					part.doCategoryLink(shref.substring(CAT_HEADING_PREFIX.length()));
171 				} else
172 					part.doOpenLink(e.getHref());
173 			}
174 
175 			@Override
176 			public void linkEntered(HyperlinkEvent e) {
177 				part.parent.handleLinkEntered(e);
178 			}
179 
180 			@Override
181 			public void linkExited(HyperlinkEvent e) {
182 				part.parent.handleLinkExited(e);
183 			}
184 		});
185 		initializeText();
186 		part.parent.hookFormText(searchResults);
187 		needsUpdating = true;
188 	}
189 
initializeText()190 	private void initializeText() {
191 		Bundle bundle = Platform.getBundle("org.eclipse.ui.views"); //$NON-NLS-1$
192 		if (bundle != null) {
193 			StringBuilder buff = new StringBuilder();
194 			buff.append("<form>"); //$NON-NLS-1$
195 			buff.append("<p><a href=\""); //$NON-NLS-1$
196 			buff.append(HREF_PROGRESS);
197 			buff.append("\""); //$NON-NLS-1$
198 			if (!Platform.getWS().equals(Platform.WS_GTK)) {
199 				buff.append(" alt=\""); //$NON-NLS-1$
200 				buff.append(Messages.EngineResultSection_progressTooltip);
201 				buff.append("\""); //$NON-NLS-1$
202 			}
203 			buff.append(">"); //$NON-NLS-1$
204 			buff.append(Messages.EngineResultSection_searchInProgress);
205 			buff.append("</a></p></form>"); //$NON-NLS-1$
206 			searchResults.setText(buff.toString(), true, false);
207 		} else {
208 			searchResults.setText(Messages.EngineResultSection_progress2, false, false);
209 		}
210 	}
211 
showProgressView()212 	private void showProgressView() {
213 		IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
214 		if (window != null) {
215 			IWorkbenchPage page = window.getActivePage();
216 			if (page != null) {
217 				try {
218 					page.showView(PROGRESS_VIEW);
219 				} catch (PartInitException e) {
220 					Platform.getLog(getClass()).error(Messages.EngineResultSection_progressError, e);
221 				}
222 			}
223 		}
224 	}
225 
add(ISearchEngineResult match)226 	public synchronized void add(ISearchEngineResult match) {
227 		hits.add(match);
228 		asyncUpdateResults(false, false);
229 	}
230 
231 	/*
232 	 * (non-Javadoc)
233 	 *
234 	 * @see org.eclipse.help.internal.search.federated.ISearchEngineResultCollector#add(org.eclipse.help.internal.search.federated.ISearchEngineResult[])
235 	 */
add(ISearchEngineResult[] matches)236 	public synchronized void add(ISearchEngineResult[] matches) {
237 		Collections.addAll(hits, matches);
238 		asyncUpdateResults(false, false);
239 	}
240 
error(IStatus status)241 	public synchronized void error(IStatus status) {
242 		errorStatus = status;
243 		asyncUpdateResults(false, false);
244 	}
245 
completed()246 	public synchronized void completed() {
247 		if (hits.isEmpty() && !searchResults.isDisposed())
248 			asyncUpdateResults(false, false);
249 	}
250 
canceling()251 	public synchronized void canceling() {
252 		if (hits.isEmpty() && !searchResults.isDisposed()) {
253 			StringBuilder buff = new StringBuilder();
254 			buff.append("<form>"); //$NON-NLS-1$
255 			buff.append("<p><span color=\"summary\">");//$NON-NLS-1$
256 			buff.append(Messages.EngineResultSection_canceling);
257 			buff.append("</span></p>"); //$NON-NLS-1$
258 			buff.append("</form>"); //$NON-NLS-1$
259 			searchResults.setText(buff.toString(), true, false);
260 		}
261 	}
262 
asyncUpdateResults(boolean now, final boolean scrollToBeginning)263 	private void asyncUpdateResults(boolean now, final boolean scrollToBeginning) {
264 		Runnable runnable = () -> BusyIndicator.showWhile(PlatformUI.getWorkbench().getDisplay(), () -> {
265 			if (section.isDisposed()) {
266 				return;
267 			}
268 			updateResults(true);
269 			if (scrollToBeginning) {
270 				searchResults.setFocus();
271 				FormToolkit.setControlVisible(section, true);
272 				part.updateSeparatorVisibility();
273 			}
274 		});
275 		if (section.isDisposed()) {
276 			return;
277 		}
278 		if (now) {
279 			PlatformUI.getWorkbench().getDisplay().syncExec(runnable);
280 		} else {
281 			PlatformUI.getWorkbench().getDisplay().asyncExec(runnable);
282 		}
283 	}
284 
getResults()285 	private ISearchEngineResult[] getResults() {
286 		ArrayList<ISearchEngineResult> list = hits;
287 		if (desc.getEngineTypeId().equals(IHelpUIConstants.INTERNAL_HELP_ID)) {
288 			if (part.parent.isFilteredByRoles()) {
289 				list = new ArrayList<>();
290 				for (int i = 0; i < hits.size(); i++) {
291 					ISearchEngineResult hit = hits.get(i);
292 					if (HelpBasePlugin.getActivitySupport().isEnabled(hit.getHref()))
293 						list.add(hit);
294 				}
295 			}
296 		}
297 		ISearchEngineResult[] results = list.toArray(new ISearchEngineResult[list.size()]);
298 		if (part.getShowCategories())
299 			sorter.sort(null, results);
300 		return results;
301 	}
302 
303 	/**
304 	 * Returns a copy of the given image but grayed and half transparent.
305 	 * This gives the icon a grayed/disabled look.
306 	 *
307 	 * @param image the image to gray
308 	 * @return the grayed image
309 	 */
getGrayedImage(Image image)310 	private Image getGrayedImage(Image image) {
311 		// first gray the image
312 		Image temp = new Image(image.getDevice(), image, SWT.IMAGE_GRAY);
313 		// then add alpha to blend it 50/50 with the background
314 		ImageData data = temp.getImageData();
315 		ImageData maskData = data.getTransparencyMask();
316 		if (maskData != null) {
317 			for (int y=0;y<maskData.height;++y) {
318 				for (int x=0;x<maskData.width;++x) {
319 					if (maskData.getPixel(x, y) == 0) {
320 						// masked; set to transparent
321 						data.setAlpha(x, y, 0);
322 					}
323 					else {
324 						// not masked; set to translucent
325 						data.setAlpha(x, y, 128);
326 					}
327 				}
328 			}
329 			data.maskData = null;
330 		}
331 		Image grayed = new Image(image.getDevice(), data);
332 		temp.dispose();
333 		return grayed;
334 	}
335 
updateResults(boolean reflow)336 	void updateResults(boolean reflow) {
337 		ISearchEngineResult[] results = getResults();
338 		updateSectionTitle(results.length);
339 		StringBuilder buff = new StringBuilder();
340 		buff.append("<form>"); //$NON-NLS-1$
341 		IHelpResource oldCat = null;
342 
343 		for (int i = resultOffset; i < results.length; i++) {
344 			if (i - resultOffset == HITS_PER_PAGE) {
345 				break;
346 			}
347 			ISearchEngineResult hit = results[i];
348 			IHelpResource cat = hit.getCategory();
349 			if (part.getShowCategories() && cat != null
350 					&& (oldCat == null || !oldCat.getLabel().equals(cat.getLabel()))) {
351 				buff.append("<p>"); //$NON-NLS-1$
352 				if (cat.getHref() != null) {
353 					buff.append("<a bold=\"true\" href=\""); //$NON-NLS-1$
354 					String absoluteHref = ""; //$NON-NLS-1$
355 					if (cat.getHref().endsWith(".xml")) { //$NON-NLS-1$
356 						absoluteHref = absoluteHref + CAT_HEADING_PREFIX;
357 					}
358 					absoluteHref = absoluteHref + hit.toAbsoluteHref(cat.getHref(), true);
359 					buff.append(EscapeUtils.escapeSpecialChars(absoluteHref));
360 					buff.append("\">"); //$NON-NLS-1$
361 					buff.append(cat.getLabel());
362 					buff.append("</a>"); //$NON-NLS-1$
363 				} else {
364 					buff.append("<b>"); //$NON-NLS-1$
365 					buff.append(cat.getLabel());
366 					buff.append("</b>"); //$NON-NLS-1$
367 				}
368 				buff.append("</p>"); //$NON-NLS-1$
369 				oldCat = cat;
370 			}
371 			int indent = part.getShowCategories() && cat != null ? 26 : 21;
372 			int bindent = part.getShowCategories() && cat != null ? 5 : 0;
373 			buff.append("<li indent=\"" + indent + "\" bindent=\"" + bindent + "\" style=\"image\" value=\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
374 			String imageId = desc.getId();
375 			boolean isPotentialHit = (hit instanceof SearchHit && ((SearchHit)hit).isPotentialHit());
376 			if (hit instanceof ISearchEngineResult2) {
377 				URL iconURL = ((ISearchEngineResult2) hit).getIconURL();
378 				if (iconURL != null) {
379 					String id = null;
380 					if (isPotentialHit) {
381 						id = registerGrayedHitIcon(iconURL);
382 					}
383 					else {
384 						id = registerHitIcon(iconURL);
385 					}
386 					if (id != null)
387 						imageId = id;
388 				}
389 			}
390 
391 			if (isPotentialHit) {
392 				imageId = KEY_PREFIX_GRAYED + imageId;
393 			}
394 
395 			buff.append(imageId);
396 			buff.append("\">"); //$NON-NLS-1$
397 			buff.append("<a href=\""); //$NON-NLS-1$
398 			String href=null;
399 			if (hit instanceof ISearchEngineResult2) {
400 				ISearchEngineResult2 hit2 = (ISearchEngineResult2)hit;
401 				if (((ISearchEngineResult2)hit).canOpen()) {
402 					href = "open:"+desc.getId()+"?id="+hit2.getId(); //$NON-NLS-1$ //$NON-NLS-2$
403 				}
404 			}
405 			if (href==null) {
406 				if (hit.getForceExternalWindow())
407 					href = "nw:";//$NON-NLS-1$
408 				href = EscapeUtils.escapeSpecialChars(hit.toAbsoluteHref(hit.getHref(), false));
409 			}
410 			buff.append(href);
411 			buff.append("\""); //$NON-NLS-1$
412 			if (hit.getCategory() != null && Platform.getWS() != Platform.WS_GTK) {
413 				buff.append(" alt=\""); //$NON-NLS-1$
414 				buff.append(hit.getCategory().getLabel());
415 				buff.append("\""); //$NON-NLS-1$
416 			}
417 			buff.append(">"); //$NON-NLS-1$
418 			String elabel = null;
419 			if (isPotentialHit) {
420 				// add "(potential hit)"
421 				elabel = Messages.bind(Messages.SearchPart_potential_hit, hit.getLabel());
422 			}
423 			else {
424 				elabel = hit.getLabel();
425 			}
426 
427 			elabel = EscapeUtils.escapeSpecialChars(elabel);
428 			buff.append(elabel);
429 			buff.append("</a>"); //$NON-NLS-1$
430 			if (part.getShowDescription()) {
431 				String edesc = hit.getDescription();
432 				if (edesc != null) {
433 					edesc = EscapeUtils.escapeSpecialChars(edesc);
434 					buff.append("<br/>"); //$NON-NLS-1$
435 					buff.append(edesc);
436 				}
437 			}
438 			buff.append("</li>"); //$NON-NLS-1$
439 		}
440 		if (errorStatus != null)
441 			updateErrorStatus(buff);
442 		updateNavigation(results.length);
443 		buff.append("</form>"); //$NON-NLS-1$
444 		searchResults.setText(buff.toString(), true, false);
445 		section.layout();
446 		if (reflow)
447 			part.reflow();
448 	}
449 
450 	/**
451 	 * Registers the given icon URL for use with this section. Icons
452 	 * must be registered before use and referenced by the returned
453 	 * ID.
454 	 *
455 	 * @param iconURL the URL to the icon
456 	 * @return the ID to use for referencing the icon
457 	 */
registerHitIcon(URL iconURL)458 	private String registerHitIcon(URL iconURL) {
459 		Image image = HelpUIResources.getImage(iconURL);
460 		if (image != null) {
461 			searchResults.setImage(iconURL.toString(), image);
462 			return iconURL.toString();
463 		}
464 		return null;
465 	}
466 
467 	/**
468 	 * Same as registerHitIcon() but to register a grayed icon. You
469 	 * can provide the same URL for both the regular and grayed icons,
470 	 * but two different IDs will be returned.
471 	 *
472 	 * @param iconURL the URL to the icon
473 	 * @return the ID to use for referencing the icon
474 	 */
registerGrayedHitIcon(URL iconURL)475 	private String registerGrayedHitIcon(URL iconURL) {
476 		Image image = HelpUIResources.getImage(iconURL);
477 		if (image != null) {
478 			searchResults.setImage(iconURL.toString(), image);
479 			return KEY_PREFIX_GRAYED + iconURL.toString();
480 		}
481 		return null;
482 	}
483 
updateErrorStatus(StringBuilder buff)484 	private void updateErrorStatus(StringBuilder buff) {
485 		int indent = 21;
486 		buff.append("<li indent=\"" + indent + "\" style=\"image\" value=\""); //$NON-NLS-1$ //$NON-NLS-2$
487 		buff.append(ISharedImages.IMG_OBJS_ERROR_TSK);
488 		buff.append("\">"); //$NON-NLS-1$
489 		buff.append("<b>"); //$NON-NLS-1$
490 		buff.append(EscapeUtils.escapeSpecialChars(errorStatus.getMessage()));
491 		buff.append("</b>"); //$NON-NLS-1$
492 		buff.append("<br/>"); //$NON-NLS-1$
493 		Throwable t = errorStatus.getException();
494 		if (t != null && t.getMessage() != null)
495 			buff.append(EscapeUtils.escapeSpecialChars(t.getMessage()));
496 		buff.append("</li>"); //$NON-NLS-1$
497 	}
498 
updateNavigation(int size)499 	private void updateNavigation(int size) {
500 		if (size > HITS_PER_PAGE) {
501 			if (prevLink == null) {
502 				FormToolkit toolkit = part.getToolkit();
503 				Composite navContainer = toolkit.createComposite(container);
504 				TableWrapData td = new TableWrapData(TableWrapData.FILL_GRAB);
505 				navContainer.setLayoutData(td);
506 				GridLayout glayout = new GridLayout();
507 				glayout.numColumns = 2;
508 				navContainer.setLayout(glayout);
509 				GridData gd;
510 				/*
511 				 * Label sep = toolkit.createLabel(navContainer, null, SWT.SEPARATOR |
512 				 * SWT.HORIZONTAL); GridData gd = new GridData(GridData.HORIZONTAL_ALIGN_FILL);
513 				 * gd.horizontalSpan = 2; gd.widthHint = 2; sep.setLayoutData(gd);
514 				 */
515 				prevLink = toolkit.createImageHyperlink(navContainer, SWT.NULL);
516 
517 				prevLink.setText(NLS.bind(Messages.EngineResultSection_previous, "" + HITS_PER_PAGE)); //$NON-NLS-1$
518 				prevLink.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(
519 						ISharedImages.IMG_TOOL_BACK));
520 				prevLink.addHyperlinkListener(new HyperlinkAdapter() {
521 
522 					@Override
523 					public void linkActivated(HyperlinkEvent e) {
524 						resultOffset -= HITS_PER_PAGE;
525 						asyncUpdateResults(false, true);
526 					}
527 				});
528 				nextLink = toolkit.createImageHyperlink(navContainer, SWT.RIGHT);
529 
530 				nextLink.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(
531 						ISharedImages.IMG_TOOL_FORWARD));
532 				gd = new GridData(GridData.HORIZONTAL_ALIGN_END);
533 				gd.grabExcessHorizontalSpace = true;
534 				nextLink.setLayoutData(gd);
535 				nextLink.addHyperlinkListener(new HyperlinkAdapter() {
536 
537 					@Override
538 					public void linkActivated(HyperlinkEvent e) {
539 						resultOffset += HITS_PER_PAGE;
540 						asyncUpdateResults(false, true);
541 					}
542 				});
543 			}
544 			prevLink.setVisible(resultOffset > 0);
545 
546 			int nextOffset = resultOffset + HITS_PER_PAGE;
547 			int remainder = hits.size() - nextOffset;
548 			remainder = Math.min(remainder, HITS_PER_PAGE);
549 
550 			nextLink.setText(NLS.bind(Messages.EngineResultSection_next, "" + remainder)); //$NON-NLS-1$
551 			nextLink.setVisible(hits.size() > resultOffset + HITS_PER_PAGE);
552 		} else {
553 			if (prevLink != null) {
554 				prevLink.getParent().setMenu(null);
555 				prevLink.getParent().dispose();
556 				prevLink = null;
557 				nextLink = null;
558 			}
559 		}
560 	}
561 
updateSectionTitle(int size)562 	private void updateSectionTitle(int size) {
563 		if (errorStatus != null) {
564 			Label label = part.getToolkit().createLabel(section, null);
565 			label.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(
566 					ISharedImages.IMG_OBJS_ERROR_TSK));
567 			section.setTextClient(label);
568 			section.setText(Messages.EngineResultSection_sectionTitle_error);
569 		} else {
570 			section.setTextClient(null);
571 		}
572 		if (size == 1)
573 			section.setText(NLS.bind(Messages.EngineResultSection_sectionTitle_hit, desc.getLabel(), "" //$NON-NLS-1$
574 					+ hits.size()));
575 		else if (size <= HITS_PER_PAGE)
576 			section.setText(NLS.bind(Messages.EngineResultSection_sectionTitle_hits, desc.getLabel(),
577 					"" + hits.size())); //$NON-NLS-1$
578 		else {
579 			int from = (resultOffset + 1);
580 			int to = (resultOffset + HITS_PER_PAGE);
581 			to = Math.min(to, size);
582 			section.setText(NLS.bind(Messages.EngineResultSection_sectionTitle_hitsRange, new String[] {
583 					desc.getLabel(), "" + from, "" + to, "" + size })); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
584 		}
585 	}
586 
doBookmark(final String label, String href)587 	private void doBookmark(final String label, String href) {
588 		final String fhref = href.substring(4);
589 		BusyIndicator.showWhile(container.getDisplay(),
590 				() -> BaseHelpSystem.getBookmarkManager().addBookmark(fhref, label));
591 	}
592 
dispose()593 	public void dispose() {
594 		part.parent.unhookFormText(searchResults);
595 		if (!section.isDisposed()) {
596 			recursiveSetMenu(section, null);
597 			section.dispose();
598 		}
599 	}
600 
recursiveSetMenu(Control control, Menu menu)601 	private void recursiveSetMenu(Control control, Menu menu) {
602 		control.setMenu(menu);
603 		if (control instanceof Composite) {
604 			Composite parent = (Composite) control;
605 			Control[] children = parent.getChildren();
606 			for (int i = 0; i < children.length; i++) {
607 				recursiveSetMenu(children[i], menu);
608 			}
609 		}
610 	}
611 }
612