/******************************************************************************* * 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 org.eclipse.core.runtime.Assert; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CCombo; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Widget; import org.eclipse.riena.ui.ridgets.IMarkableRidget; import org.eclipse.riena.ui.swt.ChoiceComposite; import org.eclipse.riena.ui.swt.CompletionCombo; import org.eclipse.riena.ui.swt.UIConstants; import org.eclipse.riena.ui.swt.utils.SWTBindingPropertyLocator; import org.eclipse.riena.ui.swt.utils.SwtUtilities; /** * Focus listener that also prevents the widget corresponding to this ridget * from getting the UI focus when the ridget is not focusable or output only. * <p> * The algorithm is as follows: * <ul> * <li>if the widget is non-focusable, select the next focusable widget</li> * <li>if the widget is output only, select the next focusable widget</li> * <li>if the widget is output only and was clicked, accept focus</li> * <li>in any other case, accept focus</li> * </ul> * Implementation note: SWT will invoke the focusGained, focusLost methods * before the mouseDown method. * * @see AbstractSWTRidget#setFocusable(boolean). * * @since 3.0 */ public final class FocusManager extends MouseAdapter implements FocusListener { private final AbstractSWTRidget ridget; private boolean clickToFocus; private boolean showComboList; /** * Create a new instance. * * @param ridget * a ridget instance; never null. */ FocusManager(final AbstractSWTRidget ridget) { Assert.isNotNull(ridget); this.ridget = ridget; } /** * Add the required listeners for operation of the focus manager to the * given control. * * @param control * a control instance; never null */ public void addListeners(final Control control) { control.addFocusListener(this); control.addMouseListener(this); } public void focusGained(final FocusEvent e) { updateShowComboListFlag(e.widget); if (isFocusable()) { // trace("## focus gained: %s %d", e.widget, e.widget.hashCode()); ridget.fireFocusGained(); } else { Control target = findFocusTarget((Control) e.widget); if (target != null) { // trace("## %s %d -> %s %d", e.widget, e.widget.hashCode(), target, target.hashCode()); target.setFocus(); } else { // no suitable control found, start searching from the top of the view final Composite topControl = findTopContentComposite((Control) e.widget); target = findFocusTarget(null, topControl); if (target != null) { // trace("## %s %d -> %s %d (from top)", e.widget, e.widget.hashCode(), target, target.hashCode()); target.setFocus(); } } if (target == null) { // trace("!! %s %d -> NO TARGET", e.widget, e.widget.hashCode()); ridget.fireFocusGained(); } } } /** * Detects if the list of the combo box should be shown after a output-only * combo box gained the focus. * * @param widget * the widget that gained the focus */ private void updateShowComboListFlag(final Widget widget) { if (isFocusable()) { showComboList = false; } else { showComboList = (widget instanceof Combo) || (widget instanceof CCombo); } } /** * Shows the list of the combo box after the combo box gained the focus. * * @param widget * the widget that gained the focus */ private void showComboList(final Widget widget) { if (!showComboList) { return; } showComboList = false; if (SwtUtilities.isDisposed(widget)) { return; } if (widget instanceof Combo) { ((Combo) widget).setListVisible(true); } else if (widget instanceof CCombo) { ((CCombo) widget).setListVisible(true); } } /** * Returns: (a) the topmost Composite of the current View in the workarea, * or as a fallback: (b) the topmost Shell. * * @return a Composite instance */ private Composite findTopContentComposite(final Control startControl) { Composite result = null; Composite start = (startControl instanceof Composite) ? (Composite) startControl : startControl.getParent(); while (start != null && result == null) { if (start.getData(UIConstants.DATA_KEY_CONTENT_COMPOSITE) != null) { result = start; } start = start.getParent(); } if (result == null) { result = startControl.getShell(); } return result; } public boolean isClickToFocus() { return clickToFocus; } public void focusLost(final FocusEvent e) { if (isFocusable()) { clickToFocus = false; ridget.fireFocusLost(); } } @Override public void mouseDown(final MouseEvent e) { if (!(e.widget instanceof CCombo)) { handleMouseEvent(e); } } @Override public void mouseUp(final MouseEvent e) { if (e.widget instanceof CCombo) { handleMouseEvent(e); } } /** * This method ensures that the output-only widget gets the focus when the * user clicks the widget with the mouse pointer. * <p> * <i>Note: For combo boxes ensures that the list is visible.</i> * * @param e * an event containing information about the mouse button press */ private void handleMouseEvent(final MouseEvent e) { if (ridget.isFocusable() && ridget.isOutputOnly()) { // trace("## mouse DOWN: %s %d", e.widget, e.widget.hashCode()); clickToFocus = true; showComboList(e.widget); ((Control) e.widget).setFocus(); } } /** * Remove the required listeners for operation of the focus manager from the * given control. * * @param control * a control instance; never null */ public void removeListeners(final Control control) { if (!control.isDisposed()) { control.removeFocusListener(this); control.removeMouseListener(this); } } // helping methods ////////////////// /** * Tests whether the given control can get the focus or cannot. * * @param control * UI control * @return {@code true} if control can get the focus; otherwise * {@code false}. */ private boolean canGetFocus(final Control control) { // skip disabled or hidden if (!control.isEnabled() || !control.isVisible()) { return false; } // skip read-only if (SwtUtilities.hasStyle(control, SWT.READ_ONLY)) { return false; } if (control instanceof Text && !((Text) control).getEditable()) { return false; } if (control instanceof ChoiceComposite && !((ChoiceComposite) control).getEditable()) { return false; } if (control instanceof CompletionCombo && !((CompletionCombo) control).getEditable()) { return false; } // skip IMarkableRidgets that are not focusable or output only final String bindingId = SWTBindingPropertyLocator.getInstance().locateBindingProperty(control); if (bindingId != null) { final Object controlsRidget = ridget.getController().getRidget(bindingId); if (controlsRidget instanceof IMarkableRidget) { final IMarkableRidget markableRidget = (IMarkableRidget) controlsRidget; return markableRidget.isFocusable() && !markableRidget.isOutputOnly(); } // skip IRidgets that are not focusable if (controlsRidget instanceof AbstractSWTRidget) { return isFocusable((AbstractSWTRidget) controlsRidget); } } // skip Composites that have no children that can get focus if (control instanceof Composite) { return findFocusTarget(null, (Composite) control) != null; } return true; } private Control findFocusTarget(final Control startControl) { Control result = null; Control start = startControl; while (start.getParent() != null && result == null) { final Composite parent = start.getParent(); result = findFocusTarget(start, parent); start = parent; } return result; } private Control findFocusTarget(final Control start, final Composite parent) { Control result = null; final Control[] siblings = parent.getTabList(); int myIndex = -1; // find index for control for (int i = 0; myIndex == -1 && i < siblings.length; i++) { if (siblings[i] == start) { myIndex = i; } } // find next possible control for (int i = myIndex + 1; result == null && i < siblings.length; i++) { final Control candidate = siblings[i]; if (canGetFocus(candidate)) { result = candidate; } } return result; } private boolean isFocusable() { return isFocusable(ridget); } private boolean isFocusable(final AbstractSWTRidget ridget) { Assert.isNotNull(ridget); return (ridget.isFocusable() && !ridget.isOutputOnly()) || ridget.getFocusManager().isClickToFocus(); } @SuppressWarnings("unused") private void trace(final String format, final Object... args) { System.out.println(String.format(format, args)); } }