JavaFxFlowController.java
/*
* *********************************************************************************************************************
*
* blueMarine II: Semantic Media Centre
* http://tidalwave.it/projects/bluemarine2
*
* Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
*
* *********************************************************************************************************************
*
* 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.
*
* *********************************************************************************************************************
*
* git clone https://bitbucket.org/tidalwave/bluemarine2-src
* git clone https://github.com/tidalwave-it/bluemarine2-src
*
* *********************************************************************************************************************
*/
package it.tidalwave.bluemarine2.ui.commons.flowcontroller.impl.javafx;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Stack;
import javafx.util.Duration;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import it.tidalwave.bluemarine2.ui.commons.OnActivate;
import it.tidalwave.bluemarine2.ui.commons.OnDeactivate;
import it.tidalwave.bluemarine2.ui.commons.flowcontroller.FlowController;
import java.lang.annotation.Annotation;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
/***********************************************************************************************************************
*
* A JavaFX implementation of {@link FlowController}.
*
* This implementation is very basic and suitable to simple applications without memory criticalities. It keeps a
stack of previous Presentations, without disposing their resources. A more efficient implementation should be similar
to the Android Activity life-cycle, which can jettison resources of past activities and recreate them on demand. But
it would require life-cycle methods on the node interfaces.
*
* @stereotype Controller
*
* @author Fabrizio Giudici
*
**********************************************************************************************************************/
@Slf4j
public class JavaFxFlowController implements FlowController
{
@RequiredArgsConstructor @Getter @ToString
static class NodeAndControl
{
@Nonnull
private final Node node;
@Nullable
private final Object control;
}
@Getter @Setter
private StackPane contentPane;
// TODO: this implementation keeps all the history in the stack, thus wasting some memory.
private final Stack<NodeAndControl> presentationStack = new Stack<>();
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override
public void showPresentation (@Nonnull final Object presentation)
{
showPresentationImpl(presentation, null);
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override
public void showPresentation (@Nonnull final Object presentation, @Nonnull final Object control)
{
showPresentationImpl(presentation, control);
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
private void showPresentationImpl (@Nonnull final Object presentation, @Nullable final Object control)
{
log.info("showPresentationImpl({}, {})", presentation, control);
// TODO: use an aspect - should be already done in some other project
// FIXME: should not be needed, presentations should already run in JavaFX thread
Platform.runLater(() ->
{
final Node newNode = (Node)presentation;
if (presentationStack.isEmpty())
{
contentPane.getChildren().add(newNode);
}
else
{
final Node oldNode = presentationStack.peek().getNode();
slide(newNode, oldNode, +1);
}
presentationStack.push(new NodeAndControl(newNode, control));
notifyActivated(control);
});
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override
public void dismissCurrentPresentation()
{
log.info("dismissCurrentPresentation()");
if (presentationStack.size() < 2)
{
// TODO: should ask for user confirmation
powerOff();
}
else
{
Platform.runLater(() ->
{
log.debug(">>>> presentationStack: {}", presentationStack);
final Node oldNode = presentationStack.pop().getNode();
final Node newNode = presentationStack.peek().getNode();
slide(newNode, oldNode, -1);
notifyActivated(presentationStack.peek().getControl());
});
}
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
public void tryToDismissCurrentPresentation()
{
log.info("tryToDismissCurrentPresentation()");
try
{
canDeactivate(presentationStack.peek().getControl(), this::dismissCurrentPresentation);
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
{
log.error("", e);
}
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override
public void powerOff()
{
log.info("Shutting down...");
// TODO: fire a PowerOff event and wait for collaboration completion
// TODO: in this case, the responsibility to fire PowerOn should be moved here
// Platform.exit();
System.exit(0); // needed, otherwise Spring won't necessarily shut down
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
private void notifyActivated (@Nullable final Object control)
{
try
{
log.debug("notifyActivated({})", control);
final Method method = findAnnotatedMethod(control, OnActivate.class);
if (method != null)
{
// FIXME: should run in a background process
method.invoke(control);
}
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
{
log.error("", e);
throw new RuntimeException(e);
}
}
/*******************************************************************************************************************
*
* If a Presentation Control is passed, it is inspected for a method annotated with {@link OnDeactivate}. If found,
* it is called. If it returns {@code false}, this method returns {@code false}.
*
* @param control the Presentation Control
*
******************************************************************************************************************/
private void canDeactivate (@Nullable final Object control, @Nonnull final Runnable runnable)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
{
log.debug("canDeactivate({})", control);
final Method method = findAnnotatedMethod(control, OnDeactivate.class);
if (method == null)
{
runnable.run();
}
else
{
// FIXME: should run in a background process
if (method.invoke(control).equals(OnDeactivate.Result.PROCEED))
{
runnable.run();
}
}
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nullable
private static Method findAnnotatedMethod (@Nullable final Object object,
@Nonnull final Class<? extends Annotation> annotationClass)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
{
log.debug("findAnnotatedMethod({}, {})", object, annotationClass);
if (object != null)
{
for (final Method method : object.getClass().getDeclaredMethods())
{
if (method.getAnnotation(annotationClass) != null)
{
log.debug(">>>> found {} annotated method on {}", annotationClass, object);
method.setAccessible(true);
return method;
}
}
}
return null;
}
/*******************************************************************************************************************
*
* Starts a "slide in/out" animation moving an old {@link Node} out and a new {@link Node} in, with a given
* direction.
*
* @param newNode the {@code Node} to move in
* @param oldNode the {@code Node} to move out
* @param direction +1 for "forward" direction, -1 for "backward" direction
*
******************************************************************************************************************/
private void slide (@Nonnull final Node newNode, @Nonnull final Node oldNode, final int direction)
{
contentPane.getChildren().add(newNode);
final double height = contentPane.getHeight();
final KeyFrame start = new KeyFrame(Duration.ZERO,
new KeyValue(newNode.translateYProperty(), height * direction),
new KeyValue(oldNode.translateYProperty(), 0));
final KeyFrame end = new KeyFrame(Duration.millis(200),
new KeyValue(newNode.translateYProperty(), 0),
new KeyValue(oldNode.translateYProperty(), -height * direction));
final Timeline slideAnimation = new Timeline(start, end);
slideAnimation.setOnFinished(event -> contentPane.getChildren().remove(oldNode));
slideAnimation.play();
}
}