package de.calette.mephisto3.ui; import de.calette.mephisto3.control.ControlListener; import de.calette.mephisto3.control.ServiceControlEvent; import de.calette.mephisto3.control.ServiceController; import de.calette.mephisto3.util.TransitionQueue; import de.calette.mephisto3.util.TransitionUtil; import javafx.animation.FadeTransition; import javafx.animation.Transition; import javafx.animation.TranslateTransition; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; /** * Super class for all regular controllable panels. */ public abstract class ControllableSelectorPanel<T> extends HBox implements ControlListener { private final static Logger LOG = LoggerFactory.getLogger(ControllableSelectorPanel.class); private Pane parent; private int scrollWidth; private TransitionQueue transitionQueue; private int index; private Class controlItemBoxClass; private List<T> models; private int backTopPadding = -1; private T selection; private double margin; public ControllableSelectorPanel(double margin, Pane parent, int scrollWidth, List<T> models, Class controlItemBoxClass) { super(margin); this.margin = margin; this.setOpacity(0); this.models = models; this.controlItemBoxClass = controlItemBoxClass; this.parent = parent; this.scrollWidth = scrollWidth; this.parent = parent; transitionQueue = new TransitionQueue(this); } public List<T> getModels() { return models; } public void setSelection(T selection) { this.selection = selection; } public void setSelectionIndex(int index) { this.index = index; } public int getSelectionIndex() { return index; } public Pane getParentPane() { return parent; } protected void setBackButton(int backTopPadding) { BackButtonBox backButton = new BackButtonBox(scrollWidth, backTopPadding); getChildren().add(backButton); index = 1; } protected int getTopPadding() { return 0; } @Override public void controlEvent(ServiceControlEvent event) { if(event.getEventType().equals(ServiceControlEvent.EVENT_TYPE.PUSH)) { Platform.runLater(new Runnable() { @Override public void run() { hidePanel(); } }); } else if(event.getEventType().equals(ServiceControlEvent.EVENT_TYPE.NEXT)) { scroll(false, -getScrollWidth()); } else if(event.getEventType().equals(ServiceControlEvent.EVENT_TYPE.PREVIOUS)) { scroll(true, getScrollWidth()); } else if(event.getEventType().equals(ServiceControlEvent.EVENT_TYPE.LONG_PUSH)) { Platform.runLater(new Runnable() { @Override public void run() { onLongPush(); } }); } } /** * Shows the panel, plays the onShow transitions, adds the control event listener. */ public void showPanel() { parent.getChildren().add(this); //so lets create all children LOG.debug("ControllableSelectorPanel creates " + models.size() + " child components"); List<Node> items = new ArrayList<>(); for(T model : models) { ControllableItemPanel item = createControllableItemPanelFor(controlItemBoxClass, model); items.add((Node) item); } getChildren().addAll(items); //set the initial left padding to focus the first item int itemCount = models.size() + 1; //+1 for the back button double leftPadding = itemCount * scrollWidth - scrollWidth - scrollWidth - scrollWidth - margin; if(backTopPadding != -1) { leftPadding = itemCount * scrollWidth - scrollWidth; } setPadding(new Insets(getTopPadding(), 0, 0, leftPadding)); //update panel view to the selected model applyLastSelection(); final ControllableItemPanel newSelection = (ControllableItemPanel) getChildren().get(index); newSelection.select(); final FadeTransition inFader = TransitionUtil.createInFader(ControllableSelectorPanel.this); inFader.setOnFinished(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent actionEvent) { ServiceController.getInstance().addControlListener(ControllableSelectorPanel.this); } }); inFader.play(); } /** * Hides the panel, plays the onHide transitions, removes the control event listener and calls "onHide". */ public void hidePanel() { ServiceController.getInstance().removeControlListener(this); final FadeTransition outFader = TransitionUtil.createOutFader(this); outFader.setOnFinished(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent actionEvent) { parent.getChildren().remove(ControllableSelectorPanel.this); T userData = (T) getSelectedPanel().getUserData(); onHide(userData); } }); outFader.play(); } protected int getScrollWidth() { return scrollWidth; } /** * Returns the selected ControllableItemPanel instance. */ public ControllableItemPanel getSelectedPanel() { return (ControllableItemPanel) this.getChildren().get(index); } protected void scroll(boolean toLeft, int width) { if(index == getItemCount() && !toLeft) { return; } if(index == 1 && getItemCount() == 1 && !toLeft) { return; } if(index == 0 && toLeft) { return; } //ignore scrolling when back button is available and selected if(backTopPadding == -1 && index == 1 && toLeft) { updateSelection(toLeft); return; } if(index != 0 || backTopPadding != -1) { Transition translateTransition = TransitionUtil.createTranslateByXTransition(this, 50, width); transitionQueue.addTransition(translateTransition); Platform.runLater(new Runnable() { @Override public void run() { transitionQueue.play(); } }); } updateSelection(toLeft); } /** * Returns the amount of items this panel should scroll. */ protected int getItemCount() { return models.size(); } /** * Updates the selection index and play the selection animation of the newly selected panel. * * @param toLeft true if the scrolling goes to the left. */ protected void updateSelection(boolean toLeft) { deselect(toLeft, index); if(toLeft) { index--; } else { index++; } select(toLeft, index); } /** * Deselects the current selection * * @param oldIndex the old index before the new index is updated. */ protected void deselect(boolean toLeft, int oldIndex) { ControllableItemPanel oldSelection = (ControllableItemPanel) getChildren().get(oldIndex); oldSelection.deselect(); } /** * Selects the current selection * * @param toLeft * @param newIndex the new index after deselection has been executed */ protected void select(boolean toLeft, int newIndex) { ControllableItemPanel newSelection = (ControllableItemPanel) getChildren().get(newIndex); newSelection.select(); } /** * Factory method to be implemented by subclasses to determine the concrete panel. * * @param controlItemBoxClass name of the class to create for the item * @param model the user data model used for the ControllableItemPanel */ protected ControllableItemPanel createControllableItemPanelFor(Class controlItemBoxClass, T model) { try { final Class<?> modelClass = model.getClass(); Constructor constructor = controlItemBoxClass.getConstructor(new Class[]{ControllableSelectorPanel.class, modelClass}); return (ControllableItemPanel) constructor.newInstance(this, model); } catch (Exception e) { LOG.error("Error creating item panel for " + controlItemBoxClass + ": " + e.getMessage(), e); } return null; } /** * Invoked then the hide transition is finished. * The user data the current selection was build with is passed here. * * @param userData the user data of the selected panel. */ protected abstract void onHide(T userData); /** * Optional action to be executed when a long push is executed. */ protected void onLongPush() { } /** * Checks if a selection model was passed. * If true, the selector scrolls silently to the position and updates the selection index. */ private void applyLastSelection() { if(selection != null) { while(!selection.equals(getSelectedPanel().getUserData())) { index++; } final TranslateTransition translateTransition = TransitionUtil.createTranslateByXTransition(this, 1, (index - 1) * (-scrollWidth)); translateTransition.play(); } } }