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 { createTooltipControl(Composite parent, String header, String details)51 public static Control createTooltipControl(Composite parent, String header, String details) { 52 return createTooltipControl(parent, header, details, 0); 53 } 54 createTooltipControl(Composite parent, String header, String details, int heightLimit)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 */ createTooltipControl_Browser(Composite parent, String html, final int heightLimitChars)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 tweakBrowserSize(Browser browser, int heightLimitChars)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 expandShellToShowFullPage_Height(Browser browser, int maxHeight)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 getContentOffsetWidth(Browser browser)229 private static int getContentOffsetWidth(Browser browser) throws Exception { 230 return evaluateScriptInt( 231 browser, 232 "return document.getElementById('_wbp_tooltip_body').offsetWidth;"); 233 } 234 getContentOffsetHeight(Browser browser)235 private static int getContentOffsetHeight(Browser browser) throws Exception { 236 return evaluateScriptInt( 237 browser, 238 "return document.getElementById('_wbp_tooltip_body').offsetHeight;"); 239 } 240 getContentScrollHeight(Browser browser)241 private static int getContentScrollHeight(Browser browser) throws Exception { 242 return evaluateScriptInt( 243 browser, 244 "return document.getElementById('_wbp_tooltip_body').scrollHeight;"); 245 } 246 getBodyOffsetHeight(Browser browser)247 private static int getBodyOffsetHeight(Browser browser) throws Exception { 248 return evaluateScriptInt(browser, "return document.body.offsetHeight;"); 249 } 250 getBodyScrollHeight(Browser browser)251 private static int getBodyScrollHeight(Browser browser) throws Exception { 252 return evaluateScriptInt(browser, "return document.body.scrollHeight;"); 253 } 254 evaluateScriptInt(Browser browser, String script)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 hideCloseElement(Browser browser)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 */ getTextLength(String html)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 */ getColorWebString(final Color color)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 */ createTooltipControl_Label(Composite parent, String html)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 hideTooltip(Control tooltip)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 */ getSource(String... lines)329 public static String getSource(String... lines) { 330 return Joiner.on('\n').join(lines); 331 } 332 } 333