package com.readytalk.swt.widgets.notifications; import com.readytalk.swt.text.painter.TextPainter; import com.readytalk.swt.text.tokenizer.TextTokenizerFactory; import com.readytalk.swt.text.tokenizer.TextTokenizerType; import com.readytalk.swt.util.ColorFactory; import com.readytalk.swt.widgets.CustomElementDataProvider; import com.readytalk.swt.widgets.notifications.BubbleRegistry.BubbleRegistrant; import org.eclipse.swt.SWT; import org.eclipse.swt.accessibility.AccessibleAdapter; import org.eclipse.swt.accessibility.AccessibleEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import java.util.logging.Logger; /** * Instances of this class represent contextual information about a UI element.<br/> * <br/> * Bubble will attempt to always be visible on screen.<br/> * If the default Bubble would appear off-screen, we will calculate a suitable location to appear.<br/> * <br/> * Bubble utilizes the system font, and allows {@link com.readytalk.swt.text.tokenizer.TextTokenizerType#WIKI} style * syntax to customize the appearance of the text in your Bubble. * <br/> * Bubble will also break up lines that would be longer than 400 pixels when drawn.<br/> * You can short-circuit this logic by providing your own line-breaks with <code>\n</code> characters in the text.<br/> * We will never format your text if your provide your own formatting.<br/> */ public class Bubble extends PopOverShell { private static final Logger LOG = Logger.getLogger(Bubble.class.getName()); private static final RGB BORDER_COLOR = new RGB(204, 204, 204); private static final int BORDER_THICKNESS = 1; //pixels static final int MAX_STRING_LENGTH = 400; //pixels private static final RGB TEXT_COLOR = new RGB(204, 204, 204); private static final int TEXT_TOP_AND_BOTTOM_PADDING = 2; //pixels private static final int TEXT_LEFT_AND_RIGHT_PADDING = 5; //pixels private VerticalLocation verticalLocation; private CenteringEdge centeringEdge; private boolean disableAutoHide; private Listener listener; private String tooltipText; private Rectangle borderRectangle; private Point textSize; private Color borderColor; private TextPainter textPainter; /** * Creates and attaches a bubble to a component that implements <code>CustomElementDataProvider</code>. * This is a convenience constructor which assumes you do not want the Bubble text to appear * in a Bold font. * * @param customElementDataProvider The CustomElementDataProvider element that the Bubble provides contextual help about * @param text The text you want to appear in the Bubble * @throws IllegalArgumentException Thrown if the parentControl or text is <code>null</code> */ public static Bubble createBubbleForCustomWidget(CustomElementDataProvider customElementDataProvider, String text, BubbleTag ... tags) throws IllegalArgumentException { return new Bubble(customElementDataProvider.getPaintedElement(), customElementDataProvider, text, tags); } /** * Creates and attaches a Bubble to any SWT Control (or descendant). * This is a convenience constructor which assumes you do not want the Bubble text to appear * in a Bold font. * * @param parentControl The parent element that the Bubble provides contextual help about * @param text The text you want to appear in the Bubble * @throws IllegalArgumentException Thrown if the parentControl or text is <code>null</code> */ public static Bubble createBubble(Control parentControl, String text, BubbleTag ... tags) { return new Bubble(parentControl, null, text, false, tags); } private Bubble(Control parentControl, CustomElementDataProvider customElementDataProvider, String text, BubbleTag ... tags) { this(parentControl, customElementDataProvider, text, false, tags); } private Bubble(Control parentControl, CustomElementDataProvider customElementDataProvider, String text, boolean useBoldFont, BubbleTag ... tags) throws IllegalArgumentException { super(parentControl, customElementDataProvider); if (text == null) { throw new IllegalArgumentException("Bubble text cannot be null."); } // This can be removed once the deprecated constructors are pruned (in addition to the parameter useBoldFont) if (useBoldFont) { text = "\'\'\'" + text + "\'\'\'"; } textPainter = new TextPainter(getPopOverShell()) .setText(text) .setTextColor(TEXT_COLOR) .setTokenizer(TextTokenizerFactory.createTextTokenizer(TextTokenizerType.FORMATTED)) .setPadding(TEXT_TOP_AND_BOTTOM_PADDING, TEXT_TOP_AND_BOTTOM_PADDING, TEXT_LEFT_AND_RIGHT_PADDING, TEXT_LEFT_AND_RIGHT_PADDING); // TextPainter does the calculations to see if we need to break the lines, thus we set the raw string, // do the calculations and then set the text again. If we don't break the String this is a no-op. this.tooltipText = maybeBreakLines(textPainter); textPainter.setText(tooltipText); // Remember to clean up after yourself onDispose. borderColor = ColorFactory.getColor(getDisplay(), BORDER_COLOR); this.verticalLocation = VerticalLocation.BELOW; this.centeringEdge = CenteringEdge.LEFT; attachListeners(); registerBubble(getPoppedOverItem(), tags); } Point getAppropriatePopOverSize() { if (textSize == null) { textSize = getTextExtent(textPainter); } return textSize; } private void registerBubble(PoppedOverItem poppedOverItem, BubbleTag ... tags) { BubbleRegistry bubbleRegistry = BubbleRegistry.getInstance(); if (poppedOverItem.getCustomElementDataProvider() != null) { bubbleRegistry.register(poppedOverItem.getCustomElementDataProvider(), this, tags); } else { bubbleRegistry.register(poppedOverItem.getControl(), this, tags); } } /** * Sets the font height (in px) of the font painted by this Bubble. * @param height * @return {@link Bubble} */ public Bubble setFontHeight(int height) { textPainter.setDefaultFontHeight(height); return this; } /** * Sets the VerticalLocation. VerticalLocation represents the relative visual location * between this bubble and its parent. The default is set to VerticalLocation.BELOW. * * @param verticalLocation */ public Bubble setVerticalLocation(VerticalLocation verticalLocation) { this.verticalLocation = verticalLocation; return this; } /** * Get the default vertical location used for positioning the Bubble relative to the parent; * @return VerticalLocation */ public VerticalLocation getVerticalLocation() { return this.verticalLocation; } /** * Sets the CenteringEdge. CenteringEdge represents the relative visual location * between this bubble and its parent. By default, Bubble is set to center its * left edge on the parent. * * @param centeringEdge */ public Bubble setCenteringEdge(CenteringEdge centeringEdge) { this.centeringEdge = centeringEdge; return this; } /** * Get the default edge used for positioning the Bubble relative to the parent; * @return CenteringEdge */ public CenteringEdge getCenteringEdge() { return this.centeringEdge; } public Bubble setPositionRelativeParent(boolean positionRelativeParent) { super.setPositionRelativeParent(positionRelativeParent); return this; } /** * @see PopOverShell#show() */ public void show() { popOverEdgeCenteredOnParent = centeringEdge; popOverAboveOrBelowParent = verticalLocation; super.show(); } /** * Add tags to a previously created Bubble. <br/> * <br/> * Tags can be shown by calling <code>BubbleRegistry.getInstance.showBubblesByTag(BubbleTag tag)</code> * @param bubbleTags Tags to associate with this Bubble */ public void addTags(BubbleTag ... bubbleTags) { BubbleRegistry.getInstance().addTags(getPoppedOverItem().getControlOrCustomElement(), bubbleTags); } /** * Remove tags from a previously created Bubble. * @param bubbleTags Tags to un-associate with this Bubble */ public void removeTags(BubbleTag ... bubbleTags) { BubbleRegistry bubbleRegistry = BubbleRegistry.getInstance(); BubbleRegistry.BubbleRegistrant registrant = bubbleRegistry.findRegistrant(getPoppedOverItem().getControlOrCustomElement()); BubbleRegistry.getInstance().removeTags(registrant, bubbleTags); } /** * Remove the Bubble from the Registry. */ public void deactivateBubble() { BubbleRegistry.getInstance().unregister(getPoppedOverItem().getControlOrCustomElement()); } /** * Returns a boolean describing whether or not auto-hide functionality is disabled for this Bubble. * @return Whether or not auto-hide functionality is disabled */ protected boolean isDisableAutoHide() { return disableAutoHide; } /** * Tells the Bubble whether or not it should auto-hide when the user mouses off the Bubble'd item. * @param disableAutoHide whether or not auto-hide should be disabled */ protected void setDisableAutoHide(boolean disableAutoHide) { this.disableAutoHide = disableAutoHide; } private void attachListeners() { listener = new Listener() { public void handleEvent(Event event) { switch (event.type) { case SWT.Paint: onPaint(event); break; case SWT.MouseDown: onMouseDown(event); break; case SWT.MouseEnter: BubbleRegistrant registrant = BubbleRegistry.getInstance().findRegistrant(getPoppedOverItem().getControlOrCustomElement()); registrant.dismissBubble(); registrant.bubble.setDisableAutoHide(false); break; default: break; } } }; popOverShell.addListener(SWT.Paint, listener); popOverShell.addListener(SWT.MouseDown, listener); popOverShell.addListener(SWT.MouseEnter, listener); addAccessibilityHooks(parentControl); } void resetWidget() { textSize = null; disableAutoHide = false; } void runBeforeShowPopOverShell() { if (textSize == null) { textSize = getTextExtent(textPainter); } borderRectangle = calculateBorderRectangle(textSize); } void widgetDispose() { deactivateBubble(); } private void onPaint(Event event) { GC gc = event.gc; gc.setForeground(borderColor); gc.setLineWidth(BORDER_THICKNESS); gc.drawRectangle(borderRectangle); textPainter.handlePaint(event.gc); } private void onMouseDown(Event event) { if(!isDisableAutoHide()) { hide(); } } private void addAccessibilityHooks(Control parentControl) { parentControl.getAccessible().addAccessibleListener(new AccessibleAdapter() { public void getHelp(AccessibleEvent e) { e.result = tooltipText; } }); } private Rectangle calculateBorderRectangle(Point textSize) { return new Rectangle(0, 0, textSize.x - BORDER_THICKNESS, textSize.y - BORDER_THICKNESS); } String maybeBreakLines(TextPainter textPainter) { GC gc = new GC(getDisplay()); String returnString; Rectangle size = textPainter.precomputeSize(gc); String rawString = textPainter.getText(); if (size.width > MAX_STRING_LENGTH && !rawString.contains("\n")) { StringBuilder sb = new StringBuilder(); String[] words = rawString.split(" "); int spaceInPixels = gc.textExtent(" ").x; int currentPixelCount = 0; for (String word : words) { int wordPixelWidth = gc.textExtent(word).x; if (currentPixelCount + wordPixelWidth + spaceInPixels < MAX_STRING_LENGTH) { sb.append(word); sb.append(" "); currentPixelCount += wordPixelWidth + spaceInPixels; } else { sb.append("\n"); sb.append(word); sb.append(" "); currentPixelCount = wordPixelWidth + spaceInPixels; } } returnString = sb.toString(); } else { returnString = rawString; } gc.dispose(); return returnString; } private Point getTextExtent(TextPainter textPainter) { GC gc = new GC(getDisplay()); Rectangle textExtent = textPainter.precomputeSize(gc); gc.dispose(); return new Point(textExtent.width, textExtent.height); } }