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

com.openfin.desktop.DockingManager Maven / Gradle / Ivy

The newest version!
package com.openfin.desktop;

import com.openfin.desktop.win32.ExternalWindowObserver;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Manages snap and docking of Openfin windows and Java windows
 *
 * Created by wche on 3/5/2016.
 *
 */
public class DockingManager {
    private final static Logger logger = LoggerFactory.getLogger(DockingManager.class.getName());

    private DesktopConnection desktopConnection;
    private int snappingDistance = 20;  // snap if distance of 2 windows equals snappingDistance
    private Map memberMap = new HashMap();
    private java.util.List dockCandidates = new ArrayList();  // stores 2 windows that are close enough to dock.
    private BoundsChangingListener boundsChangingListener;
    private BoundsChangeListener boundsChangeListener;
    private String javaWindowParentUuid;

    /**
     * Constructor for Docking Manager
     *
     * In order for a Java window to be dockable, it needs to be assigned a parent HTML5 app as a parent app.  The html5 app needs to be active when
     * registerJavaWindow is called.
     *
     * @param desktopConnection connection to OpenFin Runtime
     * @param javaWindowParentUuid UUID of parent app each Java window will be assigned
     */
    public DockingManager(DesktopConnection desktopConnection, String javaWindowParentUuid) throws Exception{
        if (desktopConnection.isConnected()) {
            this.desktopConnection = desktopConnection;
            this.javaWindowParentUuid = javaWindowParentUuid;
            this.boundsChangeListener = new BoundsChangeListener();
            this.boundsChangingListener = new BoundsChangingListener();
            initRequestHandlers();
        } else {
            logger.error("DesktopConnection is not connected");
            throw new DesktopException("DesktopConnection is not connected");
        }
    }

    private void initRequestHandlers() throws Exception {
        this.desktopConnection.getInterApplicationBus().subscribe("*", "undock-window", (uuid, topic, data) -> {
            processUndockRequest((JSONObject)data);
        });

        this.desktopConnection.getInterApplicationBus().subscribe("*", "register-docking-window", (uuid, topic, data) -> {
            processRegisterRequest((JSONObject)data);
        });

        this.desktopConnection.getInterApplicationBus().subscribe("*", "unregister-docking-window", (uuid, topic, data) -> {
            processUnregisterRequest((JSONObject)data);
        });
    }


    /**
     * Register a window with DockingManager
     *
     * @param window
     */
    public synchronized void registerWindow(Window window) {
        String newKey = WindowMember.getMemberKey(window);
        if (this.memberMap.get(newKey) == null) {
            this.addWindow(window);
        } else {
            logger.error(String.format("%s already registered", newKey));
        }
    }

    /**
     * Register a java.awt.Window with DockingManager
     *
     * @param javaWindowName a name by which the window will be referenced in OpenFin API
     * @param window
     * @param ackListener
     */
    public void registerJavaWindow(String javaWindowName, java.awt.Window window, AckListener ackListener) {
        CountDownLatch latch = new CountDownLatch(1);
        try {
            ExternalWindowObserver externalWindowObserver = new ExternalWindowObserver(desktopConnection.getPort(), javaWindowParentUuid, javaWindowName, window,
                new AckListener() {
                    @Override
                    public void onSuccess(Ack ack) {
                        if (ack.isSuccessful()) {
                            Window ofWindow = Window.wrap(javaWindowParentUuid, javaWindowName, desktopConnection);
                            registerWindow(ofWindow);
                        }
                        latch.countDown();
                    }
                    @Override
                    public void onError(Ack ack) {
                        logger.error("Error registering java window ", ack.getReason());
                        latch.countDown();
                        if (ackListener != null) {
                            ackListener.onError(ack);
                        }
                    }
                });
            latch.await(10, TimeUnit.SECONDS);
            if (latch.getCount() == 0) {
                WindowMember member = memberMap.get(WindowMember.getMemberKey(javaWindowParentUuid, javaWindowName));
                if (member != null) {
                    member.setExternalWindowObserver(externalWindowObserver);
                    ackSuccess(ackListener);
                } else {
                    ackError(ackListener, "Error registering Java window");
                }
            } else {
                ackError(ackListener, "Error registering Java window");
            }
        } catch (Exception e) {
            logger.error("Error registering external window", e);
            latch.countDown();
            ackError(ackListener, e.getMessage());
        }
    }

    private void ackSuccess(AckListener ackListener) {
        if (ackListener != null) {
            ackListener.onSuccess(new Ack(new JSONObject(), this));
        }
    }

    private void ackError(AckListener ackListener, String reason) {
        if (ackListener != null) {
            JSONObject obj = new JSONObject();
            obj.put("reason", reason);
            ackListener.onError(new Ack(obj, this));
        }
    }

    /**
     * Add a window to managed list and set up event listeners for the window
     *
     * @param window
     */
    private void addWindow(Window window) {
        WindowMember member = new WindowMember(window, this.desktopConnection);
        this.memberMap.put(member.getKey(), member);
        this.addBoundsChangeListener("disabled-frame-bounds-changing", member, this.boundsChangingListener);
        this.addBoundsChangeListener("disabled-frame-bounds-changed", member, this.boundsChangeListener);
        // This class will manage frame move and resize
        window.disableFrame(new AckListener() {
            @Override
            public void onSuccess(Ack ack) {
                logger.debug(String.format("Frame disabled %s", member.getKey()));
            }
            @Override
            public void onError(Ack ack) {
                logger.error(String.format("onError disable Frame disabled %s %s", member.getKey(), ack.getReason()));
            }
        });
        // get initial bounds
        window.getBounds(bounds -> {
                member.setBounds(bounds);
            }, new AckListener() {
                @Override
                public void onSuccess(Ack ack) {
                }
                @Override
                public void onError(Ack ack) {
                    logger.error(String.format("onError getBounds %s", ack.getReason()));
                }
        });
    }

    public synchronized void unregisterWindow(String applicationUuid, String windowName) {
        WindowMember member = this.memberMap.get(WindowMember.getMemberKey(applicationUuid, windowName));
        if (member != null) {
            removeWindow(member);
        } else {
            logger.error(String.format("Window not registered %s", WindowMember.getMemberKey(applicationUuid, windowName)));
        }
    }
    private void removeWindow(WindowMember member) {
        logger.debug(String.format("Removing %s", member.getKey()));
        this.memberMap.remove(member.getKey());
        member.getWindow().removeEventListener("disabled-frame-bounds-changing", this.boundsChangingListener, null);
        member.getWindow().removeEventListener("disabled-frame-bounds-changed", this.boundsChangeListener, null);
        member.getWindow().enableFrame(null);
        if (member.getExternalWindowObserver() != null) {
            try {
                member.getExternalWindowObserver().dispose();
            } catch (Exception e) {
                logger.error(String.format("Error disposing ExternalWindowObserver %s", member.getKey()), e);
            }
        }
    }

    /**
     *  clean up everything
     */
    public void dispose() {
        logger.debug("calling dispose");
        HashSet members = new HashSet<>(this.memberMap.values());
        members.forEach(m -> removeWindow(m));
    }

    private void addBoundsChangeListener(final  String eventName, final WindowMember member, EventListener eventListener) {
        member.getWindow().addEventListener(eventName, eventListener, new AckListener() {
            @Override
            public void onSuccess(Ack ack) {
                if (!ack.isSuccessful()) {
                    logger.error(String.format("Failed to add event listener %s to window %s", eventName, member.getKey()));
                }
            }
            @Override
            public void onError(Ack ack) {
                logger.error(String.format("Failed to add event listener %s to window %s", eventName, member.getKey()));
            }
        });
    }

    private WindowBounds parseBounds(JSONObject data) {
        WindowBounds bounds = new WindowBounds(JsonUtils.getIntegerValue(data, "top", null),
                                        JsonUtils.getIntegerValue(data, "left", null),
                                        JsonUtils.getIntegerValue(data, "width", null),
                                        JsonUtils.getIntegerValue(data, "height", null)
                                );
        return bounds;
    }

    private WindowMember parseMember(JSONObject data) {
        WindowMember member = null;
        String appUuid = data.getString("uuid");
        String windowName = data.getString("name");
        if (appUuid != null && windowName != null) {
            member = this.memberMap.get(WindowMember.getMemberKey(appUuid, windowName));
        }
        return member;
    }

    /**
     * Process disabled-frame-bounds-changing event
     *
     * @param movingMember event origin
     * @param bounds new bounds from the event
     */
    private void onWindowMoving(WindowMember movingMember, WindowBounds bounds) {
        WindowBounds movingBounds = bounds;
        if (!movingMember.isDocked()) {
            this.dockCandidates.clear();
            Iterator memberIterator = this.memberMap.values().iterator();
            while (memberIterator.hasNext()) {
                WindowMember another = memberIterator.next();
                if (!another.equals(movingMember)) {
                    WindowBounds snapBounds = shouldSnap(movingMember, movingBounds, another);
                    if (snapBounds != null) {
                        this.dockCandidates.add(movingMember);  // actual docking happens in boundsChanged
                        this.dockCandidates.add(another);
                        logger.debug(String.format("Snapping %s", movingMember.getKey()));
                        movingBounds = snapBounds;
                        break;
                    }
                }
            }
        } else {
            logger.debug("Bounds changing already docked " + movingMember.getKey());
        }
        movingMember.updateBounds(movingBounds);
    }

    /**
     * Process disabled-frame-bounds-changed
     *
     * @param movingMember event orign
     * @param bounds new bounds in the event
     */
    private void onWindowMoved(WindowMember movingMember, WindowBounds bounds) {
        if (dockCandidates.size() == 2) {
            WindowMember movingCandidate = dockCandidates.get(0);
            WindowMember targetCandidate = dockCandidates.get(1);
            if (movingCandidate.equals(movingCandidate)) {
                targetCandidate.dock(movingCandidate);
            } else {
                logger.error(String.format("Docking candidate mismatch %s %s", movingMember.getKey(), movingCandidate.getKey()));
            }
            dockCandidates.clear();
        } else {
            movingMember.updateBounds(bounds);
        }
    }

    /**
     * Check if movingMember should be snapped to anchorMember
     *
     * @param movingMember moving
     * @param movingBounds potential snap target
     * @param anchorMember new bounds of movingMember while being moved
     * @return new bounds for movingMember to move for snapping
     */
    private WindowBounds shouldSnap(WindowMember movingMember, WindowBounds movingBounds, WindowMember anchorMember) {
        logger.debug(String.format("Checking shouldDock %s to %s", movingMember.getWindow().getName(), anchorMember.getWindow().getName()));
        WindowBounds newBounds = null;
        if (!movingMember.isDocked() || !anchorMember.isDocked()) {
            WindowBounds anchorBounds = anchorMember.getBounds();
            if (movingBounds != null && anchorBounds != null) {
                int bottom1 = movingBounds.getTop() + movingBounds.getHeight();
                int bottom2 = anchorBounds.getTop() + anchorBounds.getHeight();
                int right1 = movingBounds.getLeft() + movingBounds.getWidth();
                int right2 = anchorBounds.getLeft() + anchorBounds.getWidth();
                if (movingBounds.getLeft() < right2 && anchorBounds.getLeft() < right1) {
                    // vertical docking
                    if (Math.abs(movingBounds.getTop() - bottom2) < snappingDistance) {
                        newBounds = new WindowBounds(movingBounds.getTop(), movingBounds.getLeft(), movingBounds.getWidth(), movingBounds.getHeight());
                        newBounds.setTop(bottom2);
                        logger.debug(String.format("Detecting bottom-top docking %s to %s", movingMember.getWindow().getName(), anchorMember.getWindow().getName()));
                    }
                    else if (Math.abs(anchorBounds.getTop() - bottom1) < snappingDistance) {
                        newBounds = new WindowBounds(movingBounds.getTop(), movingBounds.getLeft(), movingBounds.getWidth(), movingBounds.getHeight());
                        newBounds.setTop(anchorBounds.getTop() - movingBounds.getHeight());
                        logger.debug(String.format("Detecting top-bottom docking %s to %s", movingMember.getWindow().getName(), anchorMember.getWindow().getName()));
                    } else {
                        logger.debug(String.format("shouldDock %s to %s too far top-bottom", movingMember.getWindow().getName(), anchorMember.getWindow().getName()));
                    }
                }
                else if (movingBounds.getTop() < bottom2 && anchorBounds.getTop() < bottom1) {
                    // horizontal docking
                    if (Math.abs(movingBounds.getLeft() - right2) < snappingDistance) {
                        newBounds = new WindowBounds(movingBounds.getTop(), movingBounds.getLeft(), movingBounds.getWidth(), movingBounds.getHeight());
                        newBounds.setLeft(right2);
                        logger.debug(String.format("Detecting right-left docking %s to %s", movingMember.getWindow().getName(), anchorMember.getWindow().getName()));
                    }
                    else if (Math.abs(right1 - anchorBounds.getLeft()) < snappingDistance) {
                        newBounds = new WindowBounds(movingBounds.getTop(), movingBounds.getLeft(), movingBounds.getWidth(), movingBounds.getHeight());
                        newBounds.setLeft(anchorBounds.getLeft() - movingBounds.getWidth());
                        logger.debug(String.format("Detecting left-right docking %s to %s", movingMember.getWindow().getName(), anchorMember.getWindow().getName()));
                    }
                } else {
                    logger.debug(String.format("shouldDock %s to %s not overlapping", movingMember.getWindow().getName(), anchorMember.getWindow().getName()));
                }
            }
        }
        return newBounds;
    }

    private void processUndockRequest(JSONObject request) {
        String appUuid = request.getString("applicationUuid");
        String windowName = request.getString("windowName");
        if (appUuid != null && windowName != null) {
            WindowMember member = this.memberMap.get(WindowMember.getMemberKey(appUuid, windowName));
            if (member != null) {
                if (member.isDocked()) {
                    member.undock();
                }
            } else {
                logger.error(String.format("Window not registered %s %s", appUuid, windowName));
            }
        } else {
            logger.error(String.format("Invalid request to duck window %s", request.toString()));
        }
    }

    private void processRegisterRequest(JSONObject request) {
        String appUuid = request.getString("applicationUuid");
        String windowName = request.getString("windowName");
        if (appUuid != null && windowName != null) {
            Window window = Window.wrap(appUuid, windowName, this.desktopConnection);
            registerWindow(window);
        } else {
            logger.error(String.format("Invalid request to duck window %s", request.toString()));
        }
    }

    private void processUnregisterRequest(JSONObject request) {
        String appUuid = request.getString("applicationUuid");
        String windowName = request.getString("windowName");
        if (appUuid != null && windowName != null) {
            this.unregisterWindow(appUuid, windowName);
        } else {
            logger.error(String.format("Invalid request to unregister window %s", request.toString()));
        }
    }

    /**
     * EventLister class for disabled-frame-bounds-changed event
     */
    class BoundsChangeListener implements EventListener {
        @Override
        public void eventReceived(ActionEvent actionEvent) {
            JSONObject data = actionEvent.getEventObject();
            WindowMember member = parseMember(data);
            if (member != null) {
                WindowBounds bounds = parseBounds(data);
                onWindowMoved(member, bounds);
            } else {
                logger.error(String.format("Window not registered %s", data.toString()));
            }
        }
    }

    /**
     * EventLister class for disabled-frame-bounds-changing event
     */
    class BoundsChangingListener implements EventListener {
        @Override
        public void eventReceived(ActionEvent actionEvent) {
            JSONObject data = actionEvent.getEventObject();
            WindowMember member = parseMember(data);
            if (member != null) {
                WindowBounds bounds = parseBounds(data);
                onWindowMoving(member, bounds);
            } else {
                logger.error(String.format("Window not registered %s", data.toString()));
            }
        }
    }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy