/* * Copyright (c) 2014, grossmann * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the jo-widgets.org nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL jo-widgets.org BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. */ package org.jowidgets.tools.model.item; import java.lang.Thread.UncaughtExceptionHandler; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.Set; import org.jowidgets.api.command.IAction; import org.jowidgets.api.command.IActionChangeObservable; import org.jowidgets.api.command.IExceptionHandler; import org.jowidgets.api.command.IExecutionContext; import org.jowidgets.api.controller.IDisposeListener; import org.jowidgets.api.controller.IListenerFactory; import org.jowidgets.api.model.IListModelListener; import org.jowidgets.api.model.item.IActionItemModel; import org.jowidgets.api.model.item.IMenuItemModel; import org.jowidgets.api.model.item.IMenuModel; import org.jowidgets.api.toolkit.Toolkit; import org.jowidgets.api.widgets.IComponent; import org.jowidgets.api.widgets.IContainer; import org.jowidgets.api.widgets.IWidget; import org.jowidgets.common.image.IImageConstant; import org.jowidgets.common.types.Accelerator; import org.jowidgets.common.types.Modifier; import org.jowidgets.common.types.VirtualKey; import org.jowidgets.common.widgets.controller.IKeyEvent; import org.jowidgets.common.widgets.controller.IKeyListener; import org.jowidgets.common.widgets.controller.IKeyObservable; import org.jowidgets.tools.controller.KeyAdapter; import org.jowidgets.tools.controller.KeyObservable; import org.jowidgets.tools.controller.ListModelAdapter; import org.jowidgets.util.Assert; import org.jowidgets.util.ITypedKey; /** * This class helps to bind all actions of a given menu to theirs accelerator keys */ public final class MenuModelKeyBinding { private final Collection<IMenuModel> menus; private final IKeyObservable sourceKeyObservable; private final IWidget sourceWidget; private final IKeyListener keyListener; private final IListModelListener listModelListener; private final Map<Accelerator, IAction> actions; private Runnable actionsMapUpdater; /** * Creates a new binding for a menu and a component that contains the menu. * * If the given component is a container, a recursive key lister will be added, * otherwise a 'normal' key listener will be used for the key binding. * * If the component will be disposed, the binding will be disposed also * * @param menu The menu which actions should be bound * @param component The component which receives the key events */ public MenuModelKeyBinding(final IMenuModel menu, final IComponent component) { this(Collections.singleton(menu), component); } /** * Creates a new binding for a collection of menus and a component that contains the menu. * * If the given component is a container, a recursive key lister will be added, * otherwise a 'normal' key listener will be used for the key binding. * * If the component will be disposed, the binding will be disposed also * * @param menu The menu which actions should be bound * @param component The component which receives the key events */ public MenuModelKeyBinding(final Collection<? extends IMenuModel> menus, final IComponent component) { this(menus, createRecursiveKeyObservable(component), component); //dispose the binding if the component becomes disposed component.addDisposeListener(new IDisposeListener() { @Override public void onDispose() { dispose(); } }); } /** * Creates a new key binding for menu and a given key observable * * @param menu The menu for the key binding * @param sourceKeyObservable The key observable that receives the key events * @param source The source widget that will be used for the execution context */ public MenuModelKeyBinding(final IMenuModel menu, final IKeyObservable sourceKeyObservable, final IWidget source) { this(Collections.singleton(menu), sourceKeyObservable, source); } /** * Creates a new key binding for collection of menus and a given key observable * * @param menu The menu for the key binding * @param sourceKeyObservable The key observable that receives the key events * @param source The source widget that will be used for the execution context */ public MenuModelKeyBinding( final Collection<? extends IMenuModel> menus, final IKeyObservable sourceKeyObservable, final IWidget source) { Assert.paramNotNull(menus, "menus"); Assert.paramNotNull(sourceKeyObservable, "sourceKeyObservable"); Assert.paramNotNull(source, "source"); this.menus = new LinkedList<IMenuModel>(menus); this.sourceKeyObservable = sourceKeyObservable; this.sourceWidget = source; this.actions = new HashMap<Accelerator, IAction>(); this.keyListener = new KeyAdapter() { @Override public void keyPressed(final IKeyEvent event) { final VirtualKey virtualKey = event.getVirtualKey(); if (VirtualKey.UNDEFINED != virtualKey) { final Accelerator pressedAccelerator = getAccelerator(event); if (pressedAccelerator != null) { final IAction action = actions.get(pressedAccelerator); if (action != null && action.isEnabled()) { final IExecutionContext executionContext = new ExecutionContext(action); try { action.execute(executionContext); } catch (final Exception e) { try { final IExceptionHandler exceptionHandler = action.getExceptionHandler(); if (exceptionHandler != null) { exceptionHandler.handleException(executionContext, e); } else { throw e; } } catch (final Exception e1) { final UncaughtExceptionHandler uncaughtExceptionHandler = Thread.currentThread() .getUncaughtExceptionHandler(); if (uncaughtExceptionHandler != null) { uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), e1); } } } } } } } }; this.listModelListener = new ListModelAdapter() { @Override public void afterChildRemoved(final int index) { updateActionsMapLater(); } @Override public void afterChildAdded(final int index) { updateActionsMapLater(); } }; updateActionsMap(); } /** * Binds a collection of menus to a component * * When bound all key events of the component (recursive, if component is a IContainer) will be * checked against the key accelerators of the menu and the actions will be performed if accelerator * matches. * * If the component will be disposed, the binding will be disposed too * * @param menus The menus to bind * @param component The component to bind to * * @return The KeyBinding */ public static MenuModelKeyBinding bind(final Collection<? extends IMenuModel> menus, final IComponent component) { return new MenuModelKeyBinding(menus, component); } /** * Binds a menu to a component * * When bound all key events of the component (recursive, if component is a IContainer) will be * checked against the key accelerators of the menu and the actions will be performed if accelerator * matches. * * If the component will be disposed, the binding will be disposed too * * @param menus The menus to bind * @param component The component to bind to * * @return The KeyBinding */ public static MenuModelKeyBinding bind(final IMenuModel menu, final IComponent component) { return new MenuModelKeyBinding(menu, component); } private static IKeyObservable createRecursiveKeyObservable(final IComponent component) { Assert.paramNotNull(component, "component"); final KeyObservable result = new KeyObservable(); final IKeyListener keyListener = new IKeyListener() { @Override public void keyReleased(final IKeyEvent event) { result.fireKeyReleased(event); } @Override public void keyPressed(final IKeyEvent event) { result.fireKeyPressed(event); } }; if (component instanceof IContainer) { addKeyListenerRecursive((IContainer) component, keyListener); } else { addKeyListener(component, keyListener); } return result; } private static void addKeyListenerRecursive(final IContainer container, final IKeyListener keyListener) { Assert.paramNotNull(container, "container"); Assert.paramNotNull(keyListener, "keyListener"); final IListenerFactory<IKeyListener> listenerFactory = new IListenerFactory<IKeyListener>() { @Override public IKeyListener create(final IComponent component) { return keyListener; } }; container.addKeyListenerRecursive(listenerFactory); container.addDisposeListener(new IDisposeListener() { @Override public void onDispose() { container.removeKeyListenerRecursive(listenerFactory); } }); } private static void addKeyListener(final IComponent component, final IKeyListener keyListener) { Assert.paramNotNull(component, "component"); Assert.paramNotNull(keyListener, "keyListener"); component.addKeyListener(keyListener); component.addDisposeListener(new IDisposeListener() { @Override public void onDispose() { component.removeKeyListener(keyListener); } }); } private static Accelerator getAccelerator(final IKeyEvent event) { final VirtualKey virtualKey = event.getVirtualKey(); final Set<Modifier> modifier = event.getModifier(); if (virtualKey != null && virtualKey != VirtualKey.UNDEFINED) { return new Accelerator(virtualKey, modifier); } else { final Character character = event.getCharacter(); if (character != null) { return new Accelerator(virtualKey, modifier); } } return null; } /** * Adds a menu to the key binding * * @param menu */ public void addMenu(final IMenuModel menu) { menus.add(menu); updateActionsMapLater(); } /** * Disposes the key binding */ public void dispose() { disposeActionsMap(); sourceKeyObservable.removeKeyListener(keyListener); } private void updateActionsMapLater() { if (actionsMapUpdater == null) { actionsMapUpdater = new Runnable() { @Override public void run() { updateActionsMap(); actionsMapUpdater = null; } }; Toolkit.getUiThreadAccess().invokeLater(actionsMapUpdater); } } private void updateActionsMap() { actions.clear(); sourceKeyObservable.removeKeyListener(keyListener); for (final IMenuModel menu : menus) { updateActionsMap(menu); } if (!actions.isEmpty()) { sourceKeyObservable.addKeyListener(keyListener); } } private void updateActionsMap(final IMenuModel menu) { for (final IMenuItemModel item : menu.getChildren()) { if (item instanceof IActionItemModel) { final IAction action = ((IActionItemModel) item).getAction(); if (action != null) { final Accelerator accelerator = action.getAccelerator(); if (accelerator != null) { actions.put(accelerator, action); } } else { final Accelerator accelerator = item.getAccelerator(); if (accelerator != null) { actions.put(accelerator, new ActionItemAction((IActionItemModel) item)); } } } else if (item instanceof IMenuModel) { updateActionsMap((IMenuModel) item); } } menu.addListModelListener(listModelListener); } private void disposeActionsMap() { actions.clear(); for (final IMenuModel menu : menus) { disposeActionsMap(menu); } } private void disposeActionsMap(final IMenuModel menu) { menu.removeListModelListener(listModelListener); for (final IMenuItemModel item : menu.getChildren()) { if (item instanceof IMenuModel) { disposeActionsMap((IMenuModel) item); } } } private final class ExecutionContext implements IExecutionContext { private final IAction action; private ExecutionContext(final IAction action) { Assert.paramNotNull(action, "action"); this.action = action; } @Override public <VALUE_TYPE> VALUE_TYPE getValue(final ITypedKey<VALUE_TYPE> key) { return null; } @Override public IAction getAction() { return action; } @Override public IWidget getSource() { return sourceWidget; } } private final class ActionItemAction implements IAction { private final IActionItemModel item; ActionItemAction(final IActionItemModel item) { Assert.paramNotNull(item, "item"); this.item = item; } @Override public String getText() { return item.getText(); } @Override public String getToolTipText() { return item.getToolTipText(); } @Override public IImageConstant getIcon() { return item.getIcon(); } @Override public Character getMnemonic() { return item.getMnemonic(); } @Override public Accelerator getAccelerator() { return item.getAccelerator(); } @Override public boolean isEnabled() { return item.isEnabled(); } @Override public void execute(final IExecutionContext actionEvent) throws Exception { item.actionPerformed(); } @Override public IExceptionHandler getExceptionHandler() { return null; } @Override public IActionChangeObservable getActionChangeObservable() { return null; } } }