Home | History | Annotate | Download | only in table
      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  *******************************************************************************/
     11 package org.eclipse.wb.internal.core.model.property.table;
     12 
     13 import com.google.common.base.Charsets;
     14 import com.google.common.base.Joiner;
     15 
     16 import org.eclipse.swt.SWT;
     17 import org.eclipse.swt.browser.Browser;
     18 import org.eclipse.swt.browser.LocationAdapter;
     19 import org.eclipse.swt.browser.LocationEvent;
     20 import org.eclipse.swt.browser.ProgressAdapter;
     21 import org.eclipse.swt.browser.ProgressEvent;
     22 import org.eclipse.swt.graphics.Color;
     23 import org.eclipse.swt.graphics.Point;
     24 import org.eclipse.swt.widgets.Composite;
     25 import org.eclipse.swt.widgets.Control;
     26 import org.eclipse.swt.widgets.Event;
     27 import org.eclipse.swt.widgets.Label;
     28 import org.eclipse.swt.widgets.Listener;
     29 import org.eclipse.swt.widgets.Shell;
     30 import org.eclipse.ui.PlatformUI;
     31 import org.eclipse.ui.browser.IWebBrowser;
     32 import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
     33 import org.eclipse.wb.draw2d.IColorConstants;
     34 import org.eclipse.wb.internal.core.DesignerPlugin;
     35 import org.eclipse.wb.internal.core.EnvironmentUtils;
     36 import org.eclipse.wb.internal.core.utils.reflect.ReflectionUtils;
     37 import org.eclipse.wb.internal.core.utils.ui.GridDataFactory;
     38 import org.eclipse.wb.internal.core.utils.ui.PixelConverter;
     39 
     40 import java.io.StringReader;
     41 import java.net.URL;
     42 import java.text.MessageFormat;
     43 
     44 /**
     45  * Helper for displaying HTML tooltips.
     46  *
     47  * @author scheglov_ke
     48  * @coverage core.model.property.table
     49  */
     50 public 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