Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.print.PageLayout;
import javafx.print.PrinterJob;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Control;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Skinnable;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.TreeTableViewSkin;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.PickResult;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.transform.Scale;
import javafx.stage.PopupWindow;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.util.Callback;
import org.tentackle.bind.Bindable;
import org.tentackle.bind.Binder;
import org.tentackle.bind.Binding;
import org.tentackle.bind.BindingException;
import org.tentackle.bind.BindingMember;
import org.tentackle.common.Constants;
import org.tentackle.common.Service;
import org.tentackle.common.ServiceFactory;
import org.tentackle.common.StringHelper;
import org.tentackle.fx.component.FxTableView;
import org.tentackle.fx.component.FxTextArea;
import org.tentackle.fx.component.Note;
import org.tentackle.fx.table.TableColumnConfiguration;
import org.tentackle.fx.table.TableConfiguration;
import org.tentackle.log.Logger;
import org.tentackle.misc.ConcurrencyHelper;
import org.tentackle.validate.ValidationFailedException;
import org.tentackle.validate.ValidationMapper;
import org.tentackle.validate.ValidationResult;
import java.awt.Desktop;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.NavigableSet;
import java.util.StringTokenizer;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
interface FxUtilitiesHolder {
FxUtilities INSTANCE = ServiceFactory.createService(FxUtilities.class, FxUtilities.class);
}
/**
* Utility methods for Fx.
*
* @author harald
*/
@Service(FxUtilities.class) // defaults to self
public class FxUtilities {
/**
* The singleton.
*
* @return the singleton
*/
public static FxUtilities getInstance() {
return FxUtilitiesHolder.INSTANCE;
}
private static final Logger LOGGER = Logger.get(FxUtilities.class);
/**
* Executor for {@link #runAndWait(Runnable)} below.
*/
private final Executor blockingFxExecutor = ConcurrencyHelper.createBlockingExecutor(Platform::runLater);
/**
* The help url.
* Prefix for online help, e.g.: http://localhost/manual/index.html
*/
private String helpURL;
/**
* Applies the default stylesheets to a scene.
* Invoked for all newly created scenes.
*
* @param scene the scene
*/
public void applyStylesheets(Scene scene) {
scene.getStylesheets().add("/org/tentackle/fx/tentackle.css");
}
/**
* Returns whether decimal separator should be inserted automatically if missing in money input.
* This is usually a global setting for the whole application, but it can be filtered according
* to the component (parent component, etc...).
*
* @param component the input component
* @return true if insert separator according to scale if missing
*/
public boolean isLenientMoneyInput(FxTextComponent component) {
return false;
}
/**
* Gets the default column width for text areas if not set by the binding or the model's COLS= option.
*
* @param textArea the text area component
* @return the column width (default is 30)
*/
public int getDefaultTextAreaColumns(FxTextArea textArea) {
return 30;
}
/**
* Gets the timeout for the prefix selection in dropdown lists such as in comboboxes.
*
* @return the timeout in milliseconds (default is 500)
*/
public long getPrefixSelectionTimeout() {
return 500;
}
/**
* Opens the online help for a given component.
*
* @param control the Fx control
*/
public void showHelp(FxControl control) {
if (control != null) {
String url = control.getHelpUrl();
if (helpURL != null) { // with global prefix?
if (url != null) {
url = helpURL + url;
}
else {
url = helpURL;
}
}
if (url != null) {
try {
Desktop.getDesktop().browse(URI.create(url));
}
catch (IOException | RuntimeException ex) {
Fx.error(MessageFormat.format(FxFxBundle.getString("CANT OPEN HELP FOR {0}"), url), ex);
}
}
}
}
/**
* Gets the stage for a node.
*
* @param node the node
* @return the stage, null if node does not belong to a scene or scene does not belong to a stage.
*/
public Stage getStage(Node node) {
Stage stage = null;
if (node != null) {
Scene scene = node.getScene();
if (scene != null) {
Window window = scene.getWindow();
if (window instanceof Stage) {
stage = (Stage) window;
}
}
}
return stage;
}
/**
* Returns whether the stage is modal.
*
* @param stage the stage
* @return true if effectively modal
*/
public boolean isModal(Stage stage) {
boolean modal = false;
if (stage != null) {
switch(stage.getModality()) {
case APPLICATION_MODAL:
modal = true;
break;
case WINDOW_MODAL:
modal = stage.getOwner() != null;
break;
default:
// NONE
}
}
return modal;
}
/**
* Closes the hierarchy of stages from the given node stopping at given stage.
*
* @param node the node
* @param stopStage the stop stage, null to close the whole application
*/
public void closeStageHierarchy(Node node, Stage stopStage) {
Stage stage = getStage(node);
while (stage != null && stage != stopStage) {
stage.close();
Window owner = stage.getOwner();
if (owner instanceof Stage) {
stage = (Stage) owner;
}
else {
break;
}
}
}
/**
* Dumps the component hierarchy.
*
* @param node the node
* @return the formatted string
*/
@SuppressWarnings("rawtypes")
public String dumpComponentHierarchy(Node node) {
StringBuilder buf = new StringBuilder();
buf.append(">>> component hierarchy within ");
Stage stage = getStage(node);
if (stage != null) {
buf.append("stage '").append(stage.getTitle()).append("'");
}
while (node != null) {
buf.append("\n ");
if (node instanceof FxContainer) {
FxController controller = ((FxContainer) node).getController();
if (controller != null) {
buf.append(controller.getClass().getName()).append(": ");
}
}
else if (node instanceof FxTableView) {
TableConfiguration tableConfiguration = ((FxTableView) node).getConfiguration();
if (tableConfiguration != null) {
buf.append(tableConfiguration.getName()).append(": ");
}
}
buf.append(node).append(" @ [").append(node.getLayoutX()).append("/").append(node.getLayoutY()).append(']');
if (node instanceof FxComponent) {
Binding binding = ((FxComponent) node).getBinding();
if (binding != null) {
buf.append(" bound to ").append(binding.getMember());
}
}
node = node.getParent();
}
buf.append("\n<<<");
return buf.toString();
}
/**
* Runs a command on the FX application thread and waits for its execution to finish.
*
* @param runnable the Runnable whose run method will be executed on the FX application thread
* @throws FxRuntimeException if the calling thread is the FX application thread
* @see Platform#runLater(java.lang.Runnable)
*/
public void runAndWait(Runnable runnable) {
if (Platform.isFxApplicationThread()) {
throw new FxRuntimeException("calling thread is the FX application thread");
}
blockingFxExecutor.execute(runnable);
}
/**
* Workaround for the bug that messages are not completely displayed if text is wrapped.
*
* @param alert the alert dialog
* @param message the text message
*/
protected void setAlertMessage(Alert alert, String message) {
alert.setHeaderText(null);
Label textLabel = new Label(); // replace with non-wrapping label!
textLabel.setWrapText(false);
textLabel.setText(message);
alert.getDialogPane().setContent(textLabel);
alert.setResizable(true); // RT-38998
}
/**
* Shows an info dialog.
*
* @param message the message
* @param title optional title
* @return the alert dialog
*/
public Alert showInfoDialog(String message, String title) {
Alert alert = Fx.createAlert(Alert.AlertType.INFORMATION); // Alert is APPLICATION_MODAL
alert.setTitle(title == null ? FxFxBundle.getString("INFORMATION") : title);
setAlertMessage(alert, message);
alert.show();
return alert;
}
/**
* Shows a warning dialog.
*
* @param message the message
* @param title optional title
* @return the alert dialog
*/
public Alert showWarningDialog(String message, String title) {
Alert alert = Fx.createAlert(Alert.AlertType.WARNING); // Alert is APPLICATION_MODAL
alert.setTitle(title == null ? FxFxBundle.getString("WARNING") : title);
setAlertMessage(alert, message);
alert.show();
return alert;
}
/**
* Shows a question dialog.
* To avoid showAndWait, the method returns void and the result is provided via a Consumer.
*
* @param message the message
* @param defaultYes true if yes is the default button
* @param title optional title
* @param answer the user's answer (invoked with Boolean.TRUE or Boolean.FALSE, never null)
* @return the alert dialog
* @see Consumer
* @see org.tentackle.misc.Holder
*/
public Alert showQuestionDialog(String message, boolean defaultYes, String title, Consumer answer) {
Alert alert = Fx.createAlert(Alert.AlertType.CONFIRMATION);
alert.setTitle(title == null ? FxFxBundle.getString("QUESTION") : title);
setAlertMessage(alert, message);
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
Button noButton = (Button) alert.getDialogPane().lookupButton(ButtonType.NO);
Button yesButton = (Button) alert.getDialogPane().lookupButton(ButtonType.YES);
noButton.setDefaultButton(!defaultYes);
yesButton.setDefaultButton(defaultYes);
alert.setOnHidden(e -> answer.accept(alert.getResult() == ButtonType.YES));
alert.show();
return alert;
}
/**
* Shows an error dialog.
*
* @param message the message
* @param t optional throwable
* @param title optional title
* @return the alert dialog
*/
public Alert showErrorDialog(String message, Throwable t, String title) {
Alert alert = Fx.createAlert(Alert.AlertType.ERROR); // Alert is APPLICATION_MODAL
alert.setTitle(title == null ? FxFxBundle.getString("ERROR") : title);
setAlertMessage(alert, message);
if (t != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
t.printStackTrace(pw);
String exceptionText = sw.toString();
Label label = new Label("STACKTRACE:");
TextArea textArea = new TextArea(exceptionText);
textArea.setEditable(false);
textArea.setWrapText(true);
textArea.setMaxWidth(Double.MAX_VALUE);
textArea.setMaxHeight(Double.MAX_VALUE);
GridPane expContent = new GridPane();
expContent.setMaxWidth(Double.MAX_VALUE);
expContent.add(label, 0, 0);
expContent.add(textArea, 0, 1);
GridPane.setVgrow(textArea, Priority.ALWAYS);
GridPane.setHgrow(textArea, Priority.ALWAYS);
alert.getDialogPane().setExpandableContent(expContent);
// because of resize bug in FX8 (@todo: remove in later versions if fixed)
alert.getDialogPane().expandedProperty().addListener(l -> Platform.runLater(() -> {
alert.getDialogPane().requestLayout();
Window w = alert.getDialogPane().getScene().getWindow();
w.sizeToScene();
}));
}
alert.show();
return alert;
}
/**
* Shows an error popup for a component.
*
* @param component the component
* @return the popup, null if no errormessage in component
*/
public PopupWindow showErrorPopup(ErrorPopupSupported component) {
PopupWindow popup = null;
String errorMessage = component.getError();
if (!StringHelper.isAllWhitespace(errorMessage)) {
Note note = new Note(Note.Position.RIGHT, Note.Type.ERROR);
note.setText(errorMessage);
note.show((Node) component);
popup = note;
}
return popup;
}
/**
* Shows an info popup for a component.
*
* @param component the component
* @return the popup, null if no infomessage in component
*/
public PopupWindow showInfoPopup(InfoPopupSupported component) {
PopupWindow popup = null;
String infoMessage = component.getInfo();
if (!StringHelper.isAllWhitespace(infoMessage)) {
Note note = new Note(Note.Position.RIGHT, Note.Type.INFO);
note.setText(infoMessage);
note.show((Node) component);
popup = note;
}
return popup;
}
/**
* Creates an interactive error from a validation result.
*
* @param validationResult the validation result
* @param validationMappers the validation mappers
* @param binder the binder
* @return the interactive error
*/
public InteractiveError createInteractiveError(ValidationResult validationResult, NavigableSet validationMappers, Binder binder) {
return InteractiveErrorFactory.getInstance().createInteractiveError(
validationMappers, binder, validationResult);
}
/**
* Creates interactive errors from validation results.
*
* @param validationResults the validation results
* @param validationMappers the validation mappers
* @param binder the binder
* @return the interactive errors
*/
public List createInteractiveErrors(List validationResults, NavigableSet validationMappers, Binder binder) {
List errors = new ArrayList<>();
for (ValidationResult validationResult: validationResults) {
errors.add(createInteractiveError(validationResult, validationMappers, binder));
}
return errors;
}
/**
* Shows the validation warnings and errors in a dialog and marks the {@link FxControl}s related to those errors.
* Errors and warnings are shown in error or info dialogs.
*
* @param ex the validation exception
* @param validationMappers the validation mappers
* @param binder the binder
* @return true if there were errors, false if warnings only
*/
public boolean showValidationResults(ValidationFailedException ex, NavigableSet validationMappers, Binder binder) {
StringBuilder warnings = new StringBuilder();
StringBuilder errors = new StringBuilder();
List errorList = createInteractiveErrors(ex.getResults(), validationMappers, binder);
for (InteractiveError error : errorList) {
if (error.isWarning()) {
if (warnings.length() > 0) {
warnings.append('\n');
}
warnings.append(error.getText());
}
else {
if (errors.length() > 0) {
errors.append('\n');
}
errors.append(error.getText());
}
}
if (warnings.length() > 0 && errors.length() > 0) {
errors.append("\n\n").append(warnings);
warnings.setLength(0);
}
if (errors.length() > 0) {
Fx.error(errors.toString()).setOnHidden(event -> {
for (InteractiveError error : errorList) {
if (!error.isWarning()) {
FxControl control = error.getControl();
if (control instanceof FxComponent) {
((FxComponent) control).setError(error.getText());
}
}
}
});
}
else if (warnings.length() > 0) {
Fx.info(warnings.toString());
}
return errors.length() > 0;
}
/**
* Adds focus handling to sync with model.
*
* @param control the fx control
*/
public void setupFocusHandling(Control control) {
if (control instanceof FxComponent) {
final FxComponent component = (FxComponent) control;
control.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
FxUtilities.getInstance().focusGained(component);
component.showErrorPopup();
}
else {
FxUtilities.getInstance().focusLost(component);
component.hideErrorPopup();
}
});
control.hoverProperty().addListener((observable, oldValue, newValue) -> {
if (!control.isFocused()) {
if (newValue) {
if (component.getError() == null) {
component.showInfoPopup();
}
component.showErrorPopup();
}
else {
component.hideInfoPopup();
component.hideErrorPopup();
}
}
});
}
}
/**
* Performs all necessary operations on an FxComponent when it gained the focus.
*
* @param component the fx component
*/
public void focusGained(FxComponent component) {
if (component.getError() == null) { // don't override view if there is a parsing error
component.updateView();
if (component.isChangeable() && component instanceof FxTextComponent) {
// By default, FX autoselects a field if on re-focus.
// This cannot be configured and is hard wired (for whatever reason).
// In TT, this is configurable via the autoSelect feature.
// in FX components are autoselected if the field is focused by TAB (or ENTER in TT).
// If by mouse, the cursor is positioned where the mouse points to and nothing is selected.
// For numeric fields, however, (and where desired) this behaviour leads to unexpected data entry.
Platform.runLater(((FxTextComponent) component)::autoSelect);
}
}
}
/**
* Performs all necessary operations on an FxComponent when it lost the focus.
*
* @param component the fx component
*/
public void focusLost(FxComponent component) {
component.updateModel(); // some components will do nothing here and...
// ... isModelUpdate() will return false.
if (component.isModelUpdated() && component.getError() == null) { // no conversion error
// update view again to match the converted value
component.updateView(); // this will clear modelUpdated
ValueTranslator, ?> translator = component.getValueTranslator();
if (translator != null && translator.needsToModelTwice()) {
component.updateModel();
}
}
}
/**
* Remaps certain keys.
*
* @param control the fx control
*/
public void remapKeys(Control control) {
List mappedEvents = new ArrayList<>();
control.addEventFilter(KeyEvent.ANY, new EventHandler<>() {
@Override
public void handle(KeyEvent event) {
if (mappedEvents.remove(event)) {
return;
}
if (KeyCode.ENTER == event.getCode()) {
KeyEvent newEvent = remap(event, KeyCode.TAB);
mappedEvents.add(newEvent);
event.consume();
Event.fireEvent(event.getTarget(), newEvent);
}
}
private KeyEvent remap(KeyEvent event, KeyCode code) {
KeyEvent newEvent = new KeyEvent(
event.getEventType(),
event.getCharacter(),
event.getText(),
code,
event.isShiftDown(),
event.isControlDown(),
event.isAltDown(),
event.isMetaDown());
return newEvent.copyFor(event.getSource(), event.getTarget());
}
});
}
/**
* Filters certain keys for special features.
*
* @param control the fx control
*/
public void filterKeys(Control control) {
if (control instanceof FxComponent) {
control.addEventFilter(KeyEvent.ANY, event -> {
if (event.isControlDown() && !event.isAltDown() && !event.isMetaDown() && !event.isShiftDown()) {
FxComponent comp = (FxComponent) control;
if (event.getCode() == KeyCode.Z) {
if (comp.isViewModified() && comp.isSavedViewObjectValid()) {
if (event.getEventType() == KeyEvent.KEY_PRESSED) {
// Ctrl-Z restores the saved view value (usually the persisted values in the database)
Object viewObject = comp.getViewObject();
comp.setViewObject(comp.getSavedViewObject());
// With Ctrl-Z and Ctrl-Y the user can switch between the old persisted value and the new input value
comp.getDelegate().setLastViewObject(viewObject);
}
event.consume();
}
}
if (event.getCode() == KeyCode.Y) {
Object viewObject = comp.getDelegate().getLastViewObject();
if (viewObject != null) {
if (event.getEventType() == KeyEvent.KEY_PRESSED) {
// Ctrl-Y brings back the last set view value (the value when the user entered the field)
comp.setViewObject(viewObject);
}
event.consume();
}
}
}
});
}
}
/**
* Registers event filters for windows.
*
* @param window the window
*/
public void registerWindowEventFilters(Window window) {
window.addEventFilter(MouseEvent.ANY, event -> {
if (event.isShiftDown() && event.isControlDown() && !event.isMetaDown() && !event.isAltDown() &&
event.isPopupTrigger()) {
PickResult pickResult = event.getPickResult();
if (pickResult != null) {
Node node = pickResult.getIntersectedNode();
if (node != null) {
LOGGER.info(() -> "\n" + dumpComponentHierarchy(node));
}
}
}
});
}
/**
* Recursively expands all tree items while checking for recursion loops.
* The default implementation in TreeViewBehavior and TreeTableViewBehavior is not only in a private API,
* but cannot be accessed from the applications at all. The default implementation is dangerous since
* it does not check for recursion loops. We replace it by mapping the event to this utility method.
*
* @param treeItem the item to start expansion
* @param the item's value type
*/
public void expandAll(TreeItem treeItem) {
if (treeItem != null && !treeItem.isLeaf()) {
T value = treeItem.getValue();
if (!isValueInParentPath(treeItem)) {
treeItem.setExpanded(true);
ObservableList> children = treeItem.getChildren();
if (children != null) {
for (TreeItem childItem : children) {
expandAll(childItem);
}
}
}
}
}
/**
* Checks whether one of the parents already contains the item's value.
*
* @param treeItem the tree item to check
* @param the value's type
* @return true if value is in path
*/
public boolean isValueInParentPath(TreeItem treeItem) {
boolean inPath = false;
T value = treeItem.getValue();
if (value != null) {
TreeItem parentItem = treeItem.getParent();
while (parentItem != null) {
if (value.equals(parentItem.getValue())) {
inPath = true;
break;
}
parentItem = parentItem.getParent();
}
}
return inPath;
}
/**
* Recursively collapses all tree items.
*
* @param treeItem the item to start
* @param the item's value type
*/
public void collapseAll(TreeItem treeItem) {
if (treeItem != null && treeItem.isExpanded()) {
treeItem.setExpanded(false);
ObservableList> children = treeItem.getChildren();
if (children != null) {
for (TreeItem childItem : children) {
collapseAll(childItem);
}
}
}
}
/**
* Recursively collapses all tree items.
*
* @param treeItems the list of items to collapse
* @param the item's value type
* @see #collapseAll(TreeItem)
*/
public void collapseAll(Collection> treeItems) {
if (treeItems != null) {
for (TreeItem treeItem : treeItems) {
collapseAll(treeItem);
}
}
}
/**
* Prints a node.
* The user selects the printer and the node is scaled down if too large for the paper.
*
* @param node the node to print
*/
public void print(Node node) {
PrinterJob job = PrinterJob.createPrinterJob();
if (job != null && job.showPrintDialog(Fx.getStage(node))) {
PageLayout pageLayout = job.getJobSettings().getPageLayout();
double scaleX = 1.0;
if (pageLayout.getPrintableWidth() < node.getBoundsInParent().getWidth()) {
scaleX = pageLayout.getPrintableWidth() / node.getBoundsInParent().getWidth();
}
double scaleY = 1.0;
if (pageLayout.getPrintableHeight() < node.getBoundsInParent().getHeight()) {
scaleY = pageLayout.getPrintableHeight() / node.getBoundsInParent().getHeight();
}
double scaleXY = Double.min(scaleX, scaleY);
Scale scale = new Scale(scaleXY, scaleXY);
node.getTransforms().add(scale);
boolean success = job.printPage(node);
node.getTransforms().remove(scale);
if (success) {
job.endJob();
}
}
}
/**
* Gets a binding option from an options string.
*
* @param options the binding options string
* @param key the option key
* @return the option, null if no such option
*/
public String getBindingOption(String options, String key) {
String option = null;
StringTokenizer stok = new StringTokenizer(options.toUpperCase(), ",");
while (stok.hasMoreTokens()) {
String token = stok.nextToken().trim();
if (token.startsWith(key)) {
option = token;
break;
}
}
return option;
}
/**
* Applies the bindable options to a text component.
*
* @param comp the component
* @param member the binding member
* @param options the options from the @{@link Bindable} annotation
*/
public void applyBindingOptions(FxTextComponent comp, BindingMember member, String options) {
applyBindingOptions(comp, null, member, options);
}
/**
* Applies the bindable options to a table column.
*
* @param columnConfiguration the column configuration
* @param member the binding member
* @param options the options from the @{@link Bindable} annotation
*/
@SuppressWarnings("rawtypes")
public void applyBindingOptions(TableColumnConfiguration columnConfiguration, BindingMember member, String options) {
applyBindingOptions(null, columnConfiguration, member, options);
}
@SuppressWarnings("rawtypes")
private void applyBindingOptions(FxTextComponent comp, TableColumnConfiguration columnConfiguration, BindingMember member, String options) {
if (options != null) {
StringTokenizer stok = new StringTokenizer(options.toUpperCase(), ",");
while (stok.hasMoreTokens()) {
String token = stok.nextToken().trim();
boolean processed;
if (comp != null) {
processed = applyBindingOption(comp, member, token);
}
else if (columnConfiguration != null) {
processed = applyBindingOption(columnConfiguration, member, token);
}
else {
throw new BindingException("unsupported @Bindable target " + member);
}
if (!processed) {
throw new BindingException("unsupported @Bindable option \"" + token + "\") in " + member);
}
}
}
}
/**
* Applies a single binding option to a text component.
*
* @param comp the component
* @param member the binding member
* @param option the option
* @return true if option known and applied, false if unknown option
*/
protected boolean applyBindingOption(FxTextComponent comp, BindingMember member, String option) {
boolean processed = false;
if (Constants.BIND_UC.equals(option)) {
comp.setCaseConversion(CaseConversion.UPPER_CASE);
processed = true;
}
else if (Constants.BIND_LC.equals(option)) {
comp.setCaseConversion(CaseConversion.LOWER_CASE);
processed = true;
}
else if (Constants.BIND_AUTOSELECT.equals(option)) {
comp.setAutoSelect(true);
processed = true;
}
else if (("-" + Constants.BIND_AUTOSELECT).equals(option)) {
comp.setAutoSelect(false);
processed = true;
}
else if (Constants.BIND_UTC.equals(option)) {
comp.setUTC(true);
processed = true;
}
else if (("-" + Constants.BIND_UTC).equals(option)) {
comp.setUTC(false);
processed = true;
}
else if (Constants.BIND_UNSIGNED.equals(option)) {
comp.setUnsigned(true);
processed = true;
}
else if (("-" + Constants.BIND_UNSIGNED).equals(option)) {
comp.setUnsigned(false);
processed = true;
}
else if (Constants.BIND_DIGITS.equals(option)) {
comp.setValidChars(Constants.DIGITS);
processed = true;
}
else if (option.startsWith(Constants.BIND_COLS + "=")) {
try {
comp.setColumns(Integer.parseInt(option.substring(Constants.BIND_COLS.length() + 1)));
processed = true;
}
catch (NumberFormatException ex) {
throw new BindingException("invalid " + Constants.BIND_COLS + " @Bindable option \"" +
option + "\") in " + member, ex);
}
}
else if (option.startsWith(Constants.BIND_MAXCOLS + "=")) {
try {
comp.setMaxColumns(Integer.parseInt(option.substring(Constants.BIND_MAXCOLS.length() + 1)));
processed = true;
}
catch (NumberFormatException ex) {
throw new BindingException("invalid " + Constants.BIND_MAXCOLS + " @Bindable option \"" +
option + "\") in " + member, ex);
}
}
else if (option.startsWith(Constants.BIND_SCALE + "=")) {
try {
comp.setScale(Integer.parseInt(option.substring(Constants.BIND_SCALE.length() + 1)));
processed = true;
}
catch (NumberFormatException ex) {
throw new BindingException("invalid " + Constants.BIND_SCALE + " @Bindable option \"" +
option + "\") in " + member, ex);
}
}
else if (option.startsWith(Constants.BIND_LINES + "=")) {
if (comp instanceof TextArea) {
try {
((TextArea) comp).setPrefRowCount(Integer.parseInt(option.substring(Constants.BIND_LINES.length() + 1)));
processed = true;
}
catch (NumberFormatException ex) {
throw new BindingException("invalid " + Constants.BIND_LINES + " @Bindable option \"" +
option + "\") in " + member, ex);
}
}
else {
throw new BindingException(Constants.BIND_LINES + " @Bindable option \"" +
option + "\") in " + member + " not applicable to " + comp.getClass().getName());
}
}
return processed;
}
/**
* Processes an option for a table binding.
*
* @param columnConfiguration the column config
* @param member the binding member
* @param option the option
* @return true if option known and processed, false if unknown option
*/
@SuppressWarnings("rawtypes")
protected boolean applyBindingOption(TableColumnConfiguration columnConfiguration, BindingMember member, String option) {
boolean processed = false;
if (Constants.BIND_UC.equals(option)) {
columnConfiguration.setCaseConversion(Boolean.TRUE);
processed = true;
}
else if (Constants.BIND_LC.equals(option)) {
columnConfiguration.setCaseConversion(Boolean.FALSE);
processed = true;
}
else if (Constants.BIND_AUTOSELECT.equals(option)) {
columnConfiguration.setAutoSelect(Boolean.TRUE);
processed = true;
}
else if (("-" + Constants.BIND_AUTOSELECT).equals(option)) {
columnConfiguration.setAutoSelect(Boolean.FALSE);
processed = true;
}
else if (Constants.BIND_UNSIGNED.equals(option)) {
columnConfiguration.setUnsigned(true);
processed = true;
}
else if (("-" + Constants.BIND_UNSIGNED).equals(option)) {
columnConfiguration.setUnsigned(false);
processed = true;
}
else if (Constants.BIND_DIGITS.equals(option)) {
columnConfiguration.setValidChars(Constants.DIGITS);
processed = true;
}
else if (option.startsWith(Constants.BIND_MAXCOLS + "=")) {
try {
columnConfiguration.setMaxColumns(Integer.parseInt(option.substring(Constants.BIND_MAXCOLS.length() + 1)));
processed = true;
}
catch (NumberFormatException ex) {
throw new BindingException("invalid " + Constants.BIND_MAXCOLS + " @Bindable option \"" +
option + "\") in " + member, ex);
}
}
else if (option.startsWith(Constants.BIND_SCALE + "=")) {
try {
columnConfiguration.setScale(Integer.parseInt(option.substring(Constants.BIND_SCALE.length() + 1)));
processed = true;
}
catch (NumberFormatException ex) {
throw new BindingException("invalid " + Constants.BIND_SCALE + " @Bindable option \"" +
option + "\") in " + member, ex);
}
}
else if (option.startsWith(Constants.BIND_COLS + "=") ||
option.startsWith(Constants.BIND_LINES + "=")) {
// COLS= and LINES= not applicable to table cells: just ignore
processed = true;
}
return processed;
}
/**
* Resizes the width of all columns of a table to fit the displayed content.
* Unlike the "double-click on column separator"-feature this method computes
* the column widths according to the content currently displayed. For tables
* with a large number of rows this is much more efficient than scanning the whole table.
*
* @param table the table view
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public void resizeColumnsToFitContent(TableView> table) {
TableViewSkin> tableSkin = (TableViewSkin>) table.getSkin();
VirtualFlow> virtualFlow = (VirtualFlow>) tableSkin.getChildren().get(1);
IndexedCell> iCell = virtualFlow.getFirstVisibleCell();
if (iCell != null) {
int first = iCell.getIndex();
iCell = virtualFlow.getLastVisibleCell();
if (iCell != null) {
int last = iCell.getIndex();
for (TableColumn column: table.getColumns()) {
if (column.isVisible()) {
Callback cellFactory = column.getCellFactory();
if (cellFactory != null) {
TableCell cell = (TableCell) cellFactory.call(column);
if (cell != null) {
Font cellFont = cell.getFont();
// BOLD may not work if header is styled with CSS, but that's ok for default configs
Font headerFont = Font.font(cellFont.getFamily(), FontWeight.BOLD, cellFont.getSize());
cell.getProperties().put("deferToParentPrefWidth", Boolean.TRUE); // see TableCellSkinBase
double maxWidth = 20; // don't go below this so that column remains visible
// apply header text first
cell.updateTableColumn(column);
cell.updateTableView(table);
cell.setText(column.getText());
cell.setGraphic(column.getGraphic());
cell.setFont(headerFont);
maxWidth = maxColumnWidth(maxWidth, cell, tableSkin);
cell.setFont(cellFont);
for (int row=first; row <= last; row++) {
cell.updateIndex(row);
maxWidth = maxColumnWidth(maxWidth, cell, tableSkin);
}
table.resizeColumn(column, maxWidth - column.getWidth());
}
}
}
}
}
}
}
/**
* Resizes the width of all columns of a tree table to fit the displayed content.
* Unlike the "double-click on column separator"-feature this method computes
* the column widths according to the content currently displayed. For tables
* with a large number of rows this is much more efficient than scanning the whole table.
*
* @param table the tree table view
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public void resizeColumnsToFitContent(TreeTableView> table) {
TreeTableViewSkin> tableSkin = (TreeTableViewSkin>) table.getSkin();
VirtualFlow> virtualFlow = (VirtualFlow>) tableSkin.getChildren().get(1);
IndexedCell> iCell = virtualFlow.getFirstVisibleCell();
if (iCell != null) {
int first = iCell.getIndex();
iCell = virtualFlow.getLastVisibleCell();
if (iCell != null) {
int last = iCell.getIndex();
boolean firstVisibleColumn = true;
for (TreeTableColumn column: table.getColumns()) {
if (column.isVisible()) {
Callback cellFactory = column.getCellFactory();
if (cellFactory != null) {
TreeTableCell cell = (TreeTableCell) cellFactory.call(column);
if (cell != null) {
Font cellFont = cell.getFont();
// BOLD may not work if header is styled with CSS, but that's ok for default configs
Font headerFont = Font.font(cellFont.getFamily(), FontWeight.BOLD, cellFont.getSize());
cell.getProperties().put("deferToParentPrefWidth", Boolean.TRUE); // see TableCellSkinBase
double maxWidth = 20; // don't go below this so that column remains visible
// apply header text first
cell.updateTreeTableColumn(column);
cell.updateTreeTableView(table);
cell.setText(column.getText());
cell.setGraphic(column.getGraphic());
cell.setFont(headerFont);
maxWidth = maxColumnWidth(maxWidth, cell, tableSkin);
cell.setFont(cellFont);
for (int row=first; row <= last; row++) {
cell.updateIndex(row);
maxWidth = maxColumnWidth(maxWidth, cell, tableSkin);
}
if (firstVisibleColumn) {
maxWidth += 50; // extra space for navigation icon
firstVisibleColumn = false;
}
table.resizeColumn(column, maxWidth - column.getWidth());
}
}
}
}
}
}
}
/**
* Delivers a lis of all showing stages.
*
* @return the stages
*/
public ObservableList getAllShowingStages() {
ObservableList stages = FXCollections.observableArrayList();
Window.getWindows().forEach(w -> {
if (w instanceof Stage) {
stages.add((Stage) w);
}
});
return stages;
}
/**
* Calculates the location of a window so that it will be centered on the screen.
*
* @param window the window
* @return the location (top left corner)
*/
public Point2D determineCenteredLocation(Window window) {
Rectangle2D screenSize = Screen.getPrimary().getVisualBounds();
// align sizes (for computation below)
double windowWidth = window.getWidth();
double windowHeight = window.getHeight();
if (window.getWidth() > screenSize.getWidth()) {
windowWidth = screenSize.getWidth();
}
if (windowHeight > screenSize.getHeight()) {
windowHeight = screenSize.getHeight();
}
return new Point2D((screenSize.getWidth() - windowWidth) / 2.0,
(screenSize.getHeight() - windowHeight) / 2.0);
}
/**
* Calculates the position of a stage on the screen so that
* it is being display in an optimal manner.
*
* @param stage the stage to be positioned on the screen
* @return the location
*/
public Point2D determinePreferredStageLocation(Stage stage) {
Point2D location = new Point2D(stage.getX(), stage.getY());
if (location.getX() == 0.0 && location.getY() == 0.0) { // if no position yet
// place in the middle of the owner if possibe
Window owner = stage.getOwner();
if (owner != null) {
location = new Point2D(owner.getX() + (owner.getWidth() - stage.getWidth()) / 2.0,
owner.getY() + (owner.getHeight() - stage.getHeight()) / 2.0);
}
else {
// not much we can do: center it
location = determineCenteredLocation(stage);
}
}
return determineAlignedStageLocation(stage, location);
}
/**
* Calculates the location of a stage so that it
* is completely visible on the screen, using a "free" spot.
*
* @param stage the current stage
* @param location the desired (not necessarily current!) location
* @return the aligned location
*/
public Point2D determineAlignedStageLocation(Stage stage, Point2D location) {
Rectangle2D screenSize = Screen.getPrimary().getVisualBounds();
double maxWidth = screenSize.getWidth() - screenSize.getWidth() / XY_STEP_DIVISOR;
double maxHeight = screenSize.getHeight() - screenSize.getHeight() / XY_STEP_DIVISOR;
double minX = screenSize.getWidth() / XY_STEP_DIVISOR;
double minY = screenSize.getHeight() / XY_STEP_DIVISOR;
if (location.getX() + stage.getWidth() > maxWidth) {
location = new Point2D(maxWidth - stage.getWidth(), location.getY());
}
if (location.getX() < 0.0) {
location = new Point2D(0.0, location.getY());
}
else if (location.getX() < minX && location.getX() + stage.getWidth() < maxWidth) {
location = new Point2D(minX, location.getY());
}
if (location.getY() + stage.getHeight() > maxHeight) {
location = new Point2D(location.getX(), maxHeight - stage.getHeight());
}
if (location.getY() < 0.0) {
location = new Point2D(location.getX(), 0.0);
}
else if (location.getY() < minY && location.getY() + stage.getHeight() < maxHeight) {
location = new Point2D(location.getX(), minY);
}
return determineFreeStageLocation(stage, location);
}
/**
* Gets the virtual flow for a given control.
*
* @param control the control
* @return the virtual flow, null if no virtual flow associated to the control
*/
@SuppressWarnings("rawtypes")
public VirtualFlow getVirtualFlow(Skinnable control) {
VirtualFlow flow = null;
Skin skin = control.getSkin();
if (skin instanceof SkinBase) {
ObservableList children = ((SkinBase) skin).getChildren();
if (children != null) {
for (Object child : children) {
if (child instanceof VirtualFlow) {
flow = (VirtualFlow) child;
break;
}
}
}
}
return flow;
}
/**
* Computes the row number to scrollTo in order to position the given row in the middle of the viewport.
*
* @param control the control, usually a table- or treeview
* @param row the row to position in the middle of the viewport, if possible
* @return the row number to pass to the scrollTo method
*/
@SuppressWarnings("rawtypes")
public int computeScrollToCentered(Skinnable control, int row) {
VirtualFlow flow = getVirtualFlow(control);
if (flow != null) {
IndexedCell firstVisibleCell = flow.getFirstVisibleCell();
IndexedCell lastVisibleCell = flow.getLastVisibleCell();
if (firstVisibleCell != null && lastVisibleCell != null) {
int firstRow = firstVisibleCell.getIndex();
int lastRow = lastVisibleCell.getIndex();
row -= (lastRow - firstRow) / 2;
if (row < 0) {
row = 0;
}
}
}
return row;
}
/**
* Gets the separator string for CSV exports.
*
* @return the separator, default is ";"
*/
public String getCsvSeparator() {
return ";";
}
/**
* Update the maximum column width.
*
* @param maxWidth the current max witdh
* @param cell the cell
* @param tableSkin the skin
* @return the updates width
*/
@SuppressWarnings("rawtypes")
private double maxColumnWidth(double maxWidth, TableCell cell, TableViewSkin> tableSkin) {
if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
tableSkin.getChildren().add(cell);
cell.applyCss();
maxWidth = Math.max(maxWidth, cell.prefWidth(-1));
tableSkin.getChildren().remove(cell);
}
return maxWidth;
}
/**
* Update the maximum column width.
*
* @param maxWidth the current max witdh
* @param cell the cell
* @param treeTableSkin the skin
* @return the updates width
*/
@SuppressWarnings("rawtypes")
private double maxColumnWidth(double maxWidth, TreeTableCell cell, TreeTableViewSkin> treeTableSkin) {
if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
treeTableSkin.getChildren().add(cell);
cell.applyCss();
maxWidth = Math.max(maxWidth, cell.prefWidth(-1));
treeTableSkin.getChildren().remove(cell);
}
return maxWidth;
}
private static final double XY_STEP_DIVISOR = 32.0;
private static final int MAX_LOCATION_LOOP = 4;
/**
* Determines a location for a stage that does not overlap with stages belonging to the same owner.
*
* @param stage the stage
* @param startLocation the start location
* @return the free location
*/
private Point2D determineFreeStageLocation(Stage stage, Point2D startLocation) {
// get screensize
Rectangle2D screenSize = Screen.getPrimary().getVisualBounds();
final double stepX = screenSize.getWidth() / XY_STEP_DIVISOR;
final double stepY = screenSize.getHeight() / XY_STEP_DIVISOR;
// compute the center of the window
double x = startLocation.getX() + stage.getWidth() / 2.0;
double y = startLocation.getY() + stage.getHeight() / 2.0;
// initial diff-stepping
double dx;
double dy;
if (x > screenSize.getWidth() / 2.0) {
dx = -stepX;
}
else {
dx = stepX;
}
if (y > screenSize.getHeight() / 2.0) {
dy = -stepY;
}
else {
dy = stepY;
}
for (int loop=0; loop < MAX_LOCATION_LOOP; loop++) {
boolean abort = false;
Point2D location = startLocation;
while (!abort && isStageOverlapping(stage, location, dx, dy)) {
location = location.add(dx, dy);
if (location.getX() < 0.0) {
location = new Point2D(0.0, location.getY());
abort = true;
}
if (location.getX() + stage.getWidth() > screenSize.getWidth()) {
location = new Point2D(screenSize.getWidth() - stage.getWidth(), location.getY());
abort = true;
}
if (location.getY() < 0.0) {
location = new Point2D(location.getX(), 0.0);
abort = true;
}
if (location.getY() + stage.getHeight() > screenSize.getHeight()) {
location = new Point2D(location.getX(), screenSize.getHeight() - stage.getHeight());
abort = true;
}
}
if (!abort) {
// !isStageOverlapping
return location;
}
// try other direction clockwise
if (dx > 0.0 && dy > 0.0) {
dx = -stepX;
}
else if (dx < 0.0 && dy > 0.0) {
dy = -stepY;
}
else if (dx < 0.0 && dy < 0.0) {
dx = stepX;
}
else if (dx > 0 && dy < 0) {
dy = stepY;
}
}
return startLocation;
}
/**
* Checks if a stage would overlay any other stage that belongs to the same owner.
*
* @param stage the stage
* @param location the requested location for this stage
* @param dx maximum overlapping width
* @param dy maximum overlapping height
* @return true if overlapping, false if on free space
*/
private boolean isStageOverlapping(Stage stage, Point2D location, double dx, double dy) {
Window owner = stage.getOwner();
if (dx < 0.0) {
dx = -dx;
}
if (dy < 0.0) {
dy = -dy;
}
for (Stage w: getAllShowingStages()) {
Window o = w.getOwner();
if (w != stage && o == owner &&
((location.getX() <= w.getX() + dx &&
location.getX() + stage.getWidth() + dx >= w.getX() + w.getWidth()) ||
(location.getY() <= w.getY() + dy &&
location.getY() + stage.getHeight() + dy >= w.getY() + w.getHeight()))) {
return true;
}
}
return false;
}
}