/*******************************************************************************
* Copyright (c) 2015 itemis AG 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:
* Matthias Wienand (itemis AG) - initial API and implementation
* Alexander Nyßen (itemis AG) - major refactorings
*******************************************************************************/
package org.eclipse.gef.fx.swt.controls;
import org.eclipse.gef.fx.swt.canvas.FXCanvasEx;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.embed.swt.FXCanvas;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.Region;
import javafx.stage.Window;
/**
* The FXControlAdapter can be used to embed SWT controls into a JavaFX scene
* graph.
*
* @author mwienand
* @author anyssen
*
* @param <T>
* The SWT Control class which is wrapped by this
* {@link FXControlAdapter}.
*/
public class FXControlAdapter<T extends Control> extends Region {
/**
* The {@link FXControlAdapter.IControlFactory} can be used in conjunction
* with {@link FXControlAdapter} to create the wrapped SWT {@link Control}
* when the surrounding {@link FXCanvas} changes.
*
* @author anyssen
*
* @param <T>
* The kind of {@link Control} to be created by this factory
*/
public interface IControlFactory<T extends Control> {
/**
* Creates the {@link Control} as a child of the given {@link Composite}
* .
*
* @param parent
* The {@link Composite} in which to create the
* {@link Control}.
* @return The new {@link Control}.
*/
public T createControl(Composite parent);
}
private static final int[] FORWARD_SWT_EVENT_TYPES = new int[] {
SWT.HardKeyDown, SWT.HardKeyUp, SWT.KeyDown, SWT.KeyUp, SWT.Gesture,
SWT.MouseDown, SWT.MouseEnter, SWT.MouseExit,
SWT.MouseHorizontalWheel, SWT.MouseHover, SWT.MouseMove,
SWT.MouseUp, SWT.MouseVerticalWheel, SWT.Move, SWT.Traverse,
SWT.Verify, SWT.FocusIn };
private FXCanvas canvas;
private T control;
private Listener swtToFXEventForwardingListener;
private ChangeListener<Scene> sceneChangeListener;
private ChangeListener<Window> sceneWindowChangeListener;
private ChangeListener<Boolean> focusChangeListener;
private IControlFactory<T> controlFactory;
/**
* Creates a new {@link FXControlAdapter} which uses the given
* {@link IControlFactory} for the creation of the SWT {@link Control}.
*
* @param controlFactory
* The {@link IControlFactory} to use to create the SWT
* {@link Control}.
*/
public FXControlAdapter(IControlFactory<T> controlFactory) {
// lazy creation of control in case canvas is obtained
this.controlFactory = controlFactory;
init();
}
/**
* Creates a new {@link FXControlAdapter} which wraps the given SWT
* {@link Control}.
*
* @param control
* The SWT {@link Control} to wrap in this
* {@link FXControlAdapter}.
*/
public FXControlAdapter(T control) {
// detect SwtFXCanvas via given control
canvas = getFXCanvas(control);
if (canvas == null) {
throw new IllegalArgumentException(
"Control has to be parented by SwtFXCanvas.");
}
// assign control and register listeners
setControl(control);
init();
}
@Override
protected double computeMaxHeight(double width) {
return computePrefHeight(width);
}
@Override
protected double computeMaxWidth(double height) {
return computePrefWidth(height);
}
@Override
protected double computeMinHeight(double width) {
return computePrefHeight(width);
}
@Override
protected double computeMinWidth(double height) {
return computePrefWidth(height);
}
@Override
protected double computePrefHeight(double width) {
if (control == null) {
return 0;
}
return control.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
}
@Override
protected double computePrefWidth(double height) {
if (control == null) {
return 0;
}
return control.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
}
/**
* Deactivates this {@link FXControlAdapter}, so that the SWT
* {@link Control} will not be re-created when the {@link FXCanvas} changes.
*/
public void dispose() {
unregisterListeners();
}
/**
* We do not manage children. Therefore, it is illegal to alter the children
* list in any way.
*/
@Override
protected ObservableList<Node> getChildren() {
return getChildrenUnmodifiable();
}
/**
* Returns the SWT {@link Control} that is wrapped by this
* {@link FXControlAdapter}.
*
* @return The SWT {@link Control} that is wrapped by this
* {@link FXControlAdapter}.
*/
public T getControl() {
return control;
}
/**
* Returns the first {@link FXCanvas} which is found by walking up the
* widget hierarchy of the given {@link Control}. If no {@link FXCanvas} can
* be found, <code>null</code> is returned.
*
* @param control
* The {@link Control} for which to identify the surrounding
* {@link FXCanvas}.
* @return The first {@link FXCanvas} which is found by walking up the
* widget hierarchy of the given {@link Control}, or
* <code>null</code>.
*/
protected FXCanvas getFXCanvas(Control control) {
Control candidate = control;
while (candidate != null) {
candidate = candidate.getParent();
if (candidate instanceof FXCanvas) {
return (FXCanvas) candidate;
}
}
return null;
}
/**
* Returns the {@link FXCanvas} which embeds the {@link Scene} which
* contains the given {@link Node}.
*
* @param node
* The {@link Node} for which the embedding {@link FXCanvas} is
* determined.
* @return The {@link FXCanvas} which embeds the {@link Scene} which
* contains the given {@link Node}.
*/
protected FXCanvas getFXCanvas(Node node) {
if (node == null) {
return null;
}
return FXCanvasEx.getFXCanvas(node.getScene());
}
/**
* Hooks the given {@link Control} into the JavaFX scene graph, for example,
* registering event forwarding from SWT to JavaFX.
*
* @see #registerSwtToFXEventForwarders(FXCanvas)
* @param control
* The {@link Control} which is wrapped by this
* {@link FXControlAdapter}.
*/
protected void hookControl(T control) {
FXCanvas swtFXCanvas = getFXCanvas(control);
if (swtFXCanvas == null || swtFXCanvas != canvas) {
throw new IllegalArgumentException(
"Control needs to be hooked to the same canvas as this adapter.");
}
// register SWT listeners to forward events to JavaFX
registerSwtToFXEventForwarders(swtFXCanvas);
}
/**
* Initializes this {@link FXControlAdapter}. Per default, this
* {@link FXControlAdapter} is added to the focus traversal cycle and JavaFX
* listeners are registered for forwarding JavaFX state to SWT.
*
* @see #registerListeners()
*/
protected void init() {
// by default, be part of focus traversal cycle
focusTraversableProperty().set(true);
// register listeners
registerListeners();
}
/**
* Registers JavaFX listeners for forwarding JavaFX state to SWT. Among
* other things, this registers a listener for {@link Scene} changes which
* will then hook the SWT {@link Control} to the {@link FXCanvas} of the new
* {@link Scene}.
*
* @see #unregisterListeners()
* @see #setCanvas(FXCanvas)
*/
protected void registerListeners() {
focusChangeListener = new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean hadFocus, Boolean hasFocus) {
// if we obtained focus from JavaFX and the SWT control is not
// focused, forward the focus.
if (control != null && !control.isFocusControl() && !hadFocus
&& hasFocus) {
control.forceFocus();
}
}
};
focusedProperty().addListener(focusChangeListener);
sceneChangeListener = new ChangeListener<Scene>() {
@Override
public void changed(ObservableValue<? extends Scene> observable,
Scene oldValue, Scene newValue) {
// if the scene changed, see if we can obtain an FXCanvasEx
setCanvas(FXCanvasEx.getFXCanvas(newValue));
// register/unregister listener to detect FXCanvasEx changes of
// new scene
if (oldValue != null) {
oldValue.windowProperty()
.removeListener(sceneWindowChangeListener);
sceneWindowChangeListener = null;
}
if (newValue != null) {
sceneWindowChangeListener = new ChangeListener<Window>() {
@Override
public void changed(
ObservableValue<? extends Window> observable,
Window oldValue, Window newValue) {
setCanvas(
newValue != null
? FXCanvasEx.getFXCanvas(
newValue.getScene())
: null);
}
};
newValue.windowProperty()
.addListener(sceneWindowChangeListener);
}
}
};
sceneProperty().addListener(sceneChangeListener);
}
/**
* Registers SWT to JavaFX event forwarders for the given {@link FXCanvas}.
*
* @see #unregisterSwtToFXEventForwarders()
* @param newCanvas
* The {@link FXCanvas} for which event forwarding is registered.
*/
protected void registerSwtToFXEventForwarders(final FXCanvas newCanvas) {
swtToFXEventForwardingListener = new Listener() {
@Override
public void handleEvent(Event event) {
switch (event.type) {
case SWT.FocusIn:
requestFocus();
break;
default:
Point location = control.getLocation();
event.x += location.x;
event.y += location.y;
newCanvas.notifyListeners(event.type, event);
}
}
};
for (int eventType : FORWARD_SWT_EVENT_TYPES) {
control.addListener(eventType, swtToFXEventForwardingListener);
}
}
@Override
public void relocate(double paramDouble1, double paramDouble2) {
super.relocate(paramDouble1, paramDouble2);
updateSwtBounds();
}
@Override
public void resize(double width, double height) {
super.resize(width, height);
updateSwtBounds();
}
/**
* Changes the {@link FXCanvas} in which the {@link Control} is hooked. An
* {@link IControlFactory} has to be available for re-creating the
* {@link Control} within the new {@link FXCanvas}, otherwise an exception
* is thrown.
*
* @see #setControl(Control)
* @param newCanvas
* The new {@link FXCanvas} for the {@link Control}.
* @throws IllegalArgumentException
* when the {@link FXCanvas} is changed, but no
* {@link IControlFactory} is available.
*/
protected void setCanvas(FXCanvas newCanvas) {
// if we do not have a control factory, we are bound to an existing
// control and will not be able to handle canvas changes
if (controlFactory == null) {
if (newCanvas != null && this.canvas != newCanvas) {
throw new IllegalArgumentException(
"May not bind this adapter to another SwtFXCanvas than that of the adapted control.");
}
} else {
// use control factory to dispose/create controls as needed upon
// canvas changes
FXCanvas oldCanvas = this.canvas;
if (oldCanvas != null && oldCanvas != newCanvas) {
T oldControl = getControl();
setControl(null);
oldControl.dispose();
oldControl = null;
}
this.canvas = newCanvas;
if (newCanvas != null && oldCanvas != newCanvas) {
T newControl = controlFactory.createControl(newCanvas);
setControl(newControl);
}
}
}
/**
* Sets the {@link Control} of this {@link FXControlAdapter} to the given
* value and {@link #hookControl(Control) hooks} or
* {@link #unhookControl(Control) unhooks} the {@link Control},
* respectively.
*
* @see #hookControl(Control)
* @see #unhookControl(Control)
* @param control
* The new {@link Control} for this {@link FXControlAdapter}.
*/
protected void setControl(T control) {
T oldControl = this.control;
if (oldControl != null) {
unhookControl(oldControl);
}
this.control = control;
if (control != null) {
hookControl(control);
}
}
/**
* Unhooks the given {@link Control} from the JavaFX scene graph, for
* example, unregistering event forwarding from SWT to JavaFX.
*
* @see #hookControl(Control)
* @see #unregisterSwtToFXEventForwarders()
* @param control
* The {@link Control} which is wrapped by this
* {@link FXControlAdapter}.
*/
protected void unhookControl(T control) {
unregisterSwtToFXEventForwarders();
}
/**
* Unregisters the listeners which have previously been registered during
* {@link #registerListeners()}.
*/
protected void unregisterListeners() {
sceneProperty().removeListener(sceneChangeListener);
focusedProperty().removeListener(focusChangeListener);
}
/**
* Unregisters the event forwarders which have previously been registered
* during {@link #registerSwtToFXEventForwarders(FXCanvas)}.
*/
protected void unregisterSwtToFXEventForwarders() {
for (int eventType : FORWARD_SWT_EVENT_TYPES) {
control.removeListener(eventType, swtToFXEventForwardingListener);
}
swtToFXEventForwardingListener = null;
}
/**
* Updates the {@link Control#setBounds(int, int, int, int) bounds} of the
* {@link Control} which is wrapped by this {@link FXControlAdapter}. This
* method is automatically called when this {@link FXControlAdapter} is
* {@link #relocate(double, double) relocated} or
* {@link #resize(double, double) resized}.
*/
public void updateSwtBounds() {
if (control == null) {
return;
}
Bounds bounds = localToScene(getLayoutBounds());
control.setBounds((int) Math.ceil(bounds.getMinX()),
(int) Math.ceil(bounds.getMinY()),
(int) Math.ceil(bounds.getWidth()),
(int) Math.ceil(bounds.getHeight()));
}
}