/*
* $Id$
* This file is a part of the Arakhne Foundation Classes, http://www.arakhne.org/afc
*
* Copyright (c) 2000-2012 Stephane GALLAND.
* Copyright (c) 2005-10, Multiagent Team, Laboratoire Systemes et Transports,
* Universite de Technologie de Belfort-Montbeliard.
* Copyright (c) 2013-2016 The original authors, and other authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.arakhne.afc.ui.actionmode ;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EventListener;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import org.arakhne.afc.math.continous.object2d.Circle2f;
import org.arakhne.afc.math.continous.object2d.Shape2f;
import org.arakhne.afc.ui.ZoomableContext;
import org.arakhne.afc.ui.event.KeyEvent;
import org.arakhne.afc.ui.event.PointerEvent;
import org.arakhne.afc.ui.selection.Selectable;
import org.arakhne.afc.util.ListenerCollection;
/** ModeManager keeps track of all the
* {@link ActionMode Modes} for a given workspace.
* Events are passed to the Modes for handling. The submodes are
* prioritized according to their order on a stack, i.e., the last
* Mode added gets the first chance to handle an Event.
* <p>
* This ModeManager takes into account an exclusive Mode. An exclusive
* mode is a {@link ActionMode Mode} was received all events before the
* stacked modes. If an event was not eated by the exclusive mode,
* the exclusive mode was destroy and the event was ignored.
*
* @param <DRAW> is the type of the data supported by this container.
* @param <CANVAS> is the type of the drawing canvas.
* @param <COLOR> is the type that is representing a color.
* @author $Author: sgalland$
* @author $Author: hannoun$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
* @deprecated see JavaFX API
*/
@Deprecated
public class ActionModeManager<DRAW extends Selectable, CANVAS, COLOR> {
private final WeakReference<ActionModeManagerOwner<DRAW,CANVAS,COLOR>> container;
private final LinkedList<ActionMode<? super DRAW,CANVAS,COLOR>> modeStack = new LinkedList<>();
private final UUID viewID;
private ActionMode<? super DRAW,CANVAS,COLOR> exclusiveMode = null;
private Shape2f selectionShapeForFigureUnderTheMouse = null;
private WeakReference<DRAW> figureUnderTheMouse = null;
private boolean isForceHitResetWhenRelease = true;
private final ListenerCollection<EventListener> listeners = new ListenerCollection<>();
private ActionPointerEvent lastPointerEvent = null;
/** Construct a ModeManager with no modes.
*
* @param viewID is the identifier of the view associated to this manager.
* @param component is a reference to the component that is
* containing this mode manager.
*/
public ActionModeManager(UUID viewID, ActionModeManagerOwner<DRAW,CANVAS,COLOR> component) {
assert(viewID!=null);
this.viewID = viewID;
assert(component!=null);
this.container = new WeakReference<>(component);
}
/** Replies an unmodifiable list of the modes.
*
* @return the modes.
*/
public List<ActionMode<? super DRAW,CANVAS,COLOR>> getModes() {
return Collections.unmodifiableList(this.modeStack);
}
/** Replies the first mode of the given type from the top
* of the mode stack.
*
* @param type is the type of the mode to retreive.
* @return the mode or <code>null</code> if not found.
*/
public <M extends ActionMode<? super DRAW,CANVAS,COLOR>> M getModeFromTop(Class<M> type) {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.descendingIterator();
ActionMode<? super DRAW,CANVAS,COLOR> mode;
while (iterator.hasNext()) {
mode = iterator.next();
if (type.isInstance(mode))
return type.cast(mode);
}
return null;
}
/** Replies the first mode of the given type from the bottom
* of the mode stack.
*
* @param type is the type of the mode to retreive.
* @return the mode or <code>null</code> if not found.
*/
public <M extends ActionMode<? super DRAW,CANVAS,COLOR>> M getModeFromBottom(Class<M> type) {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.iterator();
ActionMode<? super DRAW,CANVAS,COLOR> mode;
while (iterator.hasNext()) {
mode = iterator.next();
if (type.isInstance(mode))
return type.cast(mode);
}
return null;
}
/** Add listener on the mode events.
*
* @param listener
*/
public void addModeListener(ActionModeListener listener) {
this.listeners.add(ActionModeListener.class, listener);
}
/** Remove listener on the mode events.
*
* @param listener
*/
public void removeModeListener(ActionModeListener listener) {
this.listeners.remove(ActionModeListener.class, listener);
}
/** Add listener on the figure interaction events.
*
* @param listener
*/
public void addSelectableInteractionListener(SelectableInteractionListener listener) {
this.listeners.add(SelectableInteractionListener.class, listener);
}
/** Remove listener on the figure interaction events.
*
* @param listener
*/
public void removeSelectableInteractionListener(SelectableInteractionListener listener) {
this.listeners.remove(SelectableInteractionListener.class, listener);
}
/** Notifies the listeners about the activation of a mode.
*
* @param mode
*/
private void fireModeActivated(ActionMode<?,?,?> mode) {
for(ActionModeListener listener : this.listeners.getListeners(ActionModeListener.class)) {
listener.modeActivated(mode);
}
}
/** Notifies the listeners about the desactivation of a mode.
*
* @param mode
*/
private void fireModeDesactivated(ActionMode<?,?,?> mode) {
for(ActionModeListener listener : this.listeners.getListeners(ActionModeListener.class)) {
listener.modeDesactivated(mode);
}
}
/** Notifies the listeners about the action performing on a figure.
*
* @param figure is the figure on which the action was performed.
*/
public void fireActionPerformed(Collection<? extends DRAW> figure) {
SelectableInteractionEvent event = new SelectableInteractionEvent(this,figure,
getModeManagerOwner().isEditable());
if (figure instanceof SelectableInteractionListener)
((SelectableInteractionListener)figure).actionPerformed(event);
for(SelectableInteractionListener listener : this.listeners.getListeners(SelectableInteractionListener.class)) {
if (figure!=listener) listener.actionPerformed(event);
}
}
/** Notifies the listeners about the action performing on a figure.
*
* @param figure is the figure on which the action was performed.
* @param pointerEvent is the event causing the action performing.
*/
public void fireActionPerformed(Collection<? extends DRAW> figure, ActionPointerEvent pointerEvent) {
SelectableInteractionEvent event = new SelectableInteractionEvent(this, figure, pointerEvent,
getModeManagerOwner().isEditable());
if (figure instanceof SelectableInteractionListener)
((SelectableInteractionListener)figure).actionPerformed(event);
for(SelectableInteractionListener listener : this.listeners.getListeners(SelectableInteractionListener.class)) {
if (figure!=listener) listener.actionPerformed(event);
}
}
/** Notifies the listeners about the action performing on a figure.
*
* @param figure is the figure on which the action was performed.
* @param keyEvent is the event causing the action performing.
*/
public void fireActionPerformed(Collection<? extends DRAW> figure, KeyEvent keyEvent) {
assert(figure!=null);
SelectableInteractionEvent event = new SelectableInteractionEvent(this, figure, keyEvent,
getModeManagerOwner().isEditable());
if (figure instanceof SelectableInteractionListener)
((SelectableInteractionListener)figure).actionPerformed(event);
for(SelectableInteractionListener listener : this.listeners.getListeners(SelectableInteractionListener.class)) {
if (figure!=listener) listener.actionPerformed(event);
}
}
/** Notifies the listeners about the popup action performing on a figure.
*
* @param pointerEvent is the cause of the popup opening.
* @param figure is the figure on which the action was performed. It may be
* <code>null</code> if the popup action was performed on the background.
*/
public void firePopupPerformed(ActionPointerEvent pointerEvent, DRAW figure) {
SelectableInteractionEvent event = new SelectableInteractionEvent(this,
Collections.singleton(figure), pointerEvent,
getModeManagerOwner().isEditable());
if (figure instanceof SelectableInteractionListener)
((SelectableInteractionListener)figure).popupPerformed(event);
for(SelectableInteractionListener listener : this.listeners.getListeners(SelectableInteractionListener.class)) {
if (figure!=listener) listener.popupPerformed(event);
}
}
/** Notifies the listeners about an error that occurs in one
* of the JFigureEditor components.
* This function is a wrapper to the ModeContainer function.
*
* @param error
* @since 16.0
*/
public void fireError(Throwable error) {
getModeManagerOwner().fireError(error);
}
/** Notifies the listeners about the a request to delete a figure and not of the associated
* model object.
*
* @param figure is the figure to delete.
* @param deleteModel indicates if the underlying model object must be also deleted.
* @return <code>true</code> if all the listeners accept the deletion
* of the figure; <code>false</code> if at least one listener refuses
* the deletion of the figure.
*/
public boolean fireFigureDeletionPerformed(DRAW figure, boolean deleteModel) {
boolean auth = true;
for(SelectableInteractionListener listener : this.listeners.getListeners(SelectableInteractionListener.class)) {
if (!listener.figureDeletionPerformed(figure, deleteModel)) {
auth = false;
}
}
return auth;
}
/** Replies the identifier of the view associated to this mode manager.
*
* @return the identifier of the view.
*/
public UUID getViewID() {
return this.viewID;
}
/** Replies the component that is containing this mode manager.
*
* @return the component.
*/
public ActionModeManagerOwner<DRAW,CANVAS,COLOR> getModeManagerOwner() {
return this.container.get();
}
/** Paint each mode in the stack: bottom to top.
*
* @param g the graphic context.
*/
public void paint(CANVAS g) {
if (this.exclusiveMode!=null) {
this.exclusiveMode.paint(g);
}
else {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.iterator();
while (iterator.hasNext()) {
iterator.next().paint(g);
}
}
}
/** Reply if the given {@link ActionMode Mode} was the exclusive one.
*
* @param mode
* @return <code>true</code> if <var>mode</var> was the exclusive
* mode, otherwise <code>false</code>.
*/
public boolean isExclusiveMode( ActionMode<? super DRAW,CANVAS,COLOR> mode ) {
return this.exclusiveMode!=null && this.exclusiveMode==mode;
}
/** Reply if this ModeManager have an exclusive Mode.
*
* @return <code>true</code> if this ModeManager have an
* exclusive mode, otherwise <code>false</code>.
*/
public boolean isExclusiveMode() {
return this.exclusiveMode != null;
}
/** Set or unset exclusive Mode for the given Mode.
* <p>
* If <var>exclu</var> was <code>true</code> then
* <var>mode</var> is the new exclusive mode <b>only if</b>
* it is already the exclusive mode or there are no
* exclusive mode.
* <p>
* If <var>exclu</var> was <code>false</code> then
* the current exclusive mode was unset <b>only if</b>
* <var>mode</var> was the current exclusive mode.
*
* @param mode a reference to the new/old exclusive mode.
* @param isExclusive <code>true</code> if you would like to set
* <var>mode</var> as an exclusive mode,
* otherwise <code>false</code>.
*/
void setExclusiveMode( ActionMode<? super DRAW,CANVAS,COLOR> mode, boolean isExclusive ) {
if ( isExclusive ) {
if ( this.exclusiveMode != null ) {
this.exclusiveMode.cleanMode();
}
this.exclusiveMode = mode;
}
else if ( this.exclusiveMode == mode ) {
unsetExclusiveMode();
}
}
/** Unset exclusive Mode.
*/
void unsetExclusiveMode() {
this.exclusiveMode = null;
}
/** Set the editor's current mode to the given Mode instance.
*
* @param mode the mode to push to the stack.
* @return <code>true</code> if the mode was started; otherwise <code>false</code>.
*/
public boolean beginMode(ActionMode<DRAW,CANVAS,COLOR> mode) {
if (this.modeStack.contains(mode)) return false;
mode.setModeManager(this) ;
this.modeStack.addLast(mode) ;
mode.onModeActivated();
fireModeActivated(mode);
return true;
}
/** Remove the current mode, and set the graph editor's mode to
* the most topless mode.
*/
void endMode() {
if (!this.modeStack.isEmpty()) {
ActionMode<? super DRAW,CANVAS,COLOR> mode = this.modeStack.removeLast();
if ( isExclusiveMode( mode ) ) {
unsetExclusiveMode();
}
mode.onModeDesactivated();
fireModeDesactivated(mode);
mode.setModeManager(null);
}
}
/** This method permits to reset this manager by remove temp modes
* and clean permanent modes.
*/
public void resetModes() {
unsetExclusiveMode();
List<ActionMode<? super DRAW,CANVAS,COLOR>> removedModes = new ArrayList<>();
boolean stop = false;
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.descendingIterator();
while (!stop && iterator.hasNext()) {
ActionMode<? super DRAW,CANVAS,COLOR> m = iterator.next();
if (!m.canExit()) {
stop = true;
}
else {
iterator.remove();
m.cleanMode();
removedModes.add(m);
}
}
for(ActionMode<? super DRAW,CANVAS,COLOR> m : removedModes) {
m.onModeDesactivated();
fireModeDesactivated(m);
m.setModeManager(null);
}
}
/**
* Invoked when a key has been typed.
* See the class description for {@link KeyEvent} for a definition of
* a key typed event.
*
* @param e
*/
public void keyTyped(KeyEvent e) {
this.lastPointerEvent = null;
if (this.exclusiveMode!=null) {
this.exclusiveMode.keyTyped(e);
}
else {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.descendingIterator();
while (iterator.hasNext()) {
ActionMode<? super DRAW,CANVAS,COLOR> m = iterator.next();
m.keyTyped(e);
if (e.isConsumed()) return;
}
}
}
/**
* Invoked when a key has been pressed.
* See the class description for {@link KeyEvent} for a definition of
* a key pressed event.
*
* @param e
*/
public void keyPressed(KeyEvent e) {
this.lastPointerEvent = null;
if (this.exclusiveMode!=null) {
this.exclusiveMode.keyPressed(e);
}
else {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.descendingIterator();
while (iterator.hasNext()) {
ActionMode<? super DRAW,CANVAS,COLOR> m = iterator.next();
m.keyPressed(e);
if (e.isConsumed()) return;
}
}
}
/**
* Invoked when a key has been released.
* See the class description for {@link KeyEvent} for a definition of
* a key released event.
*
* @param e
*/
public void keyReleased(KeyEvent e) {
this.lastPointerEvent = null;
if (this.exclusiveMode!=null) {
this.exclusiveMode.keyReleased(e);
}
else {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.descendingIterator();
while (iterator.hasNext()) {
ActionMode<? super DRAW,CANVAS,COLOR> m = iterator.next();
m.keyReleased(e);
if (e.isConsumed()) return;
}
}
}
/**
* Invoked when the pointer button has been clicked (pressed
* and released) on a component.
*
* @param e
* @see #pointerLongClicked(PointerEvent)
*/
public void pointerClicked(PointerEvent e) {
updatePointerInfo(e, false);
if (this.exclusiveMode!=null) {
this.exclusiveMode.pointerClicked(this.lastPointerEvent);
}
else {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.descendingIterator();
while (iterator.hasNext()) {
ActionMode<? super DRAW,CANVAS,COLOR> m = iterator.next();
m.pointerClicked(this.lastPointerEvent);
if (e.isConsumed()) return;
}
}
}
/**
* Invoked when the pointer button has been long clicked (pressed
* and released) on a component.
*
* @param e
* @see #pointerClicked(PointerEvent)
*/
public void pointerLongClicked(PointerEvent e) {
updatePointerInfo(e, false);
if (this.exclusiveMode!=null) {
this.exclusiveMode.pointerLongClicked(this.lastPointerEvent);
}
else {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.descendingIterator();
while (iterator.hasNext()) {
ActionMode<? super DRAW,CANVAS,COLOR> m = iterator.next();
m.pointerLongClicked(this.lastPointerEvent);
if (e.isConsumed()) return;
}
}
}
/**
* Invoked when a pointer button has been pressed on a component.
*
* @param e
*/
public void pointerPressed(PointerEvent e) {
if (this.isForceHitResetWhenRelease) {
this.selectionShapeForFigureUnderTheMouse = null;
this.figureUnderTheMouse = null;
}
updatePointerInfo(e, false);
if (this.exclusiveMode!=null) {
this.exclusiveMode.pointerPressed(this.lastPointerEvent);
}
else {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.descendingIterator();
while (iterator.hasNext()) {
ActionMode<? super DRAW,CANVAS,COLOR> m = iterator.next();
m.pointerPressed(this.lastPointerEvent);
if (e.isConsumed()) return;
}
}
}
/**
* Invoked when a pointer button has been released on a component.
*
* @param e
*/
public void pointerReleased(PointerEvent e) {
updatePointerInfo(e, false);
if (this.exclusiveMode!=null) {
this.exclusiveMode.pointerReleased(this.lastPointerEvent);
}
else {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.descendingIterator();
while (iterator.hasNext()) {
ActionMode<? super DRAW,CANVAS,COLOR> m = iterator.next();
m.pointerReleased(this.lastPointerEvent);
if (e.isConsumed()) return;
}
}
// This is convenient for devices that cannot handle the POINTER_MOVE events.
if (this.isForceHitResetWhenRelease) {
this.selectionShapeForFigureUnderTheMouse = null;
this.figureUnderTheMouse = null;
}
}
/** Force or not to reset the hit figure when pointer is released.
* <p>
* By default, the hit figure is determine when the pointer is
* {@link #pointerDragged(PointerEvent) dragging} and when
* the pointer is {@link #pointerMoved(PointerEvent)}.
* For the devices that cannot handle the Move event (eg. Android OS),
* the hit figure must be reset when the pointer is released to ensure
* that the mode manager will try to retreive a hit figure when
* the pointer will be pressed again.
* So, depending on the event manager of the component that is using this
* mode manager, we recommend to call this function with
* <code>true</code> as parameter when the POINTER_MOVE event cannot be fired.
*
* @param resetHit
*/
public void setResetHitFigureWhenPointerReleased(boolean resetHit) {
this.isForceHitResetWhenRelease = resetHit;
}
/** Replies if the hit figure is reset when pointer is released.
*
* @return <code>true</code> when the hit figure is reset.
*/
public boolean isResetHitFigureWhenPointerReleased() {
return this.isForceHitResetWhenRelease;
}
/**
* Invoked when the mouse is moved with a button down.
*
* @param e
*/
public void pointerDragged(PointerEvent e) {
updatePointerInfo(e, true);
if (this.exclusiveMode!=null) {
this.exclusiveMode.pointerDragged(this.lastPointerEvent);
}
else {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.descendingIterator();
while (iterator.hasNext()) {
ActionMode<? super DRAW,CANVAS,COLOR> m = iterator.next();
m.pointerDragged(this.lastPointerEvent);
if (e.isConsumed()) return;
}
}
}
/**
* Invoked when the mouse is moved with no button down.
*
* @param e
*/
public void pointerMoved(PointerEvent e) {
updatePointerInfo(e, true);
if (this.exclusiveMode!=null) {
this.exclusiveMode.pointerMoved(this.lastPointerEvent);
}
else {
Iterator<ActionMode<? super DRAW,CANVAS,COLOR>> iterator = this.modeStack.descendingIterator();
while (iterator.hasNext()) {
ActionMode<? super DRAW,CANVAS,COLOR> m = iterator.next();
m.pointerMoved(this.lastPointerEvent);
if (e.isConsumed()) return;
}
}
}
/** Replies the area that permits to hit the figure under
* the mouse.
*
* @return the hit area, or <code>null</code> if there is
* no figure hitted.
*/
public Shape2f getFigureHitArea() {
return this.selectionShapeForFigureUnderTheMouse;
}
private void updatePointedData() {
if (this.selectionShapeForFigureUnderTheMouse==null
&& this.lastPointerEvent!=null) {
DRAW fig = null;
this.figureUnderTheMouse = null;
ActionModeManagerOwner<DRAW,CANVAS,COLOR> container = getModeManagerOwner();
if (container!=null) {
Shape2f selectionArea = null;
if (this.lastPointerEvent.isToolAreaSupported()) {
for(int i=0; fig==null && i<this.lastPointerEvent.getPointerCount(); ++i) {
selectionArea = this.lastPointerEvent.getToolArea(i);
fig = container.getFigureOn(selectionArea);
}
}
else {
fig = container.getFigureAt(this.lastPointerEvent.getX(), this.lastPointerEvent.getY());
if (fig!=null) {
selectionArea = new Circle2f(this.lastPointerEvent.getX(), this.lastPointerEvent.getY(), 1);
}
}
if (fig!=null) {
this.figureUnderTheMouse = new WeakReference<>(fig);
this.selectionShapeForFigureUnderTheMouse = selectionArea;
}
}
}
}
/** Replies the figure under the mouse cursor.
* To know is the mouse cursor is inside the
* shape of the figure pluease invoke
* {@link #isPointerInFigureShape()}.
*
* @return the figure under the mosue cursor;
* or <code>null</code> if none.
* @see #isPointerInFigureShape()
*/
public DRAW getPointedFigure() {
updatePointedData();
return this.figureUnderTheMouse==null ?
null : this.figureUnderTheMouse.get();
}
/** Replies if the mouse pointer is inside the shape
* of the pointer figure.
* To determine the pointed figure, please invoke
* {@link #getPointedFigure()}.
*
* @return <code>true</code> if the mouse pointer is
* inside the shape; otherwise <code>false</code>.
* @see #getPointedFigure()
*/
public boolean isPointerInFigureShape() {
updatePointedData();
return getPointedFigure()!=null;
}
/** Update the internal data conerning the pointer.
* This function updates {@link #lastPointerEvent}.
*
* @param e
* @param invalidateHitFigure is <code>true</code> to force
* to clear any information about a pointed figure.
*/
void updatePointerInfo(PointerEvent e, boolean invalidateHitFigure) {
if (invalidateHitFigure) {
this.selectionShapeForFigureUnderTheMouse = null;
this.figureUnderTheMouse = null;
}
ZoomableContext zc = null;
ActionModeManagerOwner<DRAW,CANVAS,COLOR> container = getModeManagerOwner();
if (container!=null) {
zc = container.getZoomableContext();
}
this.lastPointerEvent = new ActionPointerEvent(e, zc);
}
/** Replies the last pointer event encountered by the manager.
*
* @return the last pointer found.
*/
public ActionPointerEvent getLastPointerEvent() {
return this.lastPointerEvent;
}
}