/******************************************************************************* * 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.navigation.ui.swt.views; import java.util.ArrayList; import java.util.Arrays; import java.util.List; 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.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.TypedEvent; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.riena.core.util.Nop; import org.eclipse.riena.core.util.RAPDetector; import org.eclipse.riena.internal.navigation.ui.swt.handlers.SwitchModule; import org.eclipse.riena.navigation.INavigationNode; import org.eclipse.riena.navigation.ISubModuleNode; import org.eclipse.riena.navigation.model.NavigationProcessor; import org.eclipse.riena.navigation.model.SubModuleNode; import org.eclipse.riena.ui.swt.lnf.LnfKeyConstants; import org.eclipse.riena.ui.swt.lnf.LnfManager; import org.eclipse.riena.ui.swt.utils.SwtUtilities; /** * Navigation for the 'submodule' tree used in {@link ModuleView}. This includes both mouse navigation (selection) and keyboard navigation (key down / up). * <p> * This class takes care of the following cases: * <ul> * <li>node clicked / selection change: activate the node</li> * <li>arrow up / down: activate the node</li> * <li>arrow up on the first node: activate the previous module, wrapping around to the last, if there is no previous one</li> * <li>arrow down on the last node: activate the next module, wrapping around to the 1st, if there is no next one</li> * </ul> * <p> * Some submodule nodes can be flagged as non-selecteble. If such a node is activated, then the first selectable child will be selected. This is done by * {@link NavigationProcessor}. Moving over such a node is not problem anymore, since the activation is only triggered after a delay - not instantly. * * @since 3.0 */ public class ModuleNavigationListener extends SelectionAdapter implements KeyListener, FocusListener { /** Keycode for 'arrow down' (16777218) */ private static final int KC_ARROW_DOWN = 16777218; /** Keycode for 'arrow up' (16777217) */ private static final int KC_ARROW_UP = 16777217; private volatile boolean keyPressed; private int keyCode; private NodeSwitcher nodeSwitcher; private boolean isFirst; private boolean isLast; public ModuleNavigationListener(final Tree moduleTree) { moduleTree.addKeyListener(this); moduleTree.addSelectionListener(this); moduleTree.addFocusListener(this); } @Override public void widgetSelected(final SelectionEvent event) { cancelSwitch(); if (!keyPressed) { // selected by mouse click startSwitch(getSelection(event)); } else { // key is down and we must check if the tree node is selectable final Tree tree = (Tree) event.widget; final TreeItem[] sel = tree.getSelection(); if (sel.length > 0) { TreeItem item = sel[0]; if (!isSelectable(item)) { // if item is not selectable find the next selectable item in the same direction if (keyCode == SWT.ARROW_UP || keyCode == SWT.ARROW_LEFT) { item = findPrevious(item); } else if (keyCode == SWT.ARROW_DOWN || keyCode == SWT.ARROW_RIGHT) { item = findNext(item); } if (item != null) { item.getParent().select(item); } // if item == null then the code in keyRelease will expand the // unselectable item and select the first child } } } } public void keyPressed(final KeyEvent event) { cancelSwitch(); blockLetterOrDigit(event); keyPressed = true; keyCode = event.keyCode; final TreeItem from = getSelection(event); isFirst = isFirst(from); isLast = isLast(from); } public void keyReleased(final KeyEvent event) { blockLetterOrDigit(event); // plain arrow up if (event.stateMask == 0 && KC_ARROW_UP == event.keyCode && isFirst) { createSwitchModuleOp().doSwitch(false); // plain arrow down } else if (event.stateMask == 0 && KC_ARROW_DOWN == event.keyCode && isLast) { createSwitchModuleOp().doSwitch(true); } else { startSwitch(getSelection(event)); } keyPressed = false; } private void blockLetterOrDigit(final KeyEvent e) { if (Character.isLetterOrDigit(e.character) && !isCharacterNavigationEnabled()) { e.doit = false; } } private boolean isCharacterNavigationEnabled() { return LnfManager.getLnf().getBooleanSetting(LnfKeyConstants.NAVIGATION_TREE_CHARACTER_SELECTION_ENABLED, false); } public void focusGained(final FocusEvent e) { // unused } public void focusLost(final FocusEvent e) { // reset key pressed state when focus is removed from the tree keyPressed = false; } // helping methods ////////////////// private void cancelSwitch() { if (nodeSwitcher != null) { nodeSwitcher.cancel(); } } private SwitchModule createSwitchModuleOp() { final SwitchModule result = new SwitchModule(); result.setActivateSubmodule(true); return result; } private TreeItem findLast(final Tree tree) { if (tree.isDisposed()) { return null; } final TreeItem[] items = tree.getItems(); return items.length > 0 ? findLast(items[items.length - 1]) : null; } private TreeItem findLast(final TreeItem item) { TreeItem result = item; if (item.getExpanded() && item.getItemCount() > 0) { final TreeItem last = item.getItem(item.getItemCount() - 1); result = findLast(last); } return result; } private static TreeItem findItem(final TreeItem[] items, final INavigationNode<?> source) { for (final TreeItem item : items) { if (item.getData() == source) { return item; } final TreeItem result = item.getItemCount() > 0 ? findItem(item.getItems(), source) : null; if (result != null) { return result; } } return null; } /** * Find the first selectable TreeItem below {@code item} in the tree. Will consider all available (=expanded) tree items. * * @return a TreeItem, may be null */ private TreeItem findNext(final TreeItem item) { final List<TreeItem> siblings = sequentialize(item.getParent().getItems()); final int index = siblings.indexOf(item); TreeItem result = index != -1 && index < siblings.size() - 1 ? siblings.get(index + 1) : null; if (result != null && !isSelectable(result)) { result = findNext(result); } return result; } /** * Find the first selectable TreeItem above {@code item} in the tree. Will consider all available (=expanded) tree items. * * @return a TreeItem, may be null */ private TreeItem findPrevious(final TreeItem item) { final List<TreeItem> siblings = sequentialize(item.getParent().getItems()); final int index = siblings.indexOf(item); TreeItem result = index > 0 ? siblings.get(index - 1) : null; if (result != null && !isSelectable(result)) { result = findPrevious(result); } return result; } private TreeItem getSelection(final TypedEvent event) { final Tree tree = (Tree) event.widget; TreeItem result = null; if (tree.getSelectionCount() > 0) { result = tree.getSelection()[0]; } return result; } private boolean isFirst(final TreeItem item) { boolean result = false; if (item != null) { final Tree tree = item.getParent(); if (tree.getItemCount() > 0) { result = tree.getItem(0) == item; } } return result; } private boolean isLast(final TreeItem item) { if (null == item) { return false; } return item == findLast(item.getParent()); } private boolean isSelectable(final TreeItem item) { boolean result = true; final INavigationNode<?> node = (INavigationNode<?>) item.getData(); if (node instanceof SubModuleNode) { result = isSelectableRoot((SubModuleNode) node); } return result; } /* * return true if the node or one of its child-successors is selectable */ private boolean isSelectableRoot(final ISubModuleNode node) { if (node.isSelectable()) { return true; } final List<ISubModuleNode> children = node.getChildren(); for (final ISubModuleNode child : children) { if (isSelectableRoot(child)) { return true; } } return false; } /** * Do a DFS traversal and return all reachable nodes. */ private List<TreeItem> sequentialize(final TreeItem[] siblings) { final List<TreeItem> stack = new ArrayList<TreeItem>(Arrays.asList(siblings)); final List<TreeItem> result = new ArrayList<TreeItem>(); while (!stack.isEmpty()) { final TreeItem item = stack.remove(0); result.add(item); if (item.getExpanded()) { stack.addAll(0, Arrays.asList(item.getItems())); } } return result; } private void startSwitch(final TreeItem item) { cancelSwitch(); if (item != null) { nodeSwitcher = createNodeSwitcher(item); nodeSwitcher.start(); } } protected NodeSwitcher createNodeSwitcher(final TreeItem item) { return new NodeSwitcher(item); } /** * Activates the given node. * <p> * If the activation fails (maybe the node is not selectable), update the selection inside the tree. * * @param node * navigation node to activated * @param tree * tree of the module */ private static void activateNode(final INavigationNode<?> node, final Tree tree) { node.setContext("fromUI", true); //$NON-NLS-1$ node.activate(); if (!node.isActivated()) { final INavigationNode<?> selectedNode = node.getNavigationProcessor().getSelectedNode(); if (selectedNode != null && !SwtUtilities.isDisposed(tree)) { final TreeItem item = findItem(tree.getItems(), selectedNode); if (item != null) { tree.setSelection(item); } } } } // helping classes ////////////////// /** * Activates a navigation node after a timeout. Can be cancelled. */ protected static class NodeSwitcher extends Thread { /** * Wait this long (ms) before activating a node. */ private static final int TIMEOUT_MS = RAPDetector.isRAPavailable() ? 50 : 300; protected final Display display; private final Tree tree; private final INavigationNode<?> node; private volatile boolean isCancelled; protected NodeSwitcher(final TreeItem item) { this.display = item.getDisplay(); this.tree = item.getParent(); this.node = (INavigationNode<?>) item.getData(); if (node == null) { throw new IllegalStateException("This class can't handle a null node. Currently node is null which is probably an error."); //$NON-NLS-1$ } } /** * @since 5.0 */ protected INavigationNode<?> getNavigationNode() { return node; } @Override public void run() { // if (!node.getParent().isActivated()) { // return; // } try { sleep(TIMEOUT_MS); } catch (final InterruptedException iex) { Nop.reason("ignore"); //$NON-NLS-1$ } if (!isCancelled) { // node activation must be triggered from the UI thread: display.syncExec(new Runnable() { public void run() { activateNode(node, tree); } }); } } /** * Cancels activation of the node, if called before the timeout. */ public void cancel() { isCancelled = true; } } }