1/*******************************************************************************
2 * Copyright (c) 2011 Google, Inc.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors:
9 *    Google, Inc. - initial API and implementation
10 *******************************************************************************/
11package org.eclipse.wb.internal.core.model.property.table;
12
13import com.google.common.base.Charsets;
14import com.google.common.base.Joiner;
15
16import org.eclipse.swt.SWT;
17import org.eclipse.swt.browser.Browser;
18import org.eclipse.swt.browser.LocationAdapter;
19import org.eclipse.swt.browser.LocationEvent;
20import org.eclipse.swt.browser.ProgressAdapter;
21import org.eclipse.swt.browser.ProgressEvent;
22import org.eclipse.swt.graphics.Color;
23import org.eclipse.swt.graphics.Point;
24import org.eclipse.swt.widgets.Composite;
25import org.eclipse.swt.widgets.Control;
26import org.eclipse.swt.widgets.Event;
27import org.eclipse.swt.widgets.Label;
28import org.eclipse.swt.widgets.Listener;
29import org.eclipse.swt.widgets.Shell;
30import org.eclipse.ui.PlatformUI;
31import org.eclipse.ui.browser.IWebBrowser;
32import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
33import org.eclipse.wb.draw2d.IColorConstants;
34import org.eclipse.wb.internal.core.DesignerPlugin;
35import org.eclipse.wb.internal.core.EnvironmentUtils;
36import org.eclipse.wb.internal.core.utils.reflect.ReflectionUtils;
37import org.eclipse.wb.internal.core.utils.ui.GridDataFactory;
38import org.eclipse.wb.internal.core.utils.ui.PixelConverter;
39
40import java.io.StringReader;
41import java.net.URL;
42import java.text.MessageFormat;
43
44/**
45 * Helper for displaying HTML tooltips.
46 *
47 * @author scheglov_ke
48 * @coverage core.model.property.table
49 */
50public final class HtmlTooltipHelper {
51  public static Control createTooltipControl(Composite parent, String header, String details) {
52    return createTooltipControl(parent, header, details, 0);
53  }
54
55  public static Control createTooltipControl(Composite parent,
56      String header,
57      String details,
58      int heightLimit) {
59    // prepare Control
60    Control control;
61    try {
62      String html = "<table cellspacing=2 cellpadding=0 border=0 margins=0 id=_wbp_tooltip_body>";
63      if (header != null) {
64        html += "<tr align=center><td><b>" + header + "</b></td></tr>";
65      }
66      html += "<tr><td align=justify>" + details + "</td></tr>";
67      html += "</table>";
68      control = createTooltipControl_Browser(parent, html, heightLimit);
69    } catch (Throwable e) {
70      control = createTooltipControl_Label(parent, details);
71    }
72    // set listeners
73    {
74      Listener listener = new Listener() {
75        @Override
76        public void handleEvent(Event event) {
77          Control tooltipControl = (Control) event.widget;
78          hideTooltip(tooltipControl);
79        }
80      };
81      control.addListener(SWT.MouseExit, listener);
82    }
83    // done
84    return control;
85  }
86
87  /**
88   * Creates {@link Browser} for displaying tooltip.
89   */
90  private static Control createTooltipControl_Browser(Composite parent,
91      String html,
92      final int heightLimitChars) {
93    // prepare styles
94    String styles;
95    try {
96        styles = DesignerPlugin.readFile(PropertyTable.class.getResourceAsStream("Tooltip.css"),
97                Charsets.US_ASCII);
98        if (styles == null) {
99            styles = "";
100        }
101    } catch (Throwable e) {
102      styles = "";
103    }
104    // prepare HTML with styles and tags
105    String wrappedHtml;
106    {
107      String bodyAttributes =
108          MessageFormat.format(
109              "text=''{0}'' bgcolor=''{1}''",
110              getColorWebString(IColorConstants.tooltipForeground),
111              getColorWebString(IColorConstants.tooltipBackground));
112      String closeElement =
113          EnvironmentUtils.IS_LINUX
114              ? "    <a href='' style='position:absolute;right:1em;' id=_wbp_close>Close</a>"
115              : "";
116      wrappedHtml =
117          /*CodeUtils.*/getSource(
118              "<html>",
119              "  <style CHARSET='ISO-8859-1' TYPE='text/css'>",
120              styles,
121              "  </style>",
122              "  <body " + bodyAttributes + ">",
123              closeElement,
124              html,
125              "  </body>",
126              "</html>");
127    }
128    // prepare Browser
129    final Browser browser = new Browser(parent, SWT.NONE);
130    browser.setText(wrappedHtml);
131    // open URLs in new window
132    browser.addLocationListener(new LocationAdapter() {
133      @Override
134      public void changing(LocationEvent event) {
135        event.doit = false;
136        hideTooltip((Browser) event.widget);
137        if (!"about:blank".equals(event.location)) {
138          try {
139            IWorkbenchBrowserSupport support = PlatformUI.getWorkbench().getBrowserSupport();
140            IWebBrowser browserSupport = support.createBrowser("wbp.browser");
141            browserSupport.openURL(new URL(event.location));
142          } catch (Throwable e) {
143            DesignerPlugin.log(e);
144          }
145        }
146      }
147    });
148    // set size
149    {
150      int textLength = getTextLength(html);
151      // horizontal hint
152      int hintH = 50;
153      if (textLength < 100) {
154        hintH = 40;
155      }
156      // vertical hint
157      int hintV = textLength / hintH + 3;
158      hintV = Math.min(hintV, 8);
159      // do set
160      GridDataFactory.create(browser).hintC(hintH, hintV);
161    }
162    // tweak size after rendering HTML
163    browser.addProgressListener(new ProgressAdapter() {
164      @Override
165      public void completed(ProgressEvent event) {
166        browser.removeProgressListener(this);
167        tweakBrowserSize(browser, heightLimitChars);
168        browser.getShell().setVisible(true);
169      }
170    });
171    // done
172    return browser;
173  }
174
175  private static void tweakBrowserSize(Browser browser, int heightLimitChars) {
176    GridDataFactory.create(browser).grab().fill();
177    // limit height
178    if (heightLimitChars != 0) {
179      PixelConverter pixelConverter = new PixelConverter(browser);
180      int maxHeight = pixelConverter.convertHeightInCharsToPixels(heightLimitChars);
181      expandShellToShowFullPage_Height(browser, maxHeight);
182    }
183    // if no limit, then show all, so make as tall as required
184    if (heightLimitChars == 0) {
185      expandShellToShowFullPage_Height(browser, Integer.MAX_VALUE);
186    }
187  }
188
189  private static void expandShellToShowFullPage_Height(Browser browser, int maxHeight) {
190    try {
191      Shell shell = browser.getShell();
192      // calculate required
193      int contentHeight;
194      {
195        getContentOffsetHeight(browser);
196        contentHeight = getContentScrollHeight(browser);
197      }
198      // apply height
199      int useHeight = Math.min(contentHeight + ((EnvironmentUtils.IS_LINUX) ? 2 : 10), maxHeight);
200      shell.setSize(shell.getSize().x, useHeight);
201      // trim height to content
202      {
203        int offsetHeight = getBodyOffsetHeight(browser);
204        int scrollHeight = getBodyScrollHeight(browser);
205        int delta = scrollHeight - offsetHeight;
206        if (delta != 0 && delta < 10) {
207          Point size = shell.getSize();
208          shell.setSize(size.x, size.y + delta + 1);
209        }
210      }
211      // trim width to content
212      {
213        int offsetWidth = getContentOffsetWidth(browser);
214        {
215          Point size = shell.getSize();
216          shell.setSize(offsetWidth + ((EnvironmentUtils.IS_MAC) ? 6 : 10), size.y);
217        }
218      }
219      // hide 'Close' if too narrow
220      if (EnvironmentUtils.IS_LINUX) {
221        if (shell.getSize().y < 30) {
222          hideCloseElement(browser);
223        }
224      }
225    } catch (Throwable e) {
226    }
227  }
228
229  private static int getContentOffsetWidth(Browser browser) throws Exception {
230    return evaluateScriptInt(
231        browser,
232        "return document.getElementById('_wbp_tooltip_body').offsetWidth;");
233  }
234
235  private static int getContentOffsetHeight(Browser browser) throws Exception {
236    return evaluateScriptInt(
237        browser,
238        "return document.getElementById('_wbp_tooltip_body').offsetHeight;");
239  }
240
241  private static int getContentScrollHeight(Browser browser) throws Exception {
242    return evaluateScriptInt(
243        browser,
244        "return document.getElementById('_wbp_tooltip_body').scrollHeight;");
245  }
246
247  private static int getBodyOffsetHeight(Browser browser) throws Exception {
248    return evaluateScriptInt(browser, "return document.body.offsetHeight;");
249  }
250
251  private static int getBodyScrollHeight(Browser browser) throws Exception {
252    return evaluateScriptInt(browser, "return document.body.scrollHeight;");
253  }
254
255  private static int evaluateScriptInt(Browser browser, String script) throws Exception {
256    Object o = ReflectionUtils.invokeMethod(browser, "evaluate(java.lang.String)", script);
257    return ((Number) o).intValue();
258  }
259
260  private static void hideCloseElement(Browser browser) throws Exception {
261    String script = "document.getElementById('_wbp_close').style.display = 'none'";
262    ReflectionUtils.invokeMethod(browser, "evaluate(java.lang.String)", script);
263  }
264
265  /**
266   * @return the length of text in given HTML. Uses internal class, so may fail, in this case
267   *         returns length on HTML.
268   */
269  private static int getTextLength(String html) {
270    StringReader htmlStringReader = new StringReader(html);
271    try {
272      ClassLoader classLoader = PropertyTable.class.getClassLoader();
273      Class<?> readerClass =
274          classLoader.loadClass("org.eclipse.jface.internal.text.html.HTML2TextReader");
275      Object reader = readerClass.getConstructors()[0].newInstance(htmlStringReader, null);
276      String text = (String) ReflectionUtils.invokeMethod(reader, "getString()");
277      return text.length();
278    } catch (Throwable e) {
279      return html.length();
280    }
281  }
282
283  /**
284   * Returns a string representation of {@link Color} suitable for web pages.
285   *
286   * @param color
287   *          the {@link Color} instance, not <code>null</code>.
288   * @return a string representation of {@link Color} suitable for web pages.
289   */
290  private static String getColorWebString(final Color color) {
291    String colorString = "#" + Integer.toHexString(color.getRed());
292    colorString += Integer.toHexString(color.getGreen());
293    colorString += Integer.toHexString(color.getBlue());
294    return colorString;
295  }
296
297  /**
298   * Creates {@link Label} if {@link Browser} can not be used.
299   */
300  private static Control createTooltipControl_Label(Composite parent, String html) {
301    // prepare Label
302    final Label label = new Label(parent, SWT.WRAP);
303    label.setText(html);
304    // set size
305    int requiredWidth = label.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
306    GridDataFactory.create(label).hintHC(50).hintHMin(requiredWidth);
307    // copy colors
308    label.setForeground(parent.getForeground());
309    label.setBackground(parent.getBackground());
310    // done
311    parent.getDisplay().asyncExec(new Runnable() {
312      @Override
313    public void run() {
314        Shell shell = label.getShell();
315        shell.setVisible(true);
316      }
317    });
318    return label;
319  }
320
321  private static void hideTooltip(Control tooltip) {
322    tooltip.getShell().dispose();
323  }
324
325  // Copied from CodeUtils.java: CodeUtils.getSource()
326  /**
327   * @return the source as single {@link String}, lines joined using "\n".
328   */
329  public static String getSource(String... lines) {
330      return Joiner.on('\n').join(lines);
331  }
332}
333