/******************************************************************************* * Copyright (c) 2007, 2014 compeople AG 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: * compeople AG - initial API and implementation *******************************************************************************/ package org.eclipse.riena.ui.swt; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.eclipse.core.runtime.Assert; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; 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.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.MouseTrackListener; 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.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.TypedListener; import org.eclipse.riena.ui.swt.facades.SWTFacade; import org.eclipse.riena.ui.swt.utils.SwtUtilities; /** * A button with only an image. (No (button) border, no text). If the button has the style {@code SWT.HOT}, the button has a border and a background like other * SWT buttons if the mouse pointer is over the button (hot/hover). * <p> * The button can have different image for different button states (e.g. pressed or disabled). * * @since 2.0 * */ public class ImageButton extends Composite { private static final Point DEF_HORIZONTAL_MARGIN = new Point(0, 0); private static final Point DEF_HOVER_BUTTON_HORIZONTAL_MARGIN = new Point(12, 12); private static final int IMAGE_INDEX = 0; private static final int PRESSED_IMAGE_INDEX = 1; // p private static final int FOCUSED_IMAGE_INDEX = 2; // f private static final int DISABLED_IMAGE_INDEX = 3; // d private static final int HOVER_IMAGE_INDEX = 4; // h private static final int HOVER_FOCUSED_IMAGE_INDEX = 5; // hp private static int idealHeight = -1; private final Image[] images = { null, null, null, null, null, null }; private boolean useIdealHeight; private boolean pressed; private boolean hover; private boolean focused; private DisposeListener disposeListener; private PaintListener paintListener; private ButtonMouseListener mouseListener; private FocusListener focusListener; private TraverseListener traverseListener; private ButtonKeyListener keyListener; private Button hoverButton; private Point horizontalMargin; private List<SelectionListener> selectionListeners; /** * Creates a new instance of {@code ImageButton}, initializes the button states and adds listeners. * * @param parent * a widget which will be the parent of the new {@code ImageButton} (cannot be null) * @param style * the style of widget to construct; SWT.HOT adds a button border and buttons background that is only visible if the mouse pointer is over the * {@code ImageButton}. */ public ImageButton(final Composite parent, final int style) { super(parent, style | SWT.DOUBLE_BUFFERED); if (hasHotStyle()) { horizontalMargin = DEF_HOVER_BUTTON_HORIZONTAL_MARGIN; setLayout(new FormLayout()); addHoverButton(); } else { horizontalMargin = DEF_HORIZONTAL_MARGIN; } addListeners(); } /** * Adds the given to the collection of listeners who will be notified when this {@code ImageButton} was selected. * * @param listener * listener to add */ public void addSelectionListener(final SelectionListener listener) { Assert.isNotNull(listener); if (selectionListeners == null) { selectionListeners = new ArrayList<SelectionListener>(); final TypedListener delegate = createSelectionDelegate(); addListener(SWT.Selection, delegate); addListener(SWT.DefaultSelection, delegate); } selectionListeners.add(listener); } /** * Computes the size of this {@code ImageButton} according the size of the image (the maximal widths and height of the images). * * @param wHint * hint for width * @param hHint * hint for height * @param changed * <i><i/> * * @return button size */ @Override public Point computeSize(final int wHint, final int hHint, final boolean changed) { checkWidget(); final Point size = new Point(0, 0); if (isUseIdealHeight()) { size.y = getIdealHeight(); } for (final Image oneImage : images) { if ((oneImage != null) && (!oneImage.isDisposed())) { final Rectangle bounds = oneImage.getBounds(); size.x = Math.max(size.x, bounds.width); size.y = Math.max(size.y, bounds.height); } } if (!SwtUtilities.isDisposed(hoverButton)) { final Point btnSize = hoverButton.computeSize(SWT.DEFAULT, SWT.DEFAULT); if (size.x < btnSize.x) { size.x = btnSize.x; } if (size.y < btnSize.y) { size.y = btnSize.y; } } size.x += horizontalMargin.x; size.x += horizontalMargin.y; if (wHint != SWT.DEFAULT) { size.x = wHint; } if (hHint != SWT.DEFAULT) { size.y = hHint; } return size; } /** * Returns the image of the button, if it is disabled. * * @return disabled image */ public Image getDisabledImage() { return images[DISABLED_IMAGE_INDEX]; } /** * Returns the image of the button, if it has the focus. * * @return focused image */ public Image getFocusedImage() { return images[FOCUSED_IMAGE_INDEX]; } /** * Returns the left and right margin between button border and image. * * @return left and right margin */ public Point getHorizontalMargin() { return new Point(horizontalMargin.x, horizontalMargin.y); } /** * Returns the image of the button, if the mouse pointer is over it and the it has the focus. * * @return hover and focused image */ public Image getHoverFocusedImage() { return images[HOVER_FOCUSED_IMAGE_INDEX]; } /** * Returns the image of the button, if the mouse pointer is over it. * * @return hover image */ public Image getHoverImage() { return images[HOVER_IMAGE_INDEX]; } /** * Returns the standard image of the button. * * @return standard image */ public Image getImage() { return images[IMAGE_INDEX]; } /** * Returns the image of the button, if it is pressed. * * @return pressed image */ public Image getPressedImage() { return images[PRESSED_IMAGE_INDEX]; } /** * Returns whether the ideal height should or shouldn't be used for this {@code ImageButton}. The {@code ImageButton} will have the same height as other * push buttons. * * @return useIdealHight {@code true} use ideal height; otherwise {@code false} */ public boolean isUseIdealHeight() { return useIdealHeight; } /** * Removes the given from the collection of listeners who will be notified when this {@code ImageButton} was selected. * * @param listener * listener to remove */ public void removeSelectionListener(final SelectionListener listener) { if (selectionListeners != null) { selectionListeners.remove(listener); } } /** * {@inheritDoc} */ @Override public void setBackground(final Color color) { super.setBackground(color); if (!SwtUtilities.isDisposed(hoverButton)) { hoverButton.setBackground(color); } } /** * Sets the image of the button, if it is disabled. * * @param image * the image to set */ public void setDisabledImage(final Image image) { images[DISABLED_IMAGE_INDEX] = image; } @Override public void setEnabled(final boolean enabled) { super.setEnabled(enabled); setPressed(false); setFocused(false); updateHoverState(); } /** * Sets the image of the button, if it has the focus. * * @param image * the image to set */ public void setFocusedImage(final Image image) { images[FOCUSED_IMAGE_INDEX] = image; } /** * Sets the left and right margin between button border and image. * * @param horizontalMargin * left and right margin * */ public void setHorizontalMargin(final Point horizontalMargin) { this.horizontalMargin = new Point(horizontalMargin.x, horizontalMargin.y); } /** * Sets the image of the button, if the mouse pointer is over it and the it has the focus. * * @param image * the image to set */ public void setHoverFocusedImage(final Image image) { images[HOVER_FOCUSED_IMAGE_INDEX] = image; } /** * Sets the image of the button, if the mouse pointer is over it. * * @param image * the image to set */ public void setHoverImage(final Image image) { images[HOVER_IMAGE_INDEX] = image; } /** * Sets the standard image of the button * * @param image * the image to set */ public void setImage(final Image image) { if (image != this.images[IMAGE_INDEX]) { images[IMAGE_INDEX] = image; redraw(); } } /** * Sets the image of the button, if it is pressed. * * @param image * the image to set */ public void setPressedImage(final Image image) { images[PRESSED_IMAGE_INDEX] = image; } /** * Sets whether the ideal height should or shouldn't be used for this {@code ImageButton}. The {@code ImageButton} will have the same height as other push * buttons.<br> * * @param useIdealHeight * {@code true} use ideal height; otherwise {@code false} */ public void setUseIdealHeight(final boolean useIdealHeight) { this.useIdealHeight = useIdealHeight; } // helping methods ////////////////// /** * Adds the "hover" button. The hover button is only visible if the mouse pointer is over this UI control. */ private void addHoverButton() { hoverButton = new Button(this, SWT.PUSH); final FormData data = new FormData(); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); data.top = new FormAttachment(0, 0); data.bottom = new FormAttachment(100, 0); hoverButton.setLayoutData(data); hoverButton.setVisible(false); } /** * Adds listeners to this {@code ImageButton} and to the "hover" button (if exists). */ private void addListeners() { final SWTFacade swtFacade = SWTFacade.getDefault(); paintListener = new PaintDelegation(); swtFacade.addPaintListener(this, paintListener); mouseListener = new ButtonMouseListener(); addMouseListener(mouseListener); swtFacade.addMouseTrackListener(this, mouseListener); swtFacade.addMouseMoveListener(this, mouseListener); if (!SwtUtilities.isDisposed(hoverButton)) { hoverButton.addMouseListener(mouseListener); swtFacade.addMouseTrackListener(hoverButton, mouseListener); swtFacade.addMouseMoveListener(hoverButton, mouseListener); } focusListener = new ButtonFocusListener(); addFocusListener(focusListener); keyListener = new ButtonKeyListener(); addKeyListener(keyListener); traverseListener = new TraverseListener() { public void keyTraversed(final TraverseEvent e) { e.doit = true; } }; addTraverseListener(traverseListener); disposeListener = new DisposeListener() { public void widgetDisposed(final DisposeEvent e) { onDispose(e); } }; addDisposeListener(disposeListener); } /** * Computes the position of the image. * * @param event * e an event containing information about the paint * @param image * the image to draw * @return position of image */ private Point computeImagePos(final PaintEvent event, final Image image) { int x = 0; int y = 0; if ((image != null) && (event != null)) { final Rectangle imgBounds = image.getBounds(); x = (event.width - imgBounds.width) / 2; if (x < 0) { x = 0; } y = (event.height - imgBounds.height) / 2; if (y < 0) { y = 0; } if (hasHotStyle() && ((event.height % 2) != 0)) { y++; } } return new Point(x, y); } private TypedListener createSelectionDelegate() { return new TypedListener(new SelectionListener() { public void widgetSelected(final SelectionEvent e) { // copy list of selectionListeners to avoid ConcurrentModificationException when the event // widgetSelected fires a dispose and removes the selectionListener final List<SelectionListener> tempSelListeners = new ArrayList<SelectionListener>(selectionListeners); for (final SelectionListener sl : tempSelListeners) { sl.widgetSelected(e); } } public void widgetDefaultSelected(final SelectionEvent e) { // copy list of selectionListeners to avoid ConcurrentModificationException when the event // widgetSelected fires a dispose and removes the selectionListener final List<SelectionListener> tempSelListeners = new ArrayList<SelectionListener>(selectionListeners); for (final SelectionListener sl : tempSelListeners) { sl.widgetDefaultSelected(e); } } }); } /** * Returns the ideal height of an image button according to the height of a push button. * * @return ideal height */ private int getIdealHeight() { if (idealHeight < 0) { final Button button = new Button(this, SWT.PUSH); idealHeight = button.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; button.dispose(); } return idealHeight; } /** * Returns the image that will be draw according to the current state of the button. * * @return image to draw */ private Image getImageToDraw() { Image imageToDraw = null; if (!isEnabled()) { imageToDraw = getDisabledImage(); if (imageToDraw == null) { imageToDraw = getImage(); } return imageToDraw; } if (isPressed()) { imageToDraw = getPressedImage(); if (imageToDraw == null) { imageToDraw = getImage(); } return imageToDraw; } if (isHover()) { if (isFocused()) { imageToDraw = getHoverFocusedImage(); } if (imageToDraw == null) { imageToDraw = getHoverImage(); } if (imageToDraw == null) { imageToDraw = getImage(); } return imageToDraw; } if (isFocused()) { imageToDraw = getFocusedImage(); } if (imageToDraw == null) { imageToDraw = getImage(); } return imageToDraw; } /** * Returns whether the style of the button has {@code SWT.HOT}. * * @return {@code true} if style has {@code SWT.HOT}; otherwise {@code false} */ private boolean hasHotStyle() { final int style = getStyle(); return (style & SWT.HOT) == SWT.HOT; } /** * Returns whether the button has the focus or hasn't the focus. * * @return {@code true} if the button has the focus; otherwise {@code false} */ private boolean isFocused() { return focused; } /** * Returns whether the mouse pointer is or isn't over the button. * * @return {@code true} if the mouse point is over the button; otherwise {@code false} */ private boolean isHover() { return hover; } /** * Returns whether the button is pressed. * * @return {@code true} if button is pressed; otherwise {@code false} */ private boolean isPressed() { return pressed; } /** * After the widget was disposed all listeners will be removed and the array with the images will be cleared. * * @param event * an event containing information about the dispose */ private void onDispose(final DisposeEvent event) { if (event.widget != this) { return; } removeListeners(); Arrays.fill(images, null); } /** * Paints the image of this {@code ImageButton}. * * @param event * e an event containing information about the paint */ private void onPaint(final PaintEvent event) { updateHoverState(); if (!SwtUtilities.isDisposed(hoverButton) && hoverButton.isVisible()) { return; } final Image image = getImageToDraw(); if (image != null) { final Point pos = computeImagePos(event, image); final GC gc = event.gc; gc.drawImage(image, pos.x, pos.y); } } /** * Removes all listeners form this {@code ImageButton} and from the "hover" button (if exists). */ private void removeListeners() { final SWTFacade swtFacade = SWTFacade.getDefault(); if (disposeListener != null) { removeDisposeListener(disposeListener); disposeListener = null; } if (traverseListener != null) { removeTraverseListener(traverseListener); traverseListener = null; } if (paintListener != null) { swtFacade.removePaintListener(this, paintListener); paintListener = null; } if (focusListener != null) { removeFocusListener(focusListener); focusListener = null; } if (mouseListener != null) { if (!SwtUtilities.isDisposed(hoverButton)) { hoverButton.removeMouseListener(mouseListener); swtFacade.removeMouseTrackListener(hoverButton, mouseListener); swtFacade.removeMouseMoveListener(hoverButton, mouseListener); } removeMouseListener(mouseListener); swtFacade.removeMouseTrackListener(this, mouseListener); swtFacade.removeMouseMoveListener(this, mouseListener); mouseListener = null; } if (keyListener != null) { removeKeyListener(keyListener); keyListener = null; } } /** * Sets whether the button has the focus or hasn't the focus. * * @param focused * {@code true} if the button has the focus; otherwise {@code false} */ private void setFocused(final boolean focused) { if (isFocused() != focused) { this.focused = focused; redraw(); } } /** * Sets whether the mouse pointer is or isn't over the button. * * @param hover * {@code true} if the mouse point is over the button; otherwise {@code false} * */ private void setHover(final boolean hover) { if (this.hover != hover) { this.hover = hover; redraw(); } } /** * Sets whether the button is pressed. * * @param pressed * {@code true} if button is pressed; otherwise {@code false} */ private void setPressed(final boolean pressed) { if (this.pressed != pressed) { this.pressed = pressed; redraw(); } } ///// check for disposed state before redrawing @Override public void redraw() { if (isDisposed()) { return; } super.redraw(); } @Override public void redraw(final int x, final int y, final int width, final int height, final boolean all) { if (isDisposed()) { return; } super.redraw(x, y, width, height, all); } /** * Shows or hides the "hover" button depending in the hover state. */ private void updateHoverButton() { if (!SwtUtilities.isDisposed(hoverButton)) { final boolean visible = isHover() || isPressed(); if (visible != hoverButton.isVisible()) { hoverButton.setVisible(visible); } if (hoverButton.isVisible()) { hoverButton.setImage(getImageToDraw()); } } } /** * Updates the hover state (flag/property {@code hover}). * <p> * The update is necessary if the button (or a parent of the button) was disabled. After the button was disabled a mouse exit will be fired and so the hover * state will be false. After the button was enabled no mouse event will be fired and so the hover state won't be true. Because of this problem at other * situations this method must be called to update the hover state. */ private void updateHoverState() { if (isEnabled()) { final Point mousePoint = getDisplay().getCursorLocation(); setHover(isOverButton(mousePoint)); } else { setHover(false); } } /** * Returns whether the given point is inside or outside the bounds of the button. * * @param pointOnDisplay * position of the mouse pointer relative to the display * @return {@code true} if point is inside the button; otherwise {@code false} */ private boolean isOverButton(final Point pointOnDisplay) { final Point onParent = getParent().toControl(pointOnDisplay); return getBounds().contains(onParent); } // helping classes ////////////////// /** * Registers whether the {@code ImageButton} has or hasn't the focus. */ private final class ButtonFocusListener implements FocusListener { /** * {@inheritDoc} */ public void focusGained(final FocusEvent e) { setFocused(true); updateHoverButton(); } /** * {@inheritDoc} */ public void focusLost(final FocusEvent e) { setFocused(false); updateHoverButton(); } } /** * Presses the button after the space key was pressed and fires a selection event after the space key was released. */ private final class ButtonKeyListener implements KeyListener { /** * {@inheritDoc} */ public void keyPressed(final KeyEvent e) { if (!isEnabled()) { return; } if (!ignore(e)) { setPressed(true); updateHoverButton(); } } /** * {@inheritDoc} */ public void keyReleased(final KeyEvent e) { if (!isEnabled()) { return; } if (!ignore(e)) { if (isPressed()) { final Event event = new Event(); notifyListeners(SWT.Selection, event); } setPressed(false); updateHoverButton(); } } /** * Ignores mouse events if the component is null, not enabled, or the event is not associated with the left mouse button. */ private boolean ignore(final KeyEvent e) { return e.character != ' '; } } /** * Listener of all mouse events. */ private final class ButtonMouseListener implements MouseListener, MouseTrackListener, MouseMoveListener { /** * {@inheritDoc} */ public void mouseDoubleClick(final MouseEvent e) { // do nothing } /** * {@inheritDoc} * <p> * Sets the pressed state of the button. */ public void mouseDown(final MouseEvent e) { if (!isEnabled()) { return; } if (!ignoreMouseButton(e) && !ignoreWidget(e)) { setPressed(true); updateHoverButton(); } } /** * {@inheritDoc} * <p> * Sets the hover state of the button. */ public void mouseEnter(final MouseEvent e) { if (!isEnabled()) { return; } if (!ignoreWidget(e)) { final boolean oldHover = isHover(); setHover(true); if (oldHover != isHover()) { updateHoverButton(); } } } /** * {@inheritDoc} * <p> * Removes the hover state of the button. */ public void mouseExit(final MouseEvent e) { if (!isEnabled()) { return; } final boolean oldHover = isHover(); updateHoverState(); if (!ignoreWidget(e)) { if (isPressed()) { setPressed(false); } setHover(false); if (oldHover != isHover()) { updateHoverButton(); } } } /** * {@inheritDoc} */ public void mouseHover(final MouseEvent e) { // do nothing } /** * {@inheritDoc} * <p> * Sets or removes the hover state of the button according the mouse pointer is over the button. */ public void mouseMove(final MouseEvent e) { if (!isEnabled()) { return; } if (!ignoreWidget(e)) { if ((e.stateMask & SWT.BUTTON_MASK) != 0) { final boolean oldHover = isHover(); Point point = new Point(e.x, e.y); point = toDisplay(point); if (isOverButton(point)) { setPressed(true); } else { setPressed(false); } if (oldHover != isHover()) { updateHoverButton(); } } } } /** * {@inheritDoc} * <p> * Fires a selection event if the button is pressed and the mouse pointer is over the button.<br> * Removes the pressed state of the button. */ public void mouseUp(final MouseEvent e) { if (!isEnabled()) { return; } if (!ignoreMouseButton(e) && !ignoreWidget(e)) { Point point = new Point(e.x, e.y); point = toDisplay(point); if (isPressed() && isOverButton(point)) { final Event event = new Event(); notifyListeners(SWT.Selection, event); } setPressed(false); updateHoverButton(); } } /** * Ignores mouse events if the event is not associated with the left mouse button. * * @param e * mouse event * @return {@code true} ignore event; otherwise {@code false} */ private boolean ignoreMouseButton(final MouseEvent e) { return e.button != 1; } /** * Ignores mouse events if the source widget is "invisible" * * @param e * mouse event * @return {@code true} ignore event; otherwise {@code false} */ private boolean ignoreWidget(final MouseEvent e) { if (!SwtUtilities.isDisposed(hoverButton)) { if (hoverButton.isVisible()) { return e.widget != hoverButton; } else { return e.widget == hoverButton; } } return false; } } /** * This listener paints the {@code ImageButton} after a paint event was fired. */ private final class PaintDelegation implements PaintListener { /** * {@inheritDoc} * <p> * Paints the {@code ImageButton}. */ public void paintControl(final PaintEvent e) { onPaint(e); } } /* * (non-Javadoc) * * @see org.eclipse.swt.widgets.Control#setToolTipText(java.lang.String) */ @Override public void setToolTipText(final String string) { super.setToolTipText(string); if (hasHotStyle()) { if (!SwtUtilities.isDisposed(hoverButton)) { hoverButton.setToolTipText(string); } } } }