/******************************************************************************* * 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.ridgets.swt; import java.beans.PropertyChangeListener; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.eclipse.core.databinding.BindingException; import org.eclipse.core.runtime.Assert; import org.eclipse.swt.SWT; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Widget; import org.eclipse.riena.core.marker.IMarker; import org.eclipse.riena.core.util.ListenerList; import org.eclipse.riena.internal.ui.ridgets.swt.Activator; import org.eclipse.riena.ui.core.marker.DisabledMarker; import org.eclipse.riena.ui.core.marker.ErrorMarker; import org.eclipse.riena.ui.core.marker.ErrorMessageMarker; import org.eclipse.riena.ui.core.marker.HiddenMarker; import org.eclipse.riena.ui.core.marker.MandatoryMarker; import org.eclipse.riena.ui.core.marker.OutputMarker; import org.eclipse.riena.ui.core.resource.IconSize; import org.eclipse.riena.ui.ridgets.AbstractMarkerSupport; import org.eclipse.riena.ui.ridgets.AbstractRidget; import org.eclipse.riena.ui.ridgets.IActionListener; import org.eclipse.riena.ui.ridgets.IBasicMarkableRidget; import org.eclipse.riena.ui.ridgets.IRidget; import org.eclipse.riena.ui.ridgets.listener.ClickEvent; import org.eclipse.riena.ui.ridgets.listener.IClickListener; import org.eclipse.riena.ui.ridgets.uibinding.IBindingPropertyLocator; import org.eclipse.riena.ui.swt.lnf.LnfManager; import org.eclipse.riena.ui.swt.utils.ImageStore; import org.eclipse.riena.ui.swt.utils.SWTBindingPropertyLocator; import org.eclipse.riena.ui.swt.utils.SwtUtilities; /** * Ridget for an SWT widget. */ public abstract class AbstractSWTWidgetRidget extends AbstractRidget implements IBasicMarkableRidget { private ListenerList<IClickListener> clickListeners; private ListenerList<IActionListener> doubleClickListeners; private Widget uiControl; private String toolTip = null; private ErrorMessageMarker errorMarker; private DisabledMarker disabledMarker; private MandatoryMarker mandatoryMarker; private OutputMarker outputMarker; private HiddenMarker hiddenMarker; private AbstractMarkerSupport markerSupport; private Listener visibilityListener; private ClickForwarder mouseListener; /** * Return true if an instance of the given {@code clazz} is a bean, false otherwise. * <p> * Implementation note: currently an instance is assumed to be a bean, if it has an addPropertyListener(PropertyChangeListener) method. * * @param clazz * a non-null class value */ public static boolean isBean(final Class<?> clazz) { try { // next line throws NoSuchMethodException, if no matching method found clazz.getMethod("addPropertyChangeListener", PropertyChangeListener.class); //$NON-NLS-1$ return true; // have bean } catch (final NoSuchMethodException e) { return false; // have pojo } } /** * {@inheritDoc} * <p> * Adding the same marker twice has no effect. */ public synchronized final void addMarker(final IMarker marker) { if (markerSupport == null) { markerSupport = createMarkerSupport(); markerSupport.setRidget(this); unbindMarkerSupport(); } if (marker instanceof MandatoryMarker) { ((MandatoryMarker) marker).setDisabled(isDisableMandatoryMarker()); } markerSupport.addMarker(marker); } /** * @since 4.0 */ public void addClickListener(final IClickListener listener) { Assert.isNotNull(listener, "listener is null"); //$NON-NLS-1$ if (clickListeners == null) { clickListeners = new ListenerList<IClickListener>(IClickListener.class); } clickListeners.add(listener); } /** * @since 4.0 */ public void removeClickListener(final IClickListener listener) { if (clickListeners != null) { clickListeners.remove(listener); } } /** * @since 4.0 */ public void addDoubleClickListener(final IActionListener listener) { Assert.isNotNull(listener, "listener is null"); //$NON-NLS-1$ if (doubleClickListeners == null) { doubleClickListeners = new ListenerList<IActionListener>(IActionListener.class); } doubleClickListeners.add(listener); } /** * @since 4.0 */ public void removeDoubleClickListener(final IActionListener listener) { if (doubleClickListeners != null) { doubleClickListeners.remove(listener); } } public String getID() { return getID(getUIControl()); } /** * Returns the (binding) ID of the given UI control. * * @param uiControl * UI control * @return ID of the given UI control; maybe {@code null}, if the control has no binding ID, is disposed or is not a {@link Widget}. * @since 5.0 */ public String getID(final Object uiControl) { final IBindingPropertyLocator locator = SWTBindingPropertyLocator.getInstance(); return locator.locateBindingProperty(uiControl); } /** * {@inheritDoc} * * @since 3.0 */ public Set<Class<IMarker>> getHiddenMarkerTypes() { if (markerSupport != null) { return markerSupport.getHiddenMarkerTypes(); } return new HashSet<Class<IMarker>>(); } public final Collection<IMarker> getMarkers() { if (markerSupport != null) { return markerSupport.getMarkers(); } return Collections.emptySet(); } public final <T extends IMarker> Collection<T> getMarkersOfType(final Class<T> type) { if (markerSupport != null) { return markerSupport.getMarkersOfType(type); } return Collections.emptySet(); } public final String getToolTipText() { return toolTip; } public Widget getUIControl() { return uiControl; } /** * {@inheritDoc} * * @since 3.0 */ public Set<Class<IMarker>> hideMarkersOfType(final Class<? extends IMarker>... types) { if (markerSupport == null) { markerSupport = createMarkerSupport(); markerSupport.setRidget(this); } return markerSupport.hideMarkersOfType(types); } public boolean hasFocus() { return false; } public boolean isEnabled() { return getMarkersOfType(DisabledMarker.class).isEmpty(); } public final boolean isErrorMarked() { return !getMarkersOfType(ErrorMarker.class).isEmpty(); } public boolean isFocusable() { return false; } /** * FIXME: this method does not check whether the MandatoryMarkers that it finds might have a disabled flag set (in the marker). We also couldnt find code * that calls this method. * * @deprecated */ @Deprecated public final boolean isMandatory() { return !getMarkersOfType(MandatoryMarker.class).isEmpty(); } public final boolean isOutputOnly() { return !getMarkersOfType(OutputMarker.class).isEmpty(); } public final void removeAllMarkers() { if (markerSupport != null) { markerSupport.removeAllMarkers(); } } /** * @since 3.0 */ public final boolean removeMarker(final IMarker marker) { if (markerSupport != null) { return markerSupport.removeMarker(marker); } return false; } public void requestFocus() { // not supported } public synchronized void setEnabled(final boolean enabled) { if (enabled) { if (disabledMarker != null) { removeMarker(disabledMarker); } } else { if (disabledMarker == null) { disabledMarker = new DisabledMarker(); } addMarker(disabledMarker); } } public final void setErrorMarked(final boolean errorMarked) { setErrorMarked(errorMarked, null); } public void setFocusable(final boolean focusable) { // not supported } public final void setMandatory(final boolean mandatory) { if (!mandatory) { if (mandatoryMarker != null) { removeMarker(mandatoryMarker); } } else { if (mandatoryMarker == null) { mandatoryMarker = new MandatoryMarker(); } addMarker(mandatoryMarker); } } public final void setOutputOnly(final boolean outputOnly) { if (!outputOnly) { if (outputMarker != null) { removeMarker(outputMarker); } } else { if (outputMarker == null) { outputMarker = new OutputMarker(); } addMarker(outputMarker); } } public final void setToolTipText(final String toolTipText) { final String oldValue = this.toolTip; this.toolTip = toolTipText; updateToolTip(); firePropertyChange(PROPERTY_TOOLTIP, oldValue, this.toolTip); } /* * Do not override. Template Method Pattern: Subclasses may implement {@code unbindUIControl()} and {@code bindUIControl}, if they need to manipulate the * control when it is bound/unbound, for example to add/remove listeners. */ public void setUIControl(final Object uiControl) { checkUIControl(uiControl); uninstallListeners(); unbindUIControl(); unbindMarkerSupport(); if (!SwtUtilities.isDisposed(this.uiControl)) { // clean up the old UI control data this.uiControl.setData(IRidget.class.getName(), null); } this.uiControl = (Widget) uiControl; if (this.uiControl != null) { // set ridget as UI control data this.uiControl.setData(IRidget.class.getName(), this); } bindMarkerSupport(); updateEnabled(); updateMarkers(); updateToolTip(); bindUIControl(); installListeners(); } public final void setVisible(final boolean visible) { if (hiddenMarker == null) { hiddenMarker = new HiddenMarker(); } if (visible) { removeMarker(hiddenMarker); } else { addMarker(hiddenMarker); } } /** * {@inheritDoc} * * @since 3.0 */ public Set<Class<IMarker>> showMarkersOfType(final Class<? extends IMarker>... types) { if (markerSupport == null) { return new HashSet<Class<IMarker>>(); } return markerSupport.showMarkersOfType(types); } /* * (non-Javadoc) * * @see org.eclipse.riena.ui.ridgets.IBasicMarkableRidget#decorateVisibleControlArea() */ /** * @since 5.0 */ public boolean decorateVisibleControlArea() { return true; } /** * {@inheritDoc} * <p> * The {@code BindingException} will be caught and re-thrown with infos about the binding ID. */ @Override final protected void checkType(final Object uiControl, final Class<?> type) { try { super.checkType(uiControl, type); } catch (final BindingException e) { final String bindingId = getID(uiControl); if (bindingId != null) { final String message = "Unexpected uicontrol has the binding ID " + bindingId; //$NON-NLS-1$ throw new BindingException(message, e); } else { throw e; } } } // abstract methods - subclasses must implement ///////////////////////////////////////////////////////// /** * Performs checks on the control about to be bound by this ridget. * <p> * Implementors must make sure the given <tt>uiControl</tt> has the expected type. * * @param uiControl * a {@link Widget} instance or null * @throws BindingException * if the <tt>uiControl</tt> fails the check */ abstract protected void checkUIControl(Object uiControl); /** * Bind the current <tt>uiControl</tt> to the ridget. * <p> * Implementors must call {@link #getUIControl()} to obtain the current control. If the control is non-null they must do whatever necessary to bind it to * the ridget. */ abstract protected void bindUIControl(); /** * Unbind the current <tt>uiControl</tt> from the ridget. * <p> * Implementors ensure they dispose the control-to-ridget binding and dispose any data structures that are not necessary in an unbound state. */ abstract protected void unbindUIControl(); /** * Returns true if mandatory markers should be disabled (i.e. if the ridget has non empty input). */ // TODO [ev] why is this public? abstract public boolean isDisableMandatoryMarker(); /** * Update the enabled state for the ridget's control, based on the information from {@link #isEnabled()} * <p> * Implementation note: if a ridget has markers, the marker support logic may compute a different enabled state for this widget. The marker support will be * invoked after this method is invoked. * * @see #setUIControl(Object) */ abstract protected void updateEnabled(); /** * Update the tooltip for the ridget's control. */ abstract protected void updateToolTip(); // protected methods //////////////////// /** * Creates and initializes the marker support for this Ridget. * <p> * The Look&Feel returns the marker support according of the Look&Feel setting and the type of this Ridget. If the Look&Feel can not return a appropriate * marker support, this method creates and returns the default marker support. * * @return marker support */ protected AbstractMarkerSupport createMarkerSupport() { AbstractMarkerSupport lnfMarkerSupport = null; if (LnfManager.getLnf() != null) { lnfMarkerSupport = LnfManager.getLnf().getMarkerSupport(this.getClass()); } if (lnfMarkerSupport == null) { // No MarkerSupport exits. Default MarkerSupport is used. lnfMarkerSupport = new MarkerSupport(); } lnfMarkerSupport.init(this, propertyChangeSupport); Assert.isNotNull(lnfMarkerSupport, "Marker support is null!"); //$NON-NLS-1$ return lnfMarkerSupport; } /** * Iterates over the MandatoryMarker instances held by this ridget changing their disabled state to given value. * * @param disable * the new disabled state */ protected final void disableMandatoryMarkers(final boolean disable) { for (final IMarker marker : getMarkersOfType(MandatoryMarker.class)) { final MandatoryMarker mMarker = (MandatoryMarker) marker; mMarker.setDisabled(disable); } } /** * "Flashes" the error marker. * <p> * * Note: this is designed to be delegated to {@link AbstractMarkerSupport} and may vary depending on the concrete implementation. * * @since 3.0 */ protected synchronized final void flash() { if (getUIControl() != null) { if (markerSupport == null) { markerSupport = createMarkerSupport(); markerSupport.setRidget(this); } markerSupport.flash(); } } /** * @since 6.0 */ protected Image getManagedImage(final String key, final IconSize imageSize) { Image image = Activator.getSharedImage(key, imageSize); if (image == null) { image = ImageStore.getInstance().getMissingImage(); } return image; } protected Image getManagedImage(final String key) { return getManagedImage(key, IconSize.NONE); } /** * Compares the two given values. * * @param oldValue * old value * @param newValue * new value * @return true, if value has changed; otherwise false */ protected boolean hasChanged(final Object oldValue, final Object newValue) { if (oldValue == null && newValue == null) { return false; } if (oldValue == null || newValue == null) { return true; } return !oldValue.equals(newValue); } /** * Adds listeners to the <tt>uiControl</tt> after it was bound to the ridget. */ protected void installListeners() { if (getUIControl() != null) { if (visibilityListener == null) { visibilityListener = new VisibilityListener(); } // TODO [ev] can we move this one class down? there it is always a Control instance if (getUIControl() instanceof Control) { addHierarchyVisibilityListener(((Control) getUIControl()).getParent(), visibilityListener); } mouseListener = new ClickForwarder(); if (getUIControl() instanceof Control) { final Control control = (Control) getUIControl(); control.addMouseListener(mouseListener); } } } protected final void setErrorMarked(final boolean errorMarked, final String message) { if (!errorMarked) { if (errorMarker != null) { removeMarker(errorMarker); } } else { if (errorMarker == null) { errorMarker = new ErrorMessageMarker(message); } else { errorMarker.setMessage(message); } addMarker(errorMarker); } } /** * Removes listeners from the <tt>uiControl</tt> when it is about to be unbound from the ridget. */ protected void uninstallListeners() { if (getUIControl() instanceof Control) { final Control control = (Control) getUIControl(); if (visibilityListener != null) { removeHierarchyVisibilityListener(control.getParent(), visibilityListener); } control.removeMouseListener(mouseListener); } } /** * @since 3.0 */ public void updateMarkers() { if (markerSupport != null) { markerSupport.updateMarkers(); } } /** * @since 4.0 */ protected ClickEvent createClickEvent(final MouseEvent e) { return new ClickEvent(this, e.button); } /** * @since 4.0 */ protected MouseListener getClickForwarder() { return mouseListener; } // helping methods ////////////////// private void addHierarchyVisibilityListener(final Composite parent, final Listener listener) { if (parent != null && !parent.isDisposed()) { parent.addListener(SWT.Show, listener); parent.addListener(SWT.Hide, listener); addHierarchyVisibilityListener(parent.getParent(), listener); } } private void bindMarkerSupport() { if (markerSupport != null) { markerSupport.bind(); } } private void removeHierarchyVisibilityListener(final Composite parent, final Listener listener) { if (parent != null && !parent.isDisposed()) { parent.removeListener(SWT.Show, listener); parent.removeListener(SWT.Hide, listener); removeHierarchyVisibilityListener(parent.getParent(), listener); } } private void unbindMarkerSupport() { if (markerSupport != null) { markerSupport.unbind(); } } // helping classes // //////////////// private final class VisibilityListener implements Listener { public void handleEvent(final Event event) { // fire a showing event for Ridgets with markers whose visibility // changes because of a parent widget so that markers can be // updated (bug 261980) fireShowingPropertyChange(); } } private void fireShowingPropertyChange() { final Widget control = getUIControl(); if (markerSupport != null && !getMarkers().isEmpty() && !SwtUtilities.isDisposed(control)) { control.getDisplay().asyncExec(new Runnable() { public void run() { if (!SwtUtilities.isDisposed(control)) { markerSupport.fireShowingPropertyChangeEvent(); } } }); } } /** * Listener of mouse events. */ private final class ClickForwarder implements MouseListener { private int pressedBtn; private boolean doubleClickInProcess = false; public void mouseDoubleClick(final MouseEvent e) { pressedBtn = -1; if (!isEnabled()) { return; } if (doubleClickInProcess) { return; } try { doubleClickInProcess = true; if (doubleClickListeners != null) { for (final IActionListener listener : doubleClickListeners.getListeners()) { listener.callback(); } } } finally { doubleClickInProcess = false; } } public void mouseDown(final MouseEvent e) { pressedBtn = -1; if (!isEnabled()) { return; } final Point point = new Point(e.x, e.y); if (isOverWidget(point)) { pressedBtn = e.button; } } public void mouseUp(final MouseEvent e) { if (!isEnabled()) { pressedBtn = -1; return; } final Point point = new Point(e.x, e.y); if ((e.button == pressedBtn) && (isOverWidget(point))) { fireClickEvent(e); } pressedBtn = -1; } /** * Returns whether the given point is inside or outside the bounds of the widget. * * @param point * position of the mouse pointer * @return {@code true} if point is inside the widget; otherwise {@code false} */ private boolean isOverWidget(final Point point) { if (SwtUtilities.isDisposed(getUIControl())) { return false; } if (getUIControl() instanceof Control) { final Control control = (Control) getUIControl(); final Rectangle widgetBounds = control.getBounds(); return (point.x <= widgetBounds.width && point.x >= 0) && (point.y <= widgetBounds.height && point.y >= 0); } else { return true; } } private void fireClickEvent(final MouseEvent e) { if (clickListeners != null) { final ClickEvent event = createClickEvent(e); for (final IClickListener listener : clickListeners.getListeners()) { listener.callback(event); } } } } /** * @since 6.2 */ public boolean handlesDisabledMarker() { return false; } }