package com.wilutions.joa.fx;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.wilutions.com.AsyncResult;
import com.wilutions.com.BackgTask;
import com.wilutions.com.ComException;
import com.wilutions.com.Dispatch;
import com.wilutions.com.DispatchImpl;
import com.wilutions.com.JoaDll;
import com.wilutions.com.WindowHandle;
import com.wilutions.joa.OfficeAddin;
import com.wilutions.joactrllib.IJoaBridgeDialog;
import com.wilutions.joactrllib._IJoaBridgeDialogEvents;
import javafx.application.Platform;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
/**
* This is the base class for all modal dialogs.
*
* @param <T>
* Result type of callback expression.
*/
public abstract class ModalDialogFX<T> implements FrameContentFactory {
private static Logger log = Logger.getLogger(ModalDialogFX.class.getName());
/**
* Helper object to show an empty modal dialog in the UI thread of Outlook.
*/
protected IJoaBridgeDialog joaDlg;
/**
* JavaFX frame window placed inside the {@link #joaDlg}.
*/
private EmbeddedFrameFX embeddedFrame = new EmbeddedFrameFX();
/**
* Native window handle of the {@link #joaDlg}
*/
private long hwndParent;
/**
* JavaFX stage. If the owner is a JavaFX window, the dialog is created
* entirely with JavaFX functionality. Either {@link #joaDlg} or
* {@link #fxDlg} is set.
*/
private Stage fxDlg;
/**
* Callback expression received from function
* {@link #showAsync(Object, AsyncResult)}.
*/
protected AsyncResult<T> asyncResult;
/**
* Result that will passed to {@link #asyncResult} when the dialog is
* closed.
*/
private T result;
// Dimensions
private double x, y, width, height, minWidth, maxWidth, minHeight, maxHeight;
/**
* Align dialog box in the center of the owner window.
*/
private boolean centerOnOwner = true;
/**
* Dialog box is re-sizable.
*/
private boolean resizable = true;
/**
* Dialog box has a minimize button.
*/
private boolean minimizeBox = false;
/**
* Dialog box has a maximize button.
*/
private boolean maximizeBox = true;
/**
* Caption
*/
private String title;
/**
* Definition for cancel button ID.
*/
public final static int CANCEL = 0;
/**
* Definition for OK button ID.
*/
public final static int OK = 1;
/**
* Internal processing state.
*
*/
private enum State {
Initialized, HasParentHwnd, IsClosed
};
private State state = State.Initialized;
/**
* Constructor.
*/
public ModalDialogFX() {
}
/**
* Show the dialog box.
*
* If _owner is an instance of Explorer or Inspector, this function opens a dialog box modal to the given explorer or inspector window.
* As a disadvantage, the dialog title is not shown activated when an input element in the form is active.
*
* If _owner is an instance of javafx.stage.Window, this function shows a dialog box modal to the given Window.
* The underlying explorer* or inspector window is not blocked.
*
* @param _owner
* Owner object, explorer or inspector window, an
* implementation of WindowHandle, or a javafx.stage.Window.
*
* @param asyncResult
* Callback expression which is called, when the dialog is
* closed.
*/
public void showAsync(Object _owner, final AsyncResult<T> asyncResult) {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "showAsync(");
if (Platform.isFxApplicationThread()) {
internalShowAsyncInFxThread(_owner, asyncResult);
}
else {
Platform.runLater(() -> internalShowAsyncInFxThread(_owner, asyncResult));
}
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, ")showAsync");
}
/**
* Close dialog and invoke callback expression.
*
* @param result
* Object to be passed to the callback expression.
* @see #showAsync(Object, AsyncResult)
*/
public void finish(T result) {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "finish(");
setResult(result);
close();
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, ")finish");
}
/**
* Close dialog. Invokes the callback expression with the current value of
* {@link #result}.
*
* @see #finish(Object)
* @see #setResult(Object)
*/
public void close() {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "finish(");
Throwable ex = null;
if (joaDlg != null) {
try {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "joaDlg.Close()");
joaDlg.Close();
}
catch (Throwable ex1) {
ex = ex1;
}
}
if (fxDlg != null) {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "fxDlg.close()");
fxDlg.close();
}
if (asyncResult != null) {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "setAsyncResult");
final Throwable fex = ex;
BackgTask.run(() -> {
asyncResult.setAsyncResult(result, fex);
});
}
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, ")finish");
}
/**
* Called when the dialog is closed over its system menu.
*/
public void onSystemMenuClose() {
this.close();
}
/**
* Set callback result.
*
* @param ret
* Result object
* @throws ComException Thrown, if a COM related error occurs.
*/
public void setResult(T ret) {
this.result = ret;
}
public T getResult() {
return this.result;
}
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public void setCenterOnOwner(boolean v) {
this.centerOnOwner = v;
}
public boolean isCenterOnOwner() {
return this.centerOnOwner;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public boolean isResizable() {
return resizable;
}
public void setResizable(boolean resizable) {
this.resizable = resizable;
}
public double getMinWidth() {
return minWidth;
}
public void setMinWidth(double minWidth) {
this.minWidth = minWidth;
}
public double getMaxWidth() {
return maxWidth;
}
public void setMaxWidth(double maxWidth) {
this.maxWidth = maxWidth;
}
public double getMinHeight() {
return minHeight;
}
public void setMinHeight(double minHeight) {
this.minHeight = minHeight;
}
public double getMaxHeight() {
return maxHeight;
}
public void setMaxHeight(double maxHeight) {
this.maxHeight = maxHeight;
}
public boolean isMinimizeBox() {
return minimizeBox;
}
public void setMinimizeBox(boolean minimizeBox) {
this.minimizeBox = minimizeBox;
}
public boolean isMaximizeBox() {
return maximizeBox;
}
public void setMaximizeBox(boolean maximizeBox) {
this.maximizeBox = maximizeBox;
}
/**
* Set event handler for WindowEvent.WINDOW_SHOWN. Only one hander is
* supported. Only the event type WINDOW_SHOWN is supported. The handler
* receives null as source parameter.
*
* @param eventType
* must be WindowEvent.WINDOW_SHOWN
* @param eventHandler
* handler expression
*/
public <E extends Event> void addEventHandler(EventType<E> eventType, EventHandler<? super E> eventHandler) {
embeddedFrame.addEventHandler(eventType, eventHandler);
}
private Integer toWin(double x) {
return Double.valueOf(x).intValue();
}
private void internalShowFxDialogAsync(Window fxOwner, AsyncResult<T> asyncResult) {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "internalShowFxDialogAsync(" + fxOwner);
try {
fxDlg = new Stage();
fxDlg.initOwner(fxOwner);
fxDlg.setTitle(title);
fxDlg.initModality(Modality.APPLICATION_MODAL);
Scene scene = createScene();
fxDlg.setScene(scene);
fxDlg.showAndWait();
}
catch (Throwable e) {
log.log(Level.SEVERE, "internalShowFxDialogAsync failed", e);
asyncResult.setAsyncResult(getResult(), e);
}
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, ")internalShowFxDialogAsync");
}
private void internalShowAsyncInFxThread(Object _owner, AsyncResult<T> _asyncResult) {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "internalShowAsyncInFxThread(" + _owner);
this.asyncResult = _asyncResult != null ? _asyncResult : ((ret, ex) -> {});
// Is the owner a COM object or another WindowHandle?
// A COM object has to implement IOleWindow.
Dispatch dispOwner = null;
long hwndOwner = 0;
Window fxOwner = null;
if (_owner instanceof WindowHandle) {
hwndOwner = ((WindowHandle) _owner).getWindowHandle();
} else if (_owner instanceof Dispatch) {
dispOwner = (Dispatch) _owner;
} else if (_owner instanceof Window) {
fxOwner = (Window) _owner;
} else if (_owner instanceof ModalDialogFX) {
@SuppressWarnings("rawtypes")
ModalDialogFX modalDialog = ((ModalDialogFX)_owner);
fxOwner = modalDialog.fxDlg;
if (fxOwner == null && modalDialog.joaDlg != null) {
hwndOwner = modalDialog.joaDlg.getHWND();
}
}
if (hwndOwner == 0 && dispOwner == null && fxOwner == null) {
Throwable ex = new IllegalStateException("Owner must not be null.");
log.log(Level.SEVERE, "internalShowAsync failed", ex);
asyncResult.setAsyncResult(null, ex);
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, ")internalShowAsync");
return;
}
if (fxOwner != null) {
internalShowFxDialogAsync(fxOwner, asyncResult);
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, ")internalShowAsync");
return;
}
try {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "createScene");
// Create the scene. Has to be implemented by subclass.
Scene scene = createScene();
// If width and height is not set, make the dialog
// as large as the scene.
maybeSetWidthAndHightFromSceneExtent(scene);
// Show native dialog
{
final Dispatch fdispOwner = dispOwner;
final long fhwndOwner = hwndOwner;
BackgTask.run(() -> {
// Create the native dialog object.
// This is the task of the JOA Util Add-in. Because
// the dialog has to be created in the UI thread
// inside Outlook.
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "create joaDlg...");
joaDlg = OfficeAddin.getJoaUtil().CreateBridgeDialog();
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "create joaDlg OK, " + joaDlg);
joaDlg.setWidth(toWin(width));
joaDlg.setHeight(toWin(height));
joaDlg.setX(toWin(x));
joaDlg.setY(toWin(y));
joaDlg.setTitle(title != null ? title : "");
joaDlg.setCenterOnOwner(centerOnOwner);
joaDlg.setResizable(resizable);
joaDlg.setMinHeight(toWin(minHeight));
joaDlg.setMaxHeight(toWin(maxHeight));
joaDlg.setMinWidth(toWin(minWidth));
joaDlg.setMaxWidth(toWin(maxWidth));
joaDlg.setMinimizeBox(minimizeBox);
joaDlg.setMaximizeBox(maximizeBox);
// Assign event handler to native dialog.
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "withEvents");
DialogEventHandler dialogHandler = new DialogEventHandler();
Dispatch.withEvents(joaDlg, dialogHandler);
if (fdispOwner != null) {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "showModal3");
joaDlg.ShowModal3(fdispOwner);
} else {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "showModal2");
joaDlg.ShowModal2(fhwndOwner);
}
// Wait until the native dialog fires the onShow
// event which is implemented by the DialogEventHandler.
// The hander stores the native dialog's window handle in hwndParent
// and sets the state as State.HasParentHwnd
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "wait until initialized");
synchronized (this) {
while (state == State.Initialized) {
try {
this.wait();
} catch (InterruptedException ex) {
break;
}
}
}
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "wait OK, state=" + state);
// Native dialog initialized?
if (state == State.HasParentHwnd) {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "createAndShowEmbeddedWindowAsync");
// Create a JavaFX frame inside the native dialog
embeddedFrame.createAndShowEmbeddedWindowAsync(hwndParent, scene, (succ, ex) -> {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "createAndShowEmbeddedWindowAsync result=" + succ + ", ex=" + ex);
if (ex == null) {
// Ensure the JavaFX frame is in the foreground.
long hwndChild = embeddedFrame.getWindowHandle();
JoaDll.nativeActivateSceneInDialog(hwndChild);
} else {
asyncResult.setAsyncResult(null, ex);
}
});
} else {
Throwable ex = new IllegalStateException("Excpected response from Office application.");
log.log(Level.SEVERE, "Failed to show dialog", ex);
asyncResult.setAsyncResult(null, ex);
dialogHandler.onClosed();
}
});
}
} catch (Throwable ex) {
log.log(Level.SEVERE, "Failed to show dialog", ex);
asyncResult.setAsyncResult(null, ex);
}
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, ")internalShowAsyncInFxThread");
}
@SuppressWarnings("deprecation")
private void maybeSetWidthAndHightFromSceneExtent(Scene scene) {
if (width == 0 || height == 0) {
scene.impl_preferredSize();
double sceneWidth = scene.getWidth();
double sceneHeight = scene.getHeight();
if (width == 0) {
width = sceneWidth + 20;
}
if (height == 0) {
height = sceneHeight + 40;
}
}
}
private class DialogEventHandler extends DispatchImpl implements _IJoaBridgeDialogEvents {
@Override
public void onShow(final Long hwndParent) throws ComException {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "onShow(" + hwndParent + ")");
synchronized (ModalDialogFX.this) {
ModalDialogFX.this.hwndParent = hwndParent;
ModalDialogFX.this.state = State.HasParentHwnd;
ModalDialogFX.this.notify();
}
}
@Override
public void onSystemMenuClose() throws ComException {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "onSystemMenuClose()");
ModalDialogFX.this.onSystemMenuClose();
}
@Override
public void onClosed() throws ComException {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "onClosed()");
if (ModalDialogFX.this.joaDlg != null) {
Dispatch.releaseEvents(ModalDialogFX.this.joaDlg, this);
}
if (ModalDialogFX.this.embeddedFrame != null) {
ModalDialogFX.this.embeddedFrame.close();
}
synchronized (ModalDialogFX.this) {
ModalDialogFX.this.state = State.IsClosed;
ModalDialogFX.this.notify();
}
}
}
}