All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tentackle.fx.FxComponentDelegate Maven / Gradle / Ivy

/*
 * Tentackle - https://tentackle.org.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

package org.tentackle.fx;

import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.PopupWindow;

import org.tentackle.bind.BindingVetoException;
import org.tentackle.common.ExceptionHelper;
import org.tentackle.fx.bind.FxComponentBinding;
import org.tentackle.fx.table.FxTableCell;
import org.tentackle.fx.table.FxTreeTableCell;
import org.tentackle.log.Logger;
import org.tentackle.validate.ValidationFailedException;
import org.tentackle.validate.ValidationResult;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Objects;

/**
 * Delegate implementing FxComponent.
 *
 * @author harald
 */
public abstract class FxComponentDelegate extends FxControlDelegate implements FxComponent {

  /**
   * Mandatory CSS style class.
   */
  public static final String MANDATORY_STYLE = "tt-mandatory";

  /**
   * Error CSS style class.
   */
  public static final String ERROR_STYLE = "tt-error";

  /**
   * Info CSS style class.
   */
  public static final String INFO_STYLE = "tt-info";


  private static final Logger LOGGER = Logger.get(FxComponentDelegate.class);

  /**
   * The type handled by the component.
   */
  private Class type;

  /**
   * The optional generic type.
   */
  private Type genericType;

  /**
   * The value translator.
   */
  private ValueTranslator valueTranslator;


  /**
   * Mandatory property.
   */
  private boolean mandatory;
  private BooleanProperty mandatoryProperty;

  /**
   * the field path to bind, null if autobinding
   */
  private String bindingPath;

  /**
   * the current binding, null if none.
   */
  private FxComponentBinding binding;

  /**
   * the component's declaration path.
   */
  private String componentPath;

  /**
   * The saved view value.
   */
  private Object savedViewObject;

  /**
   * True if saveView() invoked at least once.
   */
  private boolean savedViewObjectValid;

  /**
   * The last set view value.
   */
  private Object lastViewObject;

  /**
   * The error message if conversion failed.
* This message is shown to the user in a tooltip. */ private String errorMessage; /** * True if temporary error. */ private boolean errorTemporary; /** * The error popup shown.
* null if not shown. */ private PopupWindow errorPopup; /** * The info message.
* This message is shown to the user in a tooltip. */ private String infoMessage; /** * The info popup shown.
* null if not shown. */ private PopupWindow infoPopup; /** * true if model was updated.
* Will be cleared if view is updated again. */ private boolean modelUpdated; /** * Flag to update model only once in recursive invocations. */ private boolean updatingModel; /** * Flag to update view only once in recursive invocations. */ private boolean updatingView; /** * The table cell, if this is used as a cell editor in table. */ private FxTableCell tableCell; /** * The treetable cell, if this is used as a cell editor in a treetable. */ private FxTreeTableCell treeTableCell; @Override public FxComponentDelegate getDelegate() { return this; } /** * Gets the component of this delegate. * * @return the component */ public abstract FxComponent getComponent(); /** * Gets the component as a node. * * @return the node */ public Node getNode() { return (Node) getComponent(); } @Override public FxContainer getParentContainer() { Parent parent = ((Node) getComponent()).getParent(); return parent instanceof FxContainer ? (FxContainer) parent : null; } @Override public String toGenericString() { StringBuilder buf = new StringBuilder(); buf.append("component "). append(getComponent().getClass().getName()); String id = ((Node) getComponent()).getId(); if (id != null) { buf.append('[').append(id).append(']'); } return buf.toString(); } @Override public String toString() { return "delegate for " + toGenericString(); } @Override protected void updateChangeable(boolean changeable) { // the default for components is to use the disabled property getNode().setDisable(!changeable); // non-changeable components should not be focus traversable getNode().setFocusTraversable(changeable); // mandatory is only shown if component is changeable as well updateMandatoryStyle(isMandatory()); updateInfoStyle(changeable && infoMessage != null); updateErrorStyle(changeable && errorMessage != null); } @Override protected ReadOnlyBooleanWrapper createChangeableProperty(boolean changeable) { ReadOnlyBooleanWrapper changeableProperty = new ReadOnlyBooleanWrapper(changeable); // if the disabled property changes, change the controlChangeable property as well changeableProperty.bind(getNode().disabledProperty().not()); return changeableProperty; } @Override public void setContainerChangeable(boolean containerChangeable) { if (!isContainerChangeableIgnored()) { // don't invoke updateChangeable() as this would overwrite local controlChangeable! getNode().setDisable(!containerChangeable || !isControlChangeable()); // mandatory is only shown if component is changeable as well updateMandatoryStyle(isMandatory()); updateInfoStyle(containerChangeable && infoMessage != null); updateErrorStyle(containerChangeable && errorMessage != null); } } @Override public void setValueTranslator(ValueTranslator valueTranslator) { if (this.valueTranslator != null) { throw new FxRuntimeException("value translator already set"); } this.valueTranslator = valueTranslator; } @Override public ValueTranslator getValueTranslator() { return valueTranslator; } /** * Returns a flag to avoid recursive invocations of {@link #updateModel()}. * * @return true if currently updating the model */ protected boolean isUpdatingModel() { return updatingModel; } /** * Returns a flag to avoid recursive invocations of {@link #updateView()}. * * @return true if currently updating the view */ protected boolean isUpdatingView() { return updatingView; } @Override public void updateView() { if (!updatingView) { try { updatingView = true; modelUpdated = false; fireModelToViewListeners(); if (binding != null) { binding.setViewValue(binding.getModelValue()); } } finally { updatingView = false; } } } @Override public void updateModel() { if (!updatingModel) { try { updatingModel = true; if (!getNode().isDisabled()) { try { String oldError = getError(); if (binding != null) { binding.setModelValue(binding.getViewValue()); List results = binding.validate(); if (!results.isEmpty()) { throw new ValidationFailedException(results); } } fireViewToModelListeners(); modelUpdated = true; if (getError() != null && !getError().equals(oldError)) { handleInputFailure(getError()); } } catch (RuntimeException rex) { if (getError() == null) { ValidationFailedException vx = ExceptionHelper.extractException(ValidationFailedException.class, true, rex); String msg; if (vx == null) { BindingVetoException bve = ExceptionHelper.extractException(BindingVetoException.class, true, rex); if (bve == null) { // extended component log only if not a rethrown veto exception LOGGER.severe("update model failed for " + FxUtilities.getInstance().dumpComponentHierarchy(getNode()), rex); } msg = rex.getLocalizedMessage(); if (msg == null) { msg = rex.getClass().getName(); } } else { msg = vx.resultsAsMessage(); } // some conversion/mapping failed (usually parse-exception): // -> show error popup and stay in component handleInputFailure(msg); } } triggerViewModified(); } } finally { updatingModel = false; } } } /** * Handles a failure during conversion of the user's input to the model. * * @param msg the failure message */ protected void handleInputFailure(String msg) { setError(msg); setErrorTemporary(true); Scene scene = getNode().getScene(); if (scene != null) { Node node = scene.getFocusOwner(); if (node != getNode() && (!(node instanceof FxComponent) || ((FxComponent) node).getError() == null)) { // only if other component currently holding the focus has no error (would loop otherwise) Platform.runLater(((Node) getComponent())::requestFocus); } } } @Override public boolean isModelUpdated() { return modelUpdated; } @Override public void showErrorPopup() { hideErrorPopup(); errorPopup = FxUtilities.getInstance().showErrorPopup(getComponent()); } @Override public void hideErrorPopup() { if (errorPopup != null) { errorPopup.hide(); errorPopup = null; } } @Override public String getError() { return errorMessage; } @Override public void setError(String errorMessage) { boolean changed = !Objects.equals(this.errorMessage, errorMessage); this.errorMessage = errorMessage; if (changed) { updateErrorStyle(errorMessage != null && isChangeable()); } if (errorMessage == null) { errorTemporary = false; } } @Override public void setErrorTemporary(boolean errorTemporary) { this.errorTemporary = errorTemporary; } @Override public boolean isErrorTemporary() { return errorTemporary; } /** * Updates the error style and shows/hides the popup if component has focus. * * @param enable true to enable style */ protected void updateErrorStyle(boolean enable) { if (!enable) { getNode().getStyleClass().remove(ERROR_STYLE); } else if (!getNode().getStyleClass().contains(ERROR_STYLE)) { getNode().getStyleClass().add(ERROR_STYLE); } if (!enable || getNode().isFocused()) { showErrorPopup(); // show/hide info popup, node has focus (see ComponentConfigurator) } } @Override public void showInfoPopup() { hideInfoPopup(); infoPopup = FxUtilities.getInstance().showInfoPopup(getComponent()); } @Override public void hideInfoPopup() { if (infoPopup != null) { infoPopup.hide(); infoPopup = null; } } @Override public String getInfo() { return infoMessage; } @Override public void setInfo(String infoMessage) { boolean changed = !Objects.equals(this.infoMessage, infoMessage); this.infoMessage = infoMessage; if (changed) { updateInfoStyle(infoMessage != null && isChangeable()); } } /** * Updates the info style and shows/hides the popup if component has focus. * * @param enable true to enable style */ protected void updateInfoStyle(boolean enable) { if (!enable) { getNode().getStyleClass().remove(INFO_STYLE); } else if (!getNode().getStyleClass().contains(INFO_STYLE)) { getNode().getStyleClass().add(INFO_STYLE); } if (!enable || getNode().isFocused()) { showInfoPopup(); // show/hide info popup, node has focus (see ComponentConfigurator) } } @Override public void triggerViewModified() { if (isSavedViewObjectValid()) { // application maintains a validation lifecycle boolean viewModified = !Objects.equals(getViewObject(), getSavedViewObject()); setViewModified(viewModified); if (getParentContainer() != null) { getParentContainer().triggerViewModified(); } if (viewModified && !isErrorTemporary()) { setError(null); // error will be re-checked by application on operations like "save" or alike } } } @Override @SuppressWarnings({ "rawtypes", "unchecked" }) public void setViewValue(Object value) { ValueTranslator translator = getValueTranslator(); Object viewObject = translator.toView(value); setViewObject(viewObject); setLastViewObject(viewObject); } @Override @SuppressWarnings({ "rawtypes", "unchecked" }) public V getViewValue() { ValueTranslator translator = getValueTranslator(); return (V) translator.toModel(getViewObject()); } @Override public void saveView() { savedViewObject = getViewObject(); savedViewObjectValid = true; setViewModified(false); } @Override public void invalidateSavedView() { savedViewObjectValid = false; } @Override public boolean isSavedViewObjectValid() { return savedViewObjectValid; } @Override public Object getSavedViewObject() { if (!savedViewObjectValid) { throw new FxRuntimeException("saved value is invalid in " + getComponent()); } return savedViewObject; } /** * Gets the last view object. *

* This is a hidden feature not exposed to the public API. * * @return the last view object converted from the model */ public Object getLastViewObject() { return lastViewObject; } /** * Sets the last view object.
* The method is invoked from {@link #setViewValue(Object)} when updating from the model. *

* This is a hidden feature not exposed to the public API. * * @param lastViewObject the last view object */ public void setLastViewObject(Object lastViewObject) { this.lastViewObject = lastViewObject; } @Override public void setType(Class type) { if (this.type != null && this.type != type) { throw new FxRuntimeException("type already set to " + this.type + " (requested " + type + ") for " + getComponent()); } this.type = type; } @Override public Class getType() { return type; } @Override public void setGenericType(Type genericType) { this.genericType = genericType; } @Override public Type getGenericType() { return genericType; } @Override public void setMandatory(boolean mandatory) { if (mandatoryProperty != null) { mandatoryProperty.set(mandatory); } else { this.mandatory = mandatory; updateMandatoryStyle(mandatory); } } @Override public boolean isMandatory() { if (mandatoryProperty != null) { return mandatoryProperty.get(); } return mandatory; } @Override public BooleanProperty mandatoryProperty() { if (mandatoryProperty == null) { mandatoryProperty = new SimpleBooleanProperty(mandatory); mandatoryProperty.addListener((obs, ov, nv) -> updateMandatoryStyle(nv)); } return mandatoryProperty; } /** * Does the physical update of the style. * See resources/org/tentackle/fx/tentackle.css * * @param mandatory true if mandatory */ protected void updateMandatoryStyle(boolean mandatory) { if (mandatory && !getNode().isDisabled()) { // node must be editable by the user to // show the mandator color. if (!getNode().getStyleClass().contains(MANDATORY_STYLE)) { getNode().getStyleClass().add(MANDATORY_STYLE); } } else { getNode().getStyleClass().remove(MANDATORY_STYLE); } } @Override public void setBindingPath(String bindingPath) { this.bindingPath = bindingPath; } @Override public String getBindingPath() { return bindingPath; } @Override public void setComponentPath(String componentPath) { this.componentPath = componentPath; } @Override public String getComponentPath() { return componentPath; } @Override public void setBinding(FxComponentBinding binding) { this.binding = binding; } @Override public FxComponentBinding getBinding() { return binding; } @Override public FxTableCell getTableCell() { return tableCell; } @Override public void setTableCell(FxTableCell tableCell) { this.tableCell = tableCell; } @Override public FxTreeTableCell getTreeTableCell() { return treeTableCell; } @Override public void setTreeTableCell(FxTreeTableCell treeTableCell) { this.treeTableCell = treeTableCell; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy