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

com.panemu.tiwulfx.table.BaseCell Maven / Gradle / Ivy

Go to download

TiwulFX provides JavaFX custom components specially designed to work with java POJO object.

There is a newer version: 3.4.2
Show newest version
/*
 * License GNU LGPL
 * Copyright (C) 2012 Amrullah .
 */
package com.panemu.tiwulfx.table;

import com.panemu.tiwulfx.dialog.MessageDialogBuilder;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Control;
import javafx.scene.control.PopupControl;
import javafx.scene.control.Skin;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.util.StringConverter;

/**
 *
 * @author Amrullah
 */
public abstract class BaseCell extends TableCell {

	private Control control;
	private StringConverter stringConverter;
	private static final PseudoClass PSEUDO_CLASS_INVALID = PseudoClass.getPseudoClass("invalid");
	private boolean focusListenerAttached = false;
	public BaseCell(StringConverter stringConverter) {
		this.stringConverter = stringConverter;
		
		contentDisplayProperty().addListener(new ChangeListener() {

			@Override
			public void changed(ObservableValue observable, ContentDisplay oldValue, ContentDisplay newValue) {
				if (control == null) {
					control = getEditor();
					attachEnterEscapeEventHandler();
					setGraphic(control);
				}
				attachFocusListener();

//				if (isFocused() && isEditing()) {
//					commitEdit(getValueFromEditor());
//				}
				if (newValue == ContentDisplay.GRAPHIC_ONLY) {
					setValueToEditor(getItem());
				}
			}
		});

		addEventHandler(MouseEvent.ANY, new EventHandler() {
			@Override
			public void handle(MouseEvent event) {
				if (event.getEventType() == MouseEvent.MOUSE_EXITED || event.getEventType() == MouseEvent.MOUSE_MOVED) {
					TableColumn clm = getTableColumn();
					if (clm instanceof BaseColumn && !((BaseColumn) clm).isValid(getTableRow().getItem())) {
						BaseColumn baseColumn = (BaseColumn) clm;
						PopupControl popup = baseColumn.getPopup((R) getTableRow().getItem());
						if (event.getEventType() == MouseEvent.MOUSE_MOVED
								&& !popup.isShowing()) {

							Point2D p = BaseCell.this.localToScene(0.0, 0.0);
							popup.show(BaseCell.this,
									p.getX() + getScene().getX() + getScene().getWindow().getX(),
									p.getY() + getScene().getY() + getScene().getWindow().getY() + BaseCell.this.getHeight() - 1);
						} else if (event.getEventType() == MouseEvent.MOUSE_EXITED && popup.isShowing()) {
							popup.hide();
						}
					}
				} else if (event.getEventType() == MouseEvent.MOUSE_PRESSED) {
					/**
					 * We don't need this on java 8u05 because eventhough selection model
					 * is not cell-selection, we can get selected column using TablePos.
					 * However since 8u25 we cannot get selected column from TablePos unless
					 * selection model is cell-selection. To solve that, here we keep the information
					 * what column is clicked. This way we can get correct column to filter.
					 */
					CustomTableView ctv = (CustomTableView) getTableView();
					ctv.setSelectedColumn(getTableColumn());
				}
			}
		});
		
//		column.getInvalidRecordMap().addListener(new WeakInvalidationListener(invalidRecordListener));
		itemProperty().addListener(invalidRecordListener);
	}
	
	private final InvalidationListener invalidRecordListener = new InvalidationListener() {

		@Override
		public void invalidated(Observable observable) {
			if (BaseCell.this.getTableRow() == null) {
				pseudoClassStateChanged(PSEUDO_CLASS_INVALID, false);
			} else {
				BaseColumn column = (BaseColumn) getTableColumn();
				pseudoClassStateChanged(PSEUDO_CLASS_INVALID, column.getInvalidRecordMap().containsKey(BaseCell.this.getTableRow().getItem()));
			}
		}
	};

	/**
	 * For the case of TypeAhead, Date and Lookup, the focusable control is the
	 * textfield, not the control itself. This method is to be overridden by
	 * TypeAheadTableCell, DateTableCell and LookupTableCell
	 *
	 * @return
	 */
	protected Control getFocusableControl() {
		return control;
	}
	
	protected void attachSkinListener() {
		if (control != null) {
			control.skinProperty().addListener(new ChangeListener>() {
				@Override
				public void changed(ObservableValue> observable, Skin oldValue, Skin newValue) {
					if (oldValue == null && newValue != null) {
						attachFocusListener();
					}
				}
			});
		}
	}

	private void attachFocusListener() {
		if (focusListenerAttached) {
			return;
		}
		/**
		 * Set cell mode to edit if the editor control receives focus. This is
		 * intended to deal with mouse click. This way, commitEdit() will be
		 * called if the cell is no longer focused
		 */
		if (getFocusableControl() == null && isEditable()) {
			attachSkinListener();
		} else {
			getFocusableControl().focusedProperty().addListener(new ChangeListener() {
				@Override
				public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) {
					if (!BaseCell.this.isSelected() && newValue) {
						getTableView().getSelectionModel().select(getIndex(), getTableColumn());
					}
					if (!isEditing() && newValue) {
						programmaticallyEdited = true;
						getTableView().edit(getIndex(), getTableColumn());
						programmaticallyEdited = false;
					}
				}
			});
			focusListenerAttached = true;
		}
	}
	private boolean programmaticallyEdited = false;

	@Override
	public void startEdit() {
		/**
		 * If a row is added, new cells are created. The old cells are not
		 * disposed automatically. They still respond to user event's.
		 * Fortunately, the "should-be-discarded" cells have invisible row so we
		 * can recognize them and prevent them to interact with user's event.
		 */
		if (!this.getTableRow().isVisible() || !getTableRow().isEditable()) {
			return;
		}
		super.startEdit();
		if (!programmaticallyEdited) {

			if (control == null) {
				control = getEditor();
				attachEnterEscapeEventHandler();
			}
			attachFocusListener();
			setGraphic(control);
			setValueToEditor(getItem());
			setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
			/**
			 * put focus on the textfield so user can directly typed on it
			 */
			Runnable r = new Runnable() {
				@Override
				public void run() {
					control.requestFocus();
				}
			};
			Platform.runLater(r);
		}
	}

	protected abstract void setValueToEditor(C value);

	protected abstract C getValueFromEditor();

	protected abstract Control getEditor();

	@Override
	public void commitEdit(C newValue) {
		super.commitEdit(newValue);
		
		/**
		 * I guess this is a bug in TableView. I checked in Ensemble8,
		 * pressing ENTER to commit edited cell will throw focus out of
		 * TableView. So, let's bring the focus back to TableView
		 */
		Runnable r = new Runnable() {
			@Override
			public void run() {
				getTableView().requestFocus();
			}
		};
		Platform.runLater(r);
	}
	
	

	@Override
	public void cancelEdit() {

		if (!isFocused() && isEditing()) {
			/**
			 * The only way to commit edit in tableCell was by pressing 
			 * enter. If user select another cell by clicking it or pressing tab 
			 * than cell's value is reverted back. We want to change this behavior.
			 * Now if user move to another cell, it's value is committed. The
			 * only way to revert the value is by pressing escape. Check
			 * {@link Cell}
			 */
			commitEdit(getValueFromEditor());
			return;
		}

		setValueToEditor(getItem());
		super.cancelEdit();
		
		/**
		 * I guess this is a bug in TableView. I checked in Ensemble8,
		 * pressing ESC to cancel edited cell will throw focus out of
		 * TableView. So, let's bring the focus back to TableView
		 */
		Runnable r = new Runnable() {
			@Override
			public void run() {
				getTableView().requestFocus();
			}
		};
		Platform.runLater(r);
	}

	@Override
	public void updateItem(C item, boolean empty) {
		boolean emptyRow = getTableView().getItems().size() < getIndex() + 1;
		/**
		 * don't call super.updateItem() because it will trigger cancelEdit() if
		 * the cell is being edited. It causes calling commitEdit() ALWAYS call
		 * cancelEdit as well which is undesired.
		 *
		 */
		if (!isEditing()) {
			super.updateItem(item, empty && emptyRow);
		}
		if (empty && isSelected()) {
			updateSelected(false);
		}
		if (empty && emptyRow) {
			setText(null);
			//do not nullify graphic here. Let the TableRow to control cell dislay
		} else if (!isEditing()) {
			setText(getString(item));
		}
	}

	protected final String getString(C value) {
		try {
			return stringConverter.toString(value);
		} catch (ClassCastException ex) {
			String propertyName = getTableColumn() instanceof BaseColumn ? ((BaseColumn) getTableColumn()).getPropertyName() : "unknown";
			if (getTableRow() != null && getTableRow().getItem() != null) {
				propertyName = getTableRow().getItem().getClass().getName() + "." + propertyName;
			}
			String msg = "Invalid column type for \"" + propertyName + "\"";
			throw new RuntimeException(msg, ex);
		}
	}

	protected void attachEnterEscapeEventHandler() {
		control.setOnKeyPressed(new EventHandler() {
			@Override
			public void handle(KeyEvent t) {
				if (t.getCode() == KeyCode.ENTER && !t.isShiftDown()) {
					commitEdit(getValueFromEditor());
				} else if (t.getCode() == KeyCode.ESCAPE) {
					cancelEdit();
				}
			}
		});
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy