/******************************************************************************* * Copyright (c) 2014 Mentor Graphics and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Mentor Graphics - initial API and implementation *******************************************************************************/ package com.codesourcery.installer.ui; import org.eclipse.core.runtime.ListenerList; import org.eclipse.swt.SWT; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseTrackAdapter; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.TextLayout; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; /** * A button that supports an image, label text, and/or wrapped description * text. */ public class InfoButton extends Canvas { /** Text draw flags */ private static int DRAW_FLAGS = SWT.DRAW_MNEMONIC | SWT.DRAW_TAB | SWT.DRAW_TRANSPARENT | SWT.DRAW_DELIMITER; /** Shortened text replacement */ private static final String ELLIPSIS = " ... "; //$NON-NLS-1$ /** Element colors */ public enum ElementColor { /** Color of label text */ label, /** Color of label text when label is selected */ selectedLabel, /** Color of description text */ description, /** Color of description text when label is selected */ selectedDescription, /** Hover background color */ hoverBackground, /** Select background color */ selectBackground } /** Size of rectangle rounding */ private static final int ROUND_SIZE = 6; /** Focus line dash style */ private int[] FOCUS_LINE_STYLE = new int[] {1, 1}; /** Label image */ private Image image; /** Disabled image */ private Image disabledImage; /** Label text */ private String text; /** Label description */ private String description; /** Text layout */ private TextLayout textLayout; /** Margin between image and label text */ private int textMargin = 10; /** Horizontal margin */ private int itemHorizontalMargin = 4; /** Vertical margin */ private int itemVerticalMargin = 4; /** <code>true</code> if item is currently tracking */ private boolean tracking = false; /** Element colors */ private Color[] colors = new Color[ElementColor.values().length]; /** Label font */ private Font labelFont; /** Description font */ private Font descriptionFont; /** <code>true</code> if selected */ private boolean selected; /** <code>true</code> if has focus */ private boolean hasFocus = false; /** Selection listeners */ private ListenerList selectionListeners; /** <code>true</code> to draw rounded selection */ private boolean rounded = false; /** <code>true</code> if button label should be shortened */ private boolean shortenText = false; /** * Constructor * * @param parent Parent composite * @param style Style flags * @param image Image or <code>null</code> * @param text Text or <code>null</code> * @param description Description or <code>null</code> */ public InfoButton(Composite parent, int style, Image image, String text, String description) { this(parent, style); setImage(image); setText(text); setDescription(description); } /** * Constructor * * The following style flags are supported: * SWT.TOGGLE, SWT.RADIO, SWT.NO_FOCUS, SWT.READ_ONLY * * @param parent Parent composite * @param style Style flags */ public InfoButton(Composite parent, int style) { super(parent, style); // Create text layout textLayout = new TextLayout(getShell().getDisplay()); // Initialize default colors initDefaultColors(); // Add paint listener addPaintListener(new PaintListener() { @Override public void paintControl(PaintEvent e) { onPaint(e); } }); // Add mouse listener addMouseListener(new MouseAdapter() { @Override public void mouseDown(MouseEvent e) { select(true); } @Override public void mouseUp(MouseEvent e) { select(false); } }); // Add mouse track listener addMouseTrackListener(new MouseTrackAdapter() { @Override public void mouseEnter(MouseEvent e) { tracking = true; if (canUpdate()) { redraw(); } } @Override public void mouseExit(MouseEvent e) { tracking = false; if (canUpdate()) { redraw(); } } }); // Add focus listener addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { if (canUpdate()) { if (!tracking && ((getStyle() & SWT.NO_FOCUS) != SWT.NO_FOCUS)) { if (!getSelection()) { hasFocus = true; redraw(); } } } } @Override public void focusLost(FocusEvent e) { if (canUpdate()) { if ((getStyle() & SWT.NO_FOCUS) != SWT.NO_FOCUS) { hasFocus = false; redraw(); } } } }); // Handle focus traversal addTraverseListener(new TraverseListener() { @Override public void keyTraversed(TraverseEvent e) { e.doit = true; } }); // Add key listener addKeyListener(new KeyListener() { @Override public void keyPressed(KeyEvent e) { if (canUpdate()) { // Arrow down - increase the focus item if (e.keyCode == SWT.ARROW_DOWN) { selectNext(true); } // Arrow up - decrease the focus item else if (e.keyCode == SWT.ARROW_UP) { selectNext(false); } // Space - select item else if (e.keyCode == SWT.SPACE) { select(true); } } e.doit = true; } @Override public void keyReleased(KeyEvent e) { if (e.keyCode == SWT.SPACE){ select(false); } e.doit = true; } }); // Set label & description font setFont(parent.getFont()); } /** * Selects the next or previous button in a radio group. * * @param next <code>true</code> to select next button, <code>false</code> to * select previous button */ private void selectNext(boolean next) { Control[] children = getParent().getChildren(); for (int index = 0; index < children.length; index ++) { if (this == children[index]) { InfoButton nextButton = null; if (next) { if ((index + 1 < children.length) && (children[index] instanceof InfoButton)) { nextButton = (InfoButton)children[index + 1]; } } else { if ((index - 1 >= 0) && (children[index - 1] instanceof InfoButton)) { nextButton = (InfoButton)children[index - 1]; } } if ((nextButton != null) && ((getStyle() & SWT.RADIO) == SWT.RADIO)) { nextButton.setFocus(); nextButton.select(true); break; } } } } @Override public void dispose() { if (textLayout != null) { textLayout.dispose(); textLayout = null; } if (disabledImage != null) { disabledImage.dispose(); disabledImage = null; } super.dispose(); } /** * Returns if the button can update. * * @return <code>true</code> if button is can update */ private boolean canUpdate() { return (isEnabled() && ((getStyle() & SWT.READ_ONLY) != SWT.READ_ONLY)); } /** * Initializes default colors */ private void initDefaultColors() { setColor(ElementColor.label, getShell().getDisplay().getSystemColor(SWT.COLOR_LIST_FOREGROUND)); setColor(ElementColor.selectedLabel, getShell().getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT)); setColor(ElementColor.description, getShell().getDisplay().getSystemColor(SWT.COLOR_LIST_FOREGROUND)); setColor(ElementColor.selectedDescription, getShell().getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT)); setColor(ElementColor.hoverBackground, getShell().getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION)); setColor(ElementColor.selectBackground, getShell().getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION)); } /** * Sets if the label text should be shortened if it will not fit in the * button width. The middle of the text will be replaced with "..." to * fit the width. * * @param shortenText <code>true</code> to shorten text */ public void setShortenText(boolean shortenText) { this.shortenText = shortenText; } /** * Returns if the label text should be shortened. * * @return <code>true</code> if text will be shortened */ public boolean getShortenText() { return shortenText; } /** * Sets rounded highlight. * * @param rounded <code>true</code> to draw rounded highlight */ public void setRounded(boolean rounded) { this.rounded = rounded; } /** * Gets rounded highlight. * * @return <code>true</code> to draw rounded highlight */ public boolean getRounded() { return rounded; } /** * Sets the label and description font. * * @param font Font */ public void setFont(Font font) { super.setFont(font); this.labelFont = font; this.descriptionFont = font; redraw(); } /** * Sets the label font. * * @param font Label font */ public void setLabelFont(Font font) { this.labelFont = font; redraw(); } /** * Returns the label font. * * @return Label font */ public Font getLabelFont() { return labelFont; } /** * Sets the description font. * * @param font Description font */ public void setDescriptionFont(Font font) { this.descriptionFont = font; redraw(); } /** * Returns the description font. * * @return Description font */ public Font getDescriptionFont() { return descriptionFont; } /** * Sets an element color. * * @param element Element * @param color Color */ public void setColor(ElementColor element, Color color) { colors[element.ordinal()] = color; redraw(); } /** * Returns an element color. * * @param element Element * @return Color */ public Color getColor(ElementColor element) { return colors[element.ordinal()]; } /** * Sets the image. * * @param image Image or <code>null</code> */ public void setImage(Image image) { this.image = image; if (disabledImage != null) { disabledImage.dispose(); } disabledImage = (image != null) ? new Image(getShell().getDisplay(), image, SWT.IMAGE_GRAY) : null; redraw(); } /** * Returns the image. * * @return Image or <code>null</code> */ public Image getImage() { return image; } /** * Sets the text. * * @param text Text or <code>null</code> */ public void setText(String text) { this.text = text; redraw(); } /** * Returns the text. * * @return Text or <code>null</code> */ public String getText() { return text; } /** * Sets the description. * * @param description Description or <code>null</code> */ public void setDescription(String description) { this.description = description; redraw(); } /** * Returns the description. * * @return Description or <code>null</code> */ public String getDescription() { return description; } /** * Sets the margin between the image and text. * * @param textMargin Text margin */ public void setTextMargin(int textMargin) { this.textMargin = textMargin; redraw(); } /** * Returns the margin between the image and text. * * @return Text margin */ public int getTextMargin() { return textMargin; } /** * Sets the horizontal margin. * * @param itemHorizontalMargin Horizontal margin */ public void setHorizontalMargin(int itemHorizontalMargin) { this.itemHorizontalMargin = itemHorizontalMargin; redraw(); } /** * Returns the horizontal margin. * * @return Horizontal margin */ public int getHorizontalMargin() { return itemHorizontalMargin; } /** * Sets the vertical margin. * * @param itemVerticalMargin Vertical margin */ public void setVerticalMargin(int itemVerticalMargin) { this.itemVerticalMargin = itemVerticalMargin; redraw(); } /** * Returns the vertical margin. * * @return Vertical margin */ public int getVerticalMargin() { return itemVerticalMargin; } /** * Sets the selection. * * @param selected <code>true</code> if selected */ public void setSelection(boolean selected) { this.selected = selected; redraw(); } /** * Returns the selection. * * @return <code>true</code> if selected */ public boolean getSelection() { return selected; } /** * Adds a new listener to selection events. * If the listener is already added, this method does nothing. * * @param listener Listener to add */ public void addSelectionListener(SelectionListener listener) { if (selectionListeners == null) selectionListeners = new ListenerList(); selectionListeners.add(listener); } /** * Removes a listener from selection events. * * @param listener Listener to remove */ public void removeSelectionListener(SelectionListener listener) { if (selectionListeners != null) selectionListeners.remove(listener); } /** * Selects the button if it is enabled and does not have the SWT.READ_ONLY * flag set. * If the button has SWT.TOGGLE flag set, it's selection will be toggled. * If the button has SWT.RADIO flag set, it will be selected and all other * radio buttons in the parent will be unselected. * * @param down <code>true</code> if mouse or key is currently down */ protected void select(boolean down) { if (canUpdate()) { // Toggle if ((getStyle() & SWT.TOGGLE) == SWT.TOGGLE) { if (down) { setSelection(!getSelection()); notifySelectionListeners(); } } // Radio else if ((getStyle() & SWT.RADIO) == SWT.RADIO) { if (down && !getSelection()) { setSelection(true); Control[] siblings = getParent().getChildren(); for (Control sibling : siblings) { if ((sibling instanceof InfoButton) && !sibling.equals(this)) { InfoButton otherButton = (InfoButton)sibling; if ((otherButton.getStyle() & SWT.RADIO) == SWT.RADIO) { otherButton.setSelection(false); } } } notifySelectionListeners(); } } // Normal else { setSelection(down); if (down) notifySelectionListeners(); } } } /** * Notifies selection listeners. */ private void notifySelectionListeners() { if (selectionListeners != null) { Event event = new Event(); event.widget = this; SelectionEvent selectionEvent = new SelectionEvent(event); Object[] listeners = selectionListeners.getListeners(); for (Object listener : listeners) { try { ((SelectionListener)listener).widgetSelected(selectionEvent); } catch (Exception e) { e.printStackTrace(); } } } } /** * Computes the size of the label. * * <vertical margin> * -------------------------------------------------------------------------------------- * |<horizontal margin> | <image> | <text margin> | <text> | <horizontal margin> | * | | | | <description> | | * -------------------------------------------------------------------------------------- * <vertical margin> */ @Override public Point computeSize(int wHint, int hHint, boolean changed) { GC gc = new GC(this); // Computed width of label int width = getHorizontalMargin(); // Computed height of label int height = 0; // Image size if (getImage() != null) { // Width of image plus margin between image and text width += getImage().getImageData().width + getTextMargin(); // Height of image height = getImage().getImageData().height; } // Text size Point textSize = new Point(0, 0); if (getText() != null) { gc.setFont(getLabelFont()); textSize = gc.textExtent(getText()); } // Description size if (getDescription() != null) { gc.setFont(getDescriptionFont()); textLayout.setFont(getDescriptionFont()); Point descriptionSize = gc.textExtent(getDescription().replace('\n', ' ')); textLayout.setText(getDescription()); int descriptionWidth; // If default width then use extents of description if (wHint == SWT.DEFAULT){ descriptionWidth = descriptionSize.x; } // Else use remaining width else { descriptionWidth = wHint - width; if (descriptionWidth <= 0) descriptionWidth = 100; } // If wrapped description with is greater than the text width then // use it for the total text width if (descriptionWidth > textSize.x) textSize.x = descriptionWidth; textLayout.setWidth(descriptionWidth); // Add description height textSize.y += textLayout.getLineCount() * descriptionSize.y; } // Include total text width width += textSize.x; // If text size is greater than image size then use it for height if (textSize.y > height) height = textSize.y; // Include vertical margins in height height += getVerticalMargin() + getVerticalMargin(); // Include right horizontal margin in width width += getHorizontalMargin(); gc.dispose(); Point size = new Point(width, (hHint == SWT.DEFAULT) ? height : hHint); return size; } /** * Called to paint the label. * * @param event Paint event */ protected void onPaint(PaintEvent event) { Rectangle clientArea = getClientArea(); GC gc = event.gc; boolean selectForeground = tracking || getSelection(); // Default background gc.setBackground(getBackground()); if (canUpdate()) { // Selected background if (getSelection()) { gc.setBackground(getColor(ElementColor.selectBackground)); } // Hover background else if (tracking) { gc.setBackground(getColor(ElementColor.hoverBackground)); } } // Paint background if (getRounded()) gc.fillRoundRectangle(clientArea.x, clientArea.y, clientArea.width, clientArea.height, ROUND_SIZE, ROUND_SIZE); else gc.fillRectangle(clientArea); // Compute label size Point size = computeSize(clientArea.width, SWT.DEFAULT); int yOffset = getVerticalMargin(); int xOffset = getHorizontalMargin(); // Draw label image Image itemImage = getImage(); if (itemImage != null) { if (!isEnabled()) { itemImage = disabledImage; } gc.drawImage(itemImage, xOffset, yOffset + (size.y - getVerticalMargin()) / 2 - itemImage.getImageData().height / 2); xOffset = itemImage.getImageData().width + getTextMargin(); } // Compute text height int textHeight = 0; if (getText() != null) { gc.setFont(getLabelFont()); textHeight = gc.textExtent(getText()).y; } // Compute description height int descriptionHeight = 0; if (getDescription() != null) { gc.setFont(getDescriptionFont()); textLayout.setFont(getDescriptionFont()); textLayout.setText(getDescription()); textLayout.setWidth(clientArea.width - xOffset); Point descriptionSize = gc.textExtent(getDescription().replace('\n', ' ')); descriptionHeight = textLayout.getLineCount() * descriptionSize.y; } // Compute centered text vertical offset int yTextOffset = yOffset + size.y / 2 - textHeight / 2 - descriptionHeight / 2 - getVerticalMargin(); // Draw label text if (getText() != null) { gc.setFont(getLabelFont()); gc.setForeground(selectForeground ? getColor(ElementColor.selectedLabel) : getColor(ElementColor.label)); String labelText = getText(); if (getShortenText()) { Point sz = gc.textExtent(labelText); if (sz.x > clientArea.width - xOffset) labelText = shortenText(gc, labelText, clientArea.width - xOffset); } gc.drawText(labelText, xOffset, yTextOffset, DRAW_FLAGS); yTextOffset += textHeight; } // Draw label description if (getDescription() != null) { gc.setForeground(selectForeground ? getColor(ElementColor.selectedDescription) : getColor(ElementColor.description)); textLayout.draw(gc, xOffset, yTextOffset); } // Draw focus if (hasFocus && canUpdate()) { gc.setLineDash(FOCUS_LINE_STYLE); gc.setForeground(getSelection() ? getColor(ElementColor.selectedLabel) : getColor(ElementColor.label)); if (getRounded()) gc.drawRectangle(clientArea.x, clientArea.y, clientArea.width - 1, clientArea.height - 1); else gc.drawRoundRectangle(clientArea.x, clientArea.y, clientArea.width - 1, clientArea.height - 1, ROUND_SIZE, ROUND_SIZE); } } /** * Shorten the given text <code>t</code> so that its length doesn't exceed * the given width. The default implementation replaces characters in the * center of the original string with an ellipsis ("..."). * Override if you need a different strategy. * * @param gc the gc to use for text measurement * @param t the text to shorten * @param width the width to shorten the text to, in pixels * @return the shortened text * Note, this code was copied from * org.eclipse.swt.custom.CLabel.shortenText() */ protected String shortenText(GC gc, String t, int width) { if (t == null) return null; int w = gc.textExtent(ELLIPSIS, DRAW_FLAGS).x; if (width<=w) return t; int l = t.length(); int max = l/2; int min = 0; int mid = (max+min)/2 - 1; if (mid <= 0) return t; TextLayout layout = new TextLayout (getDisplay()); layout.setText(t); mid = validateOffset(layout, mid); while (min < mid && mid < max) { String s1 = t.substring(0, mid); String s2 = t.substring(validateOffset(layout, l-mid), l); int l1 = gc.textExtent(s1, DRAW_FLAGS).x; int l2 = gc.textExtent(s2, DRAW_FLAGS).x; if (l1+w+l2 > width) { max = mid; mid = validateOffset(layout, (max+min)/2); } else if (l1+w+l2 < width) { min = mid; mid = validateOffset(layout, (max+min)/2); } else { min = max; } } String result = mid == 0 ? t : t.substring(0, mid) + ELLIPSIS + t.substring(validateOffset(layout, l-mid), l); layout.dispose(); return result; } /** * Validates text layout offset. * * @param layout Layout * @param offset Offset * @return Next offset if it fits, previous offset otherwise * * Note, this code was copied from * org.eclipse.swt.custom.CLabel.validateOffset() */ int validateOffset(TextLayout layout, int offset) { int nextOffset = layout.getNextOffset(offset, SWT.MOVEMENT_CLUSTER); if (nextOffset != offset) return layout.getPreviousOffset(nextOffset, SWT.MOVEMENT_CLUSTER); return offset; } }