/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program 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 * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.tools.bubble; import java.awt.Component; import java.awt.Frame; import java.awt.GraphicsEnvironment; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.NavigableSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import javax.swing.SwingUtilities; import com.rapidminer.gui.RapidMinerGUI; /** * The BrowserBubble Choreographer * <ul> * <li>Displays windows under each other</li> * <li>Reuses free spaces after Windows are closed</li> * </ul> * * @author Jonas Wilms-Pfau * @since 7.5.0 * */ public class WindowChoreographer { /** * ArrayList that does not explode, if you try to set the next value * */ private class SetOrAddArrayList<E> extends ArrayList<E> { private static final long serialVersionUID = 1L; /** * <p> * Also allow to set the next free value. * </p> * * {@inheritDoc} */ @Override public E set(int index, E element) { E result = null; if (this.size() == index) { add(element); } else { result = super.set(index, element); } return result; } } private class CloseListener extends WindowAdapter { @Override public void windowClosed(WindowEvent e) { Window closedWindow = e.getWindow(); Integer freePosition = windowPosition.remove(closedWindow); closedWindow.removeWindowListener(closeListener); // Mark as free if (freePosition != null) { freeSpaces.add(freePosition); cleanUp(); } }; } /** Simple dialogs if transparency is not supported */ private static final boolean MODERN_UI = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice() .getDefaultConfiguration().isTranslucencyCapable(); /** The default margin to the top */ private static final int DEFAULT_TOP_MARGIN = 50; /** * In transparent mode the window cares about the border, since it has to paint the shadow and * the animation, in Window mode it's defined here */ private static final int DEFAULT_RIGHT_MARGIN = MODERN_UI ? 0 : 100; /** In dialog mode we have to add the margin */ private static final int DEFAULT_BOTTOM_MARGIN = MODERN_UI ? 0 : 50; /** Position of each window */ private Map<Window, Integer> windowPosition = new ConcurrentHashMap<>(); private NavigableSet<Integer> freeSpaces = new TreeSet<>(); /** MaxY Coordinate of each Window */ private List<Integer> windowYOffset = new SetOrAddArrayList<>(); /** Relative to parent */ private Component parent; /** Reserve bubbles, if the Screen is already full of bubbles */ private LinkedList<Window> bubbleStack = new LinkedList<>(); /** The close listener */ private final WindowListener closeListener = new CloseListener(); /** * Creates a new WindowChoreographer relative to the MainFrame, with a margin of * {@value #DEFAULT_TOP_MARGIN} to the top */ public WindowChoreographer() { this(RapidMinerGUI.getMainFrame().getContentPane(), DEFAULT_TOP_MARGIN); } /** * Creates a new WindowChoreographer * * @param parent * Relative to this Component * @param yOffset * The initial yOffset */ public WindowChoreographer(Component parent, int yOffset) { this.parent = parent; // Initial position windowYOffset.set(0, yOffset); // Follow the parent window, but only if not in dialog mode if (MODERN_UI) { // The content pane has no move event SwingUtilities.getRoot(parent).addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { recalculateWindowPositions(); cleanUp(); } @Override public void componentMoved(ComponentEvent e) { recalculateWindowPositions(); // Double click the taskbar only causes a componentMoved event cleanUp(); } }); } } /** * Adds a Window into the next free position * * @param w * The window * @return true if the Window could be displayed immediately */ public synchronized boolean addWindow(Window w) { if (w == null) { throw new IllegalArgumentException("w must not be null!"); } // Don't trigger in iconified mode if (RapidMinerGUI.getMainFrame().getExtendedState() == Frame.ICONIFIED) { bubbleStack.add(w); return false; } int pos = getNextPosition(w); int yOffset = windowYOffset.get(pos - 1); if (!fitsScreen(w, yOffset)) { // Lets store the bubble for later bubbleStack.add(w); return false; } // Allow a window only one time if (windowPosition.containsKey(w)) { return false; } // Remember the position of the window windowPosition.put(w, pos); // Great job Java there are two remove methods freeSpaces.remove(pos); // Remember size if it's a new window if (pos >= windowYOffset.size()) { this.windowYOffset.set(pos, yOffset + w.getHeight() + DEFAULT_BOTTOM_MARGIN); } w.addWindowListener(closeListener); w.setVisible(true); recalculateWindowPosition(w, pos); return true; } /** * Follow the parent Component position * * @param e */ private void recalculateWindowPositions() { windowPosition.forEach(this::recalculateWindowPosition); } /** * Recalculate the Window position * * @param window * @param position */ private void recalculateWindowPosition(Window window, int position) { Rectangle parentBounds = parent.getBounds(); parentBounds.setLocation(parent.getLocationOnScreen()); int rightX = (int) (parentBounds.getX() + parent.getWidth() - DEFAULT_RIGHT_MARGIN); // this was going crazy sometimes int topY = Math.max((int) parentBounds.getY(), 0); int yOffset = windowYOffset.get(position); // Recalculate the window positions window.setLocation(rightX - window.getWidth(), topY + yOffset - window.getHeight()); // Check if the Window fits into the parents bounds if (!parentBounds.contains(window.getBounds())) { // back into the bubbleStack window.setVisible(false); bubbleStack.addFirst(window); freeSpaces.add(windowPosition.remove(window)); window.removeWindowListener(closeListener); } } /** * Returns the next position that fits the window * * @param w * @return */ private int getNextPosition(Window w) { if (!freeSpaces.isEmpty()) { Integer pos = freeSpaces.first(); while (pos != null) { int start = windowYOffset.get(pos); int end = pos + 2 < windowYOffset.size() ? windowYOffset.get(pos + 1) : (int) parent.getHeight(); if (w.getHeight() <= end - start) { return pos; } pos = freeSpaces.higher(pos); } } return windowYOffset.size(); } /** * Check if there is enough space for the Window * * @param w * The window to insert * @param yOffset * The start offset of the Window * @return */ private boolean fitsScreen(Window w, int yOffset) { return yOffset + w.getHeight() <= parent.getHeight() && parent.getWidth() >= w.getWidth() + DEFAULT_RIGHT_MARGIN; } /** * Cleans up empty spaces at the end and show new bubbles for the user * */ private synchronized void cleanUp() { // Removes free spaces from the end if (!freeSpaces.isEmpty()) { Integer free = freeSpaces.last(); while (free != null) { if (free + 1 >= windowYOffset.size()) { // End of the list, remove by index NOT value int index = free.intValue(); windowYOffset.remove(index); freeSpaces.remove(free); free = freeSpaces.lower(free); } else { break; } } } // display waiting bubbles now that we have free space again boolean enoughSpace = true; while (!bubbleStack.isEmpty() && enoughSpace) { enoughSpace = addWindow(bubbleStack.pop()); } } }