blob: d20d224ade2d45264514e7ca6878978229fd266e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014 BestSolution.at and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation
*******************************************************************************/
package org.eclipse.fx.ui.controls;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.fx.core.Subscription;
import org.eclipse.fx.core.ThreadSynchronize.BlockCondition;
import org.eclipse.fx.core.geom.Size;
import org.eclipse.fx.core.text.TextUtil;
import org.eclipse.fx.ui.controls.styledtext.StyledString;
import org.eclipse.fx.ui.controls.styledtext.StyledStringSegment;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.BoundingBox;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.PopupWindow;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.util.Duration;
/**
* Utility methods
*
* @since 1.2
*/
public class Util {
/**
* Tag used to exclude a node from finding
*/
public static final String FIND_NODE_EXCLUDE = "findNodeExclude"; //$NON-NLS-1$
/**
* Boolean to indicate if mnemonic fixing is enabled
*
* @noreference
*/
public static final boolean MNEMONICS_FIX = !Boolean.getBoolean("efxclipse.mnemonicfix.disabled"); //$NON-NLS-1$
/**
* Dump the scene graph to a formatted string
*
* @param n
* the node to start with
* @return the dump as a formatted XML
*/
public static String dumpSceneGraph(Node n) {
return new SceneGraphDumper().dump(n).toString();
}
static class SceneGraphDumper {
private StringBuilder sb = new StringBuilder();
private int ident = 0;
public StringBuilder dump(Node n) {
for (int i = 0; i < this.ident; i++) {
this.sb.append(" "); //$NON-NLS-1$
}
this.ident++;
this.sb.append("<" + n.getClass().getName() + " styleClass=\"" //$NON-NLS-1$ //$NON-NLS-2$
+ n.getStyleClass() + "\">\n"); //$NON-NLS-1$
if (n instanceof Parent) {
for (Node subNode : ((Parent) n).getChildrenUnmodifiable()) {
dump(subNode);
}
}
this.ident--;
for (int i = 0; i < this.ident; i++) {
this.sb.append(" "); //$NON-NLS-1$
}
this.sb.append("</" + n.getClass().getName() + ">\n"); //$NON-NLS-1$ //$NON-NLS-2$
return this.sb;
}
}
/**
* Create a scenegraph node from the styled string
*
* @param s
* the string
* @return a scenegraph node
*/
public static Node toNode(StyledString s) {
List<Text> segList = new ArrayList<>();
for (StyledStringSegment seg : s.getSegmentList()) {
Text t = new Text(seg.getText());
t.getStyleClass().addAll(seg.getStyleClass());
segList.add(t);
}
TextFlow textFlow = new TextFlow(segList.toArray(new Node[0]));
textFlow.getStyleClass().add("styled-string"); //$NON-NLS-1$
return textFlow;
}
/**
* Find a node in all windows
*
* @param w
* the preferred window
* @param screenX
* the screen x
* @param screenY
* the screen y
* @return the node or <code>null</code>
*/
@SuppressWarnings("deprecation")
public static Node findNode(@Nullable Window w, double screenX, double screenY) {
if (w != null && new BoundingBox(w.getX(), w.getY(), w.getWidth(), w.getHeight()).contains(screenX, screenY)) {
return findNode(w.getScene().getRoot(), screenX, screenY);
}
Iterator<Window> impl_getWindows = JavaFXCompatUtil.getAllWindows().iterator();
List<Window> sortedWindows = new ArrayList<>();
Map<Window, List<Window>> parentChildRelation = new HashMap<>();
while (impl_getWindows.hasNext()) {
Window window = impl_getWindows.next();
Window owner;
if (window instanceof Stage) {
owner = ((Stage) window).getOwner();
} else if (window instanceof PopupWindow) {
owner = ((PopupWindow) window).getOwnerWindow();
} else {
owner = null;
}
if (owner == null) {
sortedWindows.add(window);
} else {
List<Window> list = parentChildRelation.get(owner);
if (list == null) {
list = new ArrayList<>();
parentChildRelation.put(owner, list);
}
list.add(window);
}
}
while (!parentChildRelation.isEmpty()) {
for (Window rw : sortedWindows.toArray(new Window[0])) {
List<Window> list = parentChildRelation.remove(rw);
if (list != null) {
sortedWindows.addAll(list);
}
}
}
Collections.reverse(sortedWindows);
for (Window window : sortedWindows) {
if (!FIND_NODE_EXCLUDE.equals(window.getUserData()) && new BoundingBox(window.getX(), window.getY(), window.getWidth(), window.getHeight()).contains(screenX, screenY)) {
return findNode(window.getScene().getRoot(), screenX, screenY);
}
}
return null;
}
/**
* Find all node at the given x/y location starting the search from the
* given node
*
* @param n
* the node to use as the start
* @param screenX
* the screen x
* @param screenY
* the screen y
* @return the node or <code>null</code>
*/
public static Node findNode(Node n, double screenX, double screenY) {
Node rv = null;
if (!n.isVisible()) {
return rv;
}
Point2D b = n.screenToLocal(screenX, screenY);
if (n.getBoundsInLocal().contains(b) && !FIND_NODE_EXCLUDE.equals(n.getUserData())) {
rv = n;
if (n instanceof Parent) {
List<Node> cList = ((Parent) n).getChildrenUnmodifiable().stream().filter(no -> no.isVisible()).collect(Collectors.toList());
for (Node c : cList) {
Node cn = findNode(c, screenX, screenY);
if (cn != null) {
rv = cn;
break;
}
}
}
}
return rv;
}
/**
* Get the window property of a node
*
* @param n
* the node the window property to observe
* @return the property
*/
public static ObservableValue<Window> windowProperty(Node n) {
ObjectProperty<Window> w = new SimpleObjectProperty<Window>();
ChangeListener<Window> l = (o, oldV, newV) -> w.set(newV);
n.sceneProperty().addListener((o, oldV, newV) -> {
if (oldV != null) {
oldV.windowProperty().removeListener(l);
}
if (newV != null) {
newV.windowProperty().addListener(l);
}
});
return w;
}
/**
* Bind the content to the source list to the target and apply the converter
* in between
*
* @param target
* the target list
* @param sourceList
* the source list
* @param converterFunction
* the function used to convert
* @param <T>
* the target type
* @param <E>
* the source type
* @return the subscription to dispose the binding
* @deprecated use
* {@link FXObservableUtils#bindContent(List, ObservableList, Function)}
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public static <T, E> Subscription bindContent(List<T> target, ObservableList<E> sourceList, Function<E, T> converterFunction) {
List<T> list = sourceList.stream().map(converterFunction).collect(Collectors.toList());
if (target instanceof ObservableList<?>) {
((ObservableList) target).setAll(list);
} else {
target.clear();
((List) target).addAll(list);
}
ListChangeListener<E> l = change -> {
while (change.next()) {
if (change.wasPermutated()) {
target.subList(change.getFrom(), change.getTo()).clear();
target.addAll(change.getFrom(), transformList(change.getList().subList(change.getFrom(), change.getTo()), converterFunction));
} else {
if (change.wasRemoved()) {
target.subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
}
if (change.wasAdded()) {
target.addAll(change.getFrom(), transformList(change.getAddedSubList(), converterFunction));
}
}
}
};
sourceList.addListener(l);
return new Subscription() {
@Override
public void dispose() {
sourceList.removeListener(l);
}
};
}
/**
* Transform the list to another list
*
* @param list
* the list
* @param converterFunction
* the converter function
* @return the list
*/
public static <T, E> List<T> transformList(List<? extends E> list, Function<E, T> converterFunction) {
return list.stream().map(converterFunction).collect(Collectors.toList());
}
/**
* Install a hover callback
*
* @param node
* the node the hover is installed on
* @param delay
* the delay
* @param hoverConsumer
* the consumer
* @return subscription
*/
public static Subscription installHoverCallback(Node node, Duration delay, Consumer<MouseEvent> hoverConsumer) {
Timeline t = new Timeline(new KeyFrame(delay));
AtomicReference<MouseEvent> event = new AtomicReference<>();
t.setOnFinished(e -> {
if (event.get() != null) {
hoverConsumer.accept(event.get());
}
});
EventHandler<MouseEvent> moveHandler = e -> {
event.set(e);
t.stop();
t.playFromStart();
};
EventHandler<MouseEvent> exitHandler = e -> {
t.stop();
};
node.addEventHandler(MouseEvent.MOUSE_MOVED, moveHandler);
node.addEventHandler(MouseEvent.MOUSE_EXITED, exitHandler);
return () -> {
node.removeEventHandler(MouseEvent.MOUSE_MOVED, moveHandler);
node.removeEventHandler(MouseEvent.MOUSE_EXITED, exitHandler);
};
}
/**
* Enter a nested event loop
*
* @param id
* the id of the nested event loop
* @since 2.3.0
*/
public static void enterNestedEventLoop(String id) {
if (org.eclipse.fx.core.Util.isFX9()) {
enterNestedEventLoop9(id);
} else {
enterNestedEventLoop8(id);
}
}
private static void enterNestedEventLoop8(String id) {
try {
Class<?> toolkit = Class.forName("com.sun.javafx.tk.Toolkit"); //$NON-NLS-1$
Object tk = toolkit.getMethod("getToolkit").invoke(null); //$NON-NLS-1$
toolkit.getMethod("enterNestedEventLoop", Object.class).invoke(tk, id); //$NON-NLS-1$
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static void enterNestedEventLoop9(String id) {
try {
Method m = Platform.class.getMethod("enterNestedEventLoop", Object.class); //$NON-NLS-1$
m.invoke(null, id);
} catch (Throwable t) {
enterNestedEventLoop8(id);
}
}
/**
* Exit the nested event loop
*
* @param id
* the nested event loop
* @since 2.3.0
*/
public static void exitNestedEventLoop(String id) {
if (org.eclipse.fx.core.Util.isFX9()) {
exitNestedEventLoop9(id);
} else {
exitNestedEventLoop8(id);
}
}
private static void exitNestedEventLoop8(String id) {
try {
Class<?> toolkit = Class.forName("com.sun.javafx.tk.Toolkit"); //$NON-NLS-1$
Object tk = toolkit.getMethod("getToolkit").invoke(null); //$NON-NLS-1$
toolkit.getMethod("exitNestedEventLoop", Object.class, Object.class).invoke(tk, id, null); //$NON-NLS-1$
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static void exitNestedEventLoop9(String id) {
try {
Method m = Platform.class.getMethod("exitNestedEventLoop", Object.class, Object.class); //$NON-NLS-1$
m.invoke(null, id, null);
} catch (Throwable t) {
exitNestedEventLoop8(id);
}
}
/**
* Wait until the condition is satisfied without blocking the UI-Thread
*
* @param blockCondition
* the condition
* @return the return value of the condition
* @since 2.3.0
*/
public static <T> @Nullable T waitUntil(@NonNull BlockCondition<T> blockCondition) {
AtomicReference<@Nullable T> rv = new AtomicReference<>();
String uuid = UUID.randomUUID().toString();
blockCondition.subscribeUnblockedCallback(new Consumer<T>() {
@Override
public void accept(@Nullable T value) {
rv.set(value);
Util.exitNestedEventLoop(uuid);
}
});
Util.enterNestedEventLoop(uuid);
return rv.get();
}
/**
* Calculate the size of the text with the given font
*
* @param text
* the text
* @param font
* the font
* @return the width
* @since 2.3.0
*/
public static double getTextWidth(String text, Font font, double fontZoomFactor) {
Text t = new Text(text);
t.setFont(Font.font(font.getName(), font.getSize() * fontZoomFactor));
return t.getLayoutBounds().getWidth();
}
/**
* Create a binding for text width calculation.
*
* @param text
* @param font
* @return
*/
public static DoubleBinding createTextWidthBinding(ObservableValue<String> text, ObservableValue<Font> font, ObservableValue<Number> fontZoomFactor) {
return Bindings.createDoubleBinding(() -> {
return getTextWidth(text.getValue(), font.getValue(), fontZoomFactor.getValue().doubleValue());
}, text, font, fontZoomFactor);
}
public static DoubleBinding createTextWidthBinding(String text, ObservableValue<Font> font, ObservableValue<Number> fontZoomFactor) {
return Bindings.createDoubleBinding(() -> {
return getTextWidth(text, font.getValue(), fontZoomFactor.getValue().doubleValue());
}, font, fontZoomFactor);
}
public static double getTextHeight(String text, Font font, double fontZoomFactor) {
Text t = new Text(text);
t.setFont(Font.font(font.getName(), font.getSize() * fontZoomFactor));
return t.getLayoutBounds().getHeight();
}
public static DoubleBinding createTextHeightBinding(String text, ObservableValue<Font> font, ObservableValue<Number> fontZoomFactor) {
return Bindings.createDoubleBinding(() -> {
return getTextHeight(text, font.getValue(), fontZoomFactor.getValue().doubleValue());
}, font, fontZoomFactor);
}
public static boolean isCopyEvent(MouseEvent event) {
if (org.eclipse.fx.core.Util.isMacOS()) {
return event.isAltDown();
} else {
return event.isControlDown();
}
}
private static Cache<CacheKey, Size> FONT_SIZE_CACHE;
private static class CacheKey {
private final Font f;
private final char c;
CacheKey(Font f, char c) {
this.f = f;
this.c = c;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.c;
result = prime * result + ((this.f == null) ? 0 : f.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CacheKey other = (CacheKey) obj;
if (this.c != other.c)
return false;
if (this.f == null) {
if (other.f != null)
return false;
} else if (!this.f.equals(other.f))
return false;
return true;
}
}
/**
* Get the size for the provided character
*
* @param font
* the font
* @param c
* the character
* @return the size
*/
public static Size getSize(Font font, char c) {
if (FONT_SIZE_CACHE == null) {
FONT_SIZE_CACHE = CacheBuilder.newBuilder().maximumSize(20).build();
}
Size rv = FONT_SIZE_CACHE.getIfPresent(new CacheKey(font, c));
if (rv == null) {
Text t = new Text(TextUtil.toString(c));
t.setFont(font);
FONT_SIZE_CACHE.put(new CacheKey(font, c), rv = new Size(t.prefWidth(-1), t.prefHeight(-1)));
}
return rv;
}
/**
* Create a simple background fill
*
* @param p
* the paint
* @return the background
*/
public static Background getSimpleBackground(Paint p) {
return new Background(new BackgroundFill(p, CornerRadii.EMPTY, Insets.EMPTY));
}
}