/* * This file is part of WebLookAndFeel library. * * WebLookAndFeel library is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * WebLookAndFeel library 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with WebLookAndFeel library. If not, see . */ package com.alee.extended.window; import com.alee.laf.WebLookAndFeel; import com.alee.laf.panel.WebPanel; import com.alee.laf.rootpane.WebDialog; import com.alee.managers.style.StyleManager; import com.alee.managers.style.skin.web.PopupStyle; import com.alee.managers.style.skin.web.WebPopOverPainter; import com.alee.managers.style.skin.web.WebPopupPainter; import com.alee.utils.CollectionUtils; import com.alee.utils.SwingUtils; import com.alee.utils.laf.Styleable; import com.alee.utils.swing.DataProvider; import com.alee.utils.swing.WindowFollowAdapter; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.List; /** * Custom stylish pop-over dialog with a special corner that follows invoker component. * It may also act as a simple dialog with custom styling if configured so. * * @author Mikle Garin * @see WebDialog * @see WebPopupPainter * @see PopOverSourcePoint * @see PopOverDirection * @see PopOverAlignment */ public class WebPopOver extends WebDialog implements Styleable { /** * todo 1. Fix directional WebPopOver location when dragged to/from secondary screen * todo 2. Add "opened" listener event */ /** * Whether WebPopOver should be movable or not. */ protected boolean movable = WebPopOverStyle.movable; /** * WebPopOver display source point. */ protected PopOverSourcePoint popOverSourcePoint = WebPopOverStyle.popOverSourcePoint; /** * WebPopOver components container. */ protected WebPanel container; /** * Whether WebPopOver is attached to invoker component or not. */ protected boolean attached = false; /** * Preferred direction in which WebPopOver should be displayed. */ protected PopOverDirection preferredDirection = null; /** * Current display direction. */ protected PopOverDirection currentDirection = null; /** * Preferred WebPopOver alignment relative to display source point. */ protected PopOverAlignment preferredAlignment = null; /** * WebPopOver state listeners. */ protected List popOverListeners = new ArrayList ( 1 ); /** * Constructs new WebPopOver dialog. */ public WebPopOver () { super (); } /** * Constructs new WebPopOver dialog. * * @param owner dialog owner frame */ public WebPopOver ( final Frame owner ) { super ( owner ); } /** * Constructs new WebPopOver dialog. * * @param owner dialog owner frame * @param title dialog title (might not be displayed depending on styling) */ public WebPopOver ( final Frame owner, final String title ) { super ( owner, title ); } /** * Constructs new WebPopOver dialog. * * @param owner dialog owner dialog */ public WebPopOver ( final Dialog owner ) { super ( owner ); } /** * Constructs new WebPopOver dialog. * * @param owner dialog owner dialog * @param title dialog title (might not be displayed depending on styling) */ public WebPopOver ( final Dialog owner, final String title ) { super ( owner, title ); } /** * Constructs new WebPopOver dialog. * * @param owner dialog owner component */ public WebPopOver ( final Component owner ) { super ( owner ); } /** * Constructs new WebPopOver dialog. * * @param owner dialog owner component * @param title dialog title (might not be displayed depending on styling) */ public WebPopOver ( final Component owner, final String title ) { super ( owner, title ); } /** * Constructs new WebPopOver dialog. * * @param owner dialog owner window */ public WebPopOver ( final Window owner ) { super ( owner ); } /** * Constructs new WebPopOver dialog. * * @param owner dialog owner window * @param title dialog title (might not be displayed depending on styling) */ public WebPopOver ( final Window owner, final String title ) { super ( owner, title ); } /** * WebPopOver settings initialization. */ @Override protected void initialize () { super.initialize (); getRootPane ().setWindowDecorationStyle ( JRootPane.NONE ); setUndecorated ( true ); setWindowOpaque ( false ); // todo Custom shade opacity when not focused // painter = new WebPopupPainter () // { // @Override // public float getShadeTransparency () // { // final float actualShadeOpacity = super.getShadeTransparency (); // return WebPopOver.this.isFocused () ? actualShadeOpacity : actualShadeOpacity * 0.7f; // } // }; // painter.setBorderColor ( WebPopOverStyle.borderColor ); // painter.setRound ( WebPopOverStyle.round ); // painter.setShadeWidth ( WebPopOverStyle.shadeWidth ); // painter.setShadeTransparency ( WebPopOverStyle.shadeTransparency ); // painter.setCornerWidth ( WebPopOverStyle.cornerWidth ); // painter.setTransparency ( WebPopOverStyle.transparency ); // painter.setPopupStyle ( PopupStyle.simple ); container = new WebPanel ( /*painter*/ ); container.setStyleId ( "pop-over" ); setContentPane ( container ); final ComponentMoveAdapter moveAdapter = new ComponentMoveAdapter () { @Override protected Rectangle getDragStartBounds ( final MouseEvent e ) { if ( movable ) { final int sw = getShadeWidth (); return new Rectangle ( sw, sw, container.getWidth () - sw * 2, container.getHeight () - sw * 2 ); } else { return null; } } @Override public void mouseDragged ( final MouseEvent e ) { // De-attach dialog if ( dragging && attached ) { attached = false; preferredDirection = null; setPopupStyle ( PopupStyle.simple ); firePopOverDetached (); } super.mouseDragged ( e ); } }; container.addMouseListener ( moveAdapter ); container.addMouseMotionListener ( moveAdapter ); addWindowFocusListener ( new WindowFocusListener () { @Override public void windowGainedFocus ( final WindowEvent e ) { setPopOverFocused ( true ); } @Override public void windowLostFocus ( final WindowEvent e ) { setPopOverFocused ( false ); } } ); // Removing all listeners on window close event final PopOverCloseListener closeListener = new PopOverCloseListener () { @Override public void popOverClosed () { firePopOverClosed (); } }; addComponentListener ( closeListener ); addWindowListener ( closeListener ); } /** * {@inheritDoc} */ @Override public String getStyleId () { return container.getStyleId (); } /** * {@inheritDoc} */ @Override public void setStyleId ( final String id ) { container.setStyleId ( id ); } /** * Sets WebPopOver content background color. * * @param color new content background color */ public void setContentBackground ( final Color color ) { container.setBackground ( color ); } /** * Returns WebPopOver content background color. * * @return WebPopOver content background color */ public Color getContentBackground () { return container.getBackground (); } /** * Returns WebPopOver container margin. * * @return WebPopOver container margin */ public Insets getMargin () { return container.getMargin (); } /** * Sets WebPopOver container margin. * * @param margin margin insets * @return WebPopOver */ public WebPopOver setMargin ( final Insets margin ) { container.setMargin ( margin ); return this; } /** * Sets WebPopOver container margin. * * @param top top margin * @param left left margin * @param bottom bottom margin * @param right right margin * @return WebPopOver */ public WebPopOver setMargin ( final int top, final int left, final int bottom, final int right ) { container.setMargin ( top, left, bottom, right ); return this; } /** * Sets WebPopOver container margin. * * @param margin sides margin * @return WebPopOver */ public WebPopOver setMargin ( final int margin ) { container.setMargin ( margin ); return this; } /** * Returns base WebPopOver painter. * * @return base WebPopOver painter */ public WebPopOverPainter getPainter () { return ( WebPopOverPainter ) container.getPainter (); } /** * Sets base WebPopOver painter. * * @param painter base WebPopOver painter */ public void setPainter ( final WebPopOverPainter painter ) { container.setPainter ( painter ); } /** * Returns whether this WebPopOver is movable or not. * * @return true if this WebPopOver is movable, false otherwise */ public boolean isMovable () { return movable; } /** * Sets whether this WebPopOver should be movable or not. * * @param movable whether this WebPopOver should be movable or not */ public void setMovable ( final boolean movable ) { this.movable = movable; } /** * Returns popup display source point. * * @return popup display source point */ public PopOverSourcePoint getPopOverSourcePoint () { return popOverSourcePoint; } /** * Sets popup display source point. * * @param popOverSourcePoint popup display source point */ public void setPopOverSourcePoint ( final PopOverSourcePoint popOverSourcePoint ) { this.popOverSourcePoint = popOverSourcePoint; } /** * Returns popup menu border color. * * @return popup menu border color */ public Color getBorderColor () { return getPainter ().getBorderColor (); } /** * Sets popup menu border color. * * @param color new popup menu border color */ public void setBorderColor ( final Color color ) { StyleManager.setCustomPainterProperty ( container, "borderColor", color ); } /** * Returns popup menu border corners rounding. * * @return popup menu border corners rounding */ @Override public int getRound () { return getPainter ().getRound (); } /** * Sets popup menu border corners rounding. * * @param round new popup menu border corners rounding */ @Override public void setRound ( final int round ) { StyleManager.setCustomPainterProperty ( container, "round", round ); } /** * Returns popup menu shade width. * * @return popup menu shade width */ @Override public int getShadeWidth () { return getPainter ().getShadeWidth (); } /** * Sets popup menu shade width. * * @param width new popup menu shade width */ @Override public void setShadeWidth ( final int width ) { StyleManager.setCustomPainterProperty ( container, "shadeWidth", width ); } /** * Returns popup menu shade transparency. * * @return popup menu shade transparency */ public float getShadeTransparency () { return getPainter ().getShadeTransparency (); } /** * Sets popup menu shade transparency. * * @param transparency new popup menu shade transparency */ public void setShadeTransparency ( final float transparency ) { StyleManager.setCustomPainterProperty ( container, "shadeTransparency", transparency ); } /** * Returns popup menu dropdown style corner width. * * @return popup menu dropdown style corner width */ public int getCornerWidth () { return getPainter ().getCornerWidth (); } /** * Sets popup menu dropdown style corner width. * * @param width popup menu dropdown style corner width */ public void setCornerWidth ( final int width ) { StyleManager.setCustomPainterProperty ( container, "cornerWidth", width ); } /** * Returns popup menu background transparency. * * @return popup menu background transparency */ public float getTransparency () { return getPainter ().getTransparency (); } /** * Sets popup menu background transparency. * * @param transparency popup menu background transparency */ public void setTransparency ( final float transparency ) { StyleManager.setCustomPainterProperty ( container, "transparency", transparency ); } /** * Returns popup style. * This method is made protected because requesting style is necessary only within the WebPopOver. * * @return popup style */ protected PopupStyle getPopupStyle () { return getPainter ().getPopupStyle (); } /** * Sets popup style. * This method is made protected because modifying style is necessary only within the WebPopOver. * * @param style new popup style */ protected void setPopupStyle ( final PopupStyle style ) { StyleManager.setCustomPainterProperty ( container, "popupStyle", style ); } /** * Returns dropdown style corner side. * * @return dropdown style corner side */ protected int getCornerSide () { return getPainter ().getCornerSide (); } /** * Sets dropdown style corner side. * * @param cornerSide dropdown style corner side */ protected void setCornerSide ( final int cornerSide ) { StyleManager.setCustomPainterProperty ( container, "cornerSide", cornerSide ); } /** * Returns relative dropdown corner position. * * @return relative dropdown corner position */ protected int getRelativeCorner () { return getPainter ().getRelativeCorner (); } /** * Sets relative dropdown corner position. * * @param relativeCorner relative dropdown corner position */ protected void setRelativeCorner ( final int relativeCorner ) { StyleManager.setCustomPainterProperty ( container, "relativeCorner", relativeCorner ); } /** * Returns dropdown corner alignment. * * @return dropdown corner alignment */ protected int getCornerAlignment () { return getPainter ().getCornerAlignment (); } /** * Sets dropdown corner alignment. * * @param cornerAlignment dropdown corner alignment */ protected void setCornerAlignment ( final int cornerAlignment ) { StyleManager.setCustomPainterProperty ( container, "cornerAlignment", cornerAlignment ); } /** * Returns whether this WebPopOver is focus owner or not. * * @return true if this WebPopOver is focus owner, false otherwise */ public boolean isPopOverFocused () { return getPainter ().isPopOverFocused (); } /** * Sets whether this WebPopOver is focus owner or not. * * @param focused whether this WebPopOver is focus owner or not */ public void setPopOverFocused ( final boolean focused ) { StyleManager.setCustomPainterProperty ( container, "popOverFocused", focused ); } /** * Displays unattached WebPopOver at the specified screen location. * * @param location WebPopOver location on screen * @return displayed WebPopOver */ public WebPopOver show ( final PopOverLocation location ) { // Updating WebPopOver variables attached = false; preferredDirection = null; setPopupStyle ( PopupStyle.simple ); // Updating dialog location on screen and size final Dimension ss = Toolkit.getDefaultToolkit ().getScreenSize (); pack (); switch ( location ) { case center: { setLocation ( ss.width / 2 - getWidth () / 2, ss.height / 2 - getHeight () / 2 ); break; } case topLeft: { setLocation ( 0, 0 ); break; } case topRight: { setLocation ( ss.width - getWidth (), 0 ); break; } case bottomLeft: { setLocation ( 0, ss.height - getHeight () ); break; } case bottomRight: { setLocation ( ss.width - getWidth (), ss.height - getHeight () ); break; } case topCenter: { setLocation ( ss.width / 2 - getWidth () / 2, 0 ); break; } case bottomCenter: { setLocation ( ss.width / 2 - getWidth () / 2, ss.height - getHeight () ); break; } case leftCenter: { setLocation ( 0, ss.height / 2 - getHeight () / 2 ); break; } case rightCenter: { setLocation ( ss.width - getWidth (), ss.height / 2 - getHeight () / 2 ); break; } } // Displaying dialog setVisible ( true ); return this; } /** * Displays unattached WebPopOver at the specified location. * * @param location WebPopOver location * @return displayed WebPopOver */ public WebPopOver show ( final Point location ) { return show ( location.x, location.y ); } /** * Displays unattached WebPopOver at the specified location. * * @param x WebPopOver X location * @param y WebPopOver Y location * @return displayed WebPopOver */ public WebPopOver show ( final int x, final int y ) { // Updating WebPopOver variables attached = false; preferredDirection = null; setPopupStyle ( PopupStyle.simple ); // Updating dialog location on screen and size pack (); setLocation ( x - getShadeWidth (), y - getShadeWidth () ); // Displaying dialog setVisible ( true ); return this; } /** * Displays WebPopOver attached to the invoker component and faced to preferred direction. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker ) { return show ( invoker, PopOverDirection.down ); } /** * Displays WebPopOver attached to the invoker component and faced to specified direction. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @param direction preferred display direction * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker, final PopOverDirection direction ) { return show ( invoker, direction, PopOverAlignment.centered ); } /** * Displays WebPopOver attached to the invoker component and faced to specified direction. * It will also be aligned using the specified alignment type when possible. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @param direction preferred display direction * @param alignment preferred display alignment * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker, final PopOverDirection direction, final PopOverAlignment alignment ) { return show ( invoker, null, direction, alignment ); } /** * Displays WebPopOver attached to the invoker component coordinates and faced to specified direction. * It will also be aligned using the specified alignment type when possible. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @param x source area X coordinate in invoker's component coordinate system * @param y source area Y coordinate in invoker's component coordinate system * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker, final int x, final int y ) { return show ( invoker, x, y, PopOverDirection.down ); } /** * Displays WebPopOver attached to the invoker component coordinates and faced to specified direction. * It will also be aligned using the specified alignment type when possible. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @param x source area X coordinate in invoker's component coordinate system * @param y source area Y coordinate in invoker's component coordinate system * @param direction preferred display direction * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker, final int x, final int y, final PopOverDirection direction ) { return show ( invoker, x, y, direction, PopOverAlignment.centered ); } /** * Displays WebPopOver attached to the invoker component coordinates and faced to specified direction. * It will also be aligned using the specified alignment type when possible. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @param x source area X coordinate in invoker's component coordinate system * @param y source area Y coordinate in invoker's component coordinate system * @param direction preferred display direction * @param alignment preferred display alignment * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker, final int x, final int y, final PopOverDirection direction, final PopOverAlignment alignment ) { return show ( invoker, x, y, 0, 0, direction, alignment ); } /** * Displays WebPopOver attached to the invoker component area and faced to specified direction. * It will also be aligned using the specified alignment type when possible. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @param x source area X coordinate in invoker's component coordinate system * @param y source area Y coordinate in invoker's component coordinate system * @param w source area width * @param h source area height * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker, final int x, final int y, final int w, final int h ) { return show ( invoker, x, y, w, h, PopOverDirection.down ); } /** * Displays WebPopOver attached to the invoker component area and faced to specified direction. * It will also be aligned using the specified alignment type when possible. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @param x source area X coordinate in invoker's component coordinate system * @param y source area Y coordinate in invoker's component coordinate system * @param w source area width * @param h source area height * @param direction preferred display direction * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker, final int x, final int y, final int w, final int h, final PopOverDirection direction ) { return show ( invoker, x, y, w, h, direction, PopOverAlignment.centered ); } /** * Displays WebPopOver attached to the invoker component area and faced to specified direction. * It will also be aligned using the specified alignment type when possible. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @param x source area X coordinate in invoker's component coordinate system * @param y source area Y coordinate in invoker's component coordinate system * @param w source area width * @param h source area height * @param direction preferred display direction * @param alignment preferred display alignment * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker, final int x, final int y, final int w, final int h, final PopOverDirection direction, final PopOverAlignment alignment ) { final Rectangle bounds = new Rectangle ( x, y, w, h ); return show ( invoker, new DataProvider () { @Override public Rectangle provide () { return bounds; } }, direction, alignment ); } /** * Displays WebPopOver attached to the invoker component area and faced to specified direction. * It will also be aligned using the specified alignment type when possible. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @param boundsProvider source area provider * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker, final DataProvider boundsProvider ) { return show ( invoker, boundsProvider, PopOverDirection.down ); } /** * Displays WebPopOver attached to the invoker component area and faced to specified direction. * It will also be aligned using the specified alignment type when possible. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @param boundsProvider source area provider * @param direction preferred display direction * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker, final DataProvider boundsProvider, final PopOverDirection direction ) { return show ( invoker, boundsProvider, direction, PopOverAlignment.centered ); } /** * Displays WebPopOver attached to the invoker component area and faced to specified direction. * It will also be aligned using the specified alignment type when possible. * WebPopOver opened in this way will always auto-follow invoker's ancestor window. * * @param invoker invoker component * @param boundsProvider source area provider * @param direction preferred display direction * @param alignment preferred display alignment * @return displayed WebPopOver */ public WebPopOver show ( final Component invoker, final DataProvider boundsProvider, final PopOverDirection direction, final PopOverAlignment alignment ) { // Translating coordinates into screen coordinates system final DataProvider actualBoundsProvider = boundsProvider == null ? null : new DataProvider () { private Rectangle lastBounds = new Rectangle (); @Override public Rectangle provide () { // Invoker might be hidden while WebPopOver is still visible // This is why we should simply stop updating its position when that happens // It is not the best workaround but at least it will keep us safe from exceptions if ( invoker.isShowing () ) { final Rectangle bounds = boundsProvider.provide (); final Point los = invoker.getLocationOnScreen (); lastBounds = new Rectangle ( los.x + bounds.x, los.y + bounds.y, bounds.width, bounds.height ); } return lastBounds; } }; // Updating WebPopOver variables attached = true; preferredDirection = direction; preferredAlignment = alignment; // Updating dialog location on screen and size pack (); updatePopOverLocation ( invoker, actualBoundsProvider ); installPopOverLocationUpdater ( invoker, actualBoundsProvider ); // Displaying dialog setVisible ( true ); return this; } /** * Updates WebPopOver location on screen. * * @param invoker invoker component */ protected void updatePopOverLocation ( final Component invoker, final DataProvider invokerBoundsProvider ) { if ( invokerBoundsProvider != null ) { updatePopOverLocation ( invokerBoundsProvider.provide () ); } else { updatePopOverLocation ( invoker ); } } /** * Updates WebPopOver location on screen. * * @param invoker invoker component */ protected void updatePopOverLocation ( final Component invoker ) { if ( invoker instanceof Window ) { // Applying proper painter style setPopupStyle ( PopupStyle.simple ); // Determining final WebPopOver position final Rectangle ib = invoker.getBounds (); final Dimension size = getSize (); // Updating current direction currentDirection = null; // Updating WebPopOver location setLocation ( ib.x + ib.width / 2 - size.width / 2, ib.y + ib.height / 2 - size.height / 2 ); } else if ( invoker.isShowing () ) { // Updating WebPopOver location in a smarter way updatePopOverLocation ( SwingUtils.getBoundsOnScreen ( invoker ) ); } } /** * Updates WebPopOver location on screen. * * @param invokerBounds invoker component bounds on screen */ protected void updatePopOverLocation ( final Rectangle invokerBounds ) { // Applying proper painter style setPopupStyle ( PopupStyle.dropdown ); // WebPopOver preferred size without shade final Dimension size = getSize (); final int sw = getShadeWidth (); final int round = getRound (); final int cw = getCornerWidth (); final Dimension ps = new Dimension ( size.width - sw * 2, size.height - sw * 2 ); final Rectangle screenBounds = getGraphicsConfiguration ().getBounds (); final boolean ltr = getComponentOrientation ().isLeftToRight (); // Determining actual direction final PopOverDirection actualDirection = getActualDirection ( invokerBounds, ltr, cw, ps, screenBounds ); setCornerSide ( actualDirection.getCornerSide ( ltr ) ); currentDirection = actualDirection; // Determining position according to alignment final Point actualLocation = getActualLocation ( invokerBounds, ltr, round, cw, ps, screenBounds, actualDirection ); actualLocation.x -= sw; actualLocation.y -= sw; // Updating corner position setCornerAlignment ( -1 ); setRelativeCorner ( getRelativeCorner ( invokerBounds, actualDirection, actualLocation ) ); // Updating WebPopOver location setLocation ( actualLocation ); } /** * Installs listeners to update WebPopOver location. * * @param invoker invoker component */ protected void installPopOverLocationUpdater ( final Component invoker, final DataProvider invokerBoundsProvider ) { // Invoker component window final Window invokerWindow = SwingUtils.getWindowAncestor ( invoker ); // Invoker window follow adapter final WindowFollowAdapter windowFollowAdapter = new WindowFollowAdapter ( WebPopOver.this, invokerWindow ) { @Override public boolean isEnabled () { return !attached; } }; invokerWindow.addComponentListener ( windowFollowAdapter ); // Invoker window adapter final ComponentAdapter invokerWindowAdapter = new ComponentAdapter () { @Override public void componentMoved ( final ComponentEvent e ) { if ( attached ) { updatePopOverLocation ( invoker, invokerBoundsProvider ); windowFollowAdapter.updateLastLocation (); } } }; invokerWindow.addComponentListener ( invokerWindowAdapter ); // Invoker component adapter final ComponentAdapter invokerAdapter = new ComponentAdapter () { @Override public void componentMoved ( final ComponentEvent e ) { if ( attached ) { updatePopOverLocation ( invoker, invokerBoundsProvider ); windowFollowAdapter.updateLastLocation (); } } @Override public void componentResized ( final ComponentEvent e ) { if ( attached ) { updatePopOverLocation ( invoker, invokerBoundsProvider ); windowFollowAdapter.updateLastLocation (); } } }; invoker.addComponentListener ( invokerAdapter ); // WebPopOver orientation change listener final PropertyChangeListener orientationListener = new PropertyChangeListener () { @Override public void propertyChange ( final PropertyChangeEvent evt ) { updatePopOverLocation ( invoker, invokerBoundsProvider ); windowFollowAdapter.updateLastLocation (); } }; addPropertyChangeListener ( WebLookAndFeel.ORIENTATION_PROPERTY, orientationListener ); // Removing all listeners on window close event addPopOverListener ( new PopOverAdapter () { @Override public void popOverClosed () { removePopOverListener ( this ); invokerWindow.removeComponentListener ( invokerWindowAdapter ); invokerWindow.removeComponentListener ( windowFollowAdapter ); invoker.removeComponentListener ( invokerAdapter ); removePropertyChangeListener ( WebLookAndFeel.ORIENTATION_PROPERTY, orientationListener ); } } ); } /** * Returns relative corner position. * * @param ib invoker component bounds on screen * @param actualDirection actual WebPopOver direction * @param actualLocation actual WebPopOver location * @return relative corner position */ protected int getRelativeCorner ( final Rectangle ib, final PopOverDirection actualDirection, final Point actualLocation ) { switch ( actualDirection ) { case up: case down: return ib.x + ib.width / 2 - actualLocation.x; case left: case right: return ib.y + ib.height / 2 - actualLocation.y; } return -1; } /** * Returns actual WebPopOver location. * Shade width is not yet taken into account within this location. * * @param ib invoker component bounds on screen * @param ltr whether LTR orientation is active or not * @param round corners round * @param cw corner width * @param ps WebPopOver size without shade widths * @param screenBounds screen bounds * @param actualDirection actual WebPopOver direction * @return actual WebPopOver location */ protected Point getActualLocation ( final Rectangle ib, final boolean ltr, final int round, final int cw, final Dimension ps, final Rectangle screenBounds, final PopOverDirection actualDirection ) { final Point sp = getActualSourcePoint ( ib, ltr, actualDirection ); if ( actualDirection == PopOverDirection.up ) { if ( preferredAlignment == PopOverAlignment.centered ) { final Point location = new Point ( sp.x - ps.width / 2, sp.y - cw - ps.height ); return checkRightCollision ( checkLeftCollision ( location, screenBounds ), ps, screenBounds ); } else if ( preferredAlignment == ( ltr ? PopOverAlignment.leading : PopOverAlignment.trailing ) ) { return checkLeftCollision ( new Point ( sp.x + cw * 2 + round - ps.width, sp.y - cw - ps.height ), screenBounds ); } else if ( preferredAlignment == ( ltr ? PopOverAlignment.trailing : PopOverAlignment.leading ) ) { return checkRightCollision ( new Point ( sp.x - cw * 2 - round, sp.y - cw - ps.height ), ps, screenBounds ); } } else if ( actualDirection == PopOverDirection.down ) { if ( preferredAlignment == PopOverAlignment.centered ) { final Point location = new Point ( sp.x - ps.width / 2, sp.y + cw ); return checkRightCollision ( checkLeftCollision ( location, screenBounds ), ps, screenBounds ); } else if ( preferredAlignment == ( ltr ? PopOverAlignment.leading : PopOverAlignment.trailing ) ) { return checkLeftCollision ( new Point ( sp.x + cw * 2 + round - ps.width, sp.y + cw ), screenBounds ); } else if ( preferredAlignment == ( ltr ? PopOverAlignment.trailing : PopOverAlignment.leading ) ) { return checkRightCollision ( new Point ( sp.x - cw * 2 - round, sp.y + cw ), ps, screenBounds ); } } else if ( actualDirection == ( ltr ? PopOverDirection.left : PopOverDirection.right ) ) { if ( preferredAlignment == PopOverAlignment.centered ) { final Point location = new Point ( sp.x - cw - ps.width, sp.y - ps.height / 2 ); return checkBottomCollision ( checkTopCollision ( location, screenBounds ), ps, screenBounds ); } else if ( preferredAlignment == PopOverAlignment.leading ) { return checkTopCollision ( new Point ( sp.x - cw - ps.width, sp.y + cw * 2 + round - ps.height ), screenBounds ); } else if ( preferredAlignment == PopOverAlignment.trailing ) { return checkBottomCollision ( new Point ( sp.x - cw - ps.width, sp.y - cw * 2 - round ), ps, screenBounds ); } } else if ( actualDirection == ( ltr ? PopOverDirection.right : PopOverDirection.left ) ) { if ( preferredAlignment == PopOverAlignment.centered ) { final Point location = new Point ( sp.x + cw, sp.y - ps.height / 2 ); return checkBottomCollision ( checkTopCollision ( location, screenBounds ), ps, screenBounds ); } else if ( preferredAlignment == PopOverAlignment.leading ) { return checkTopCollision ( new Point ( sp.x + cw, sp.y + cw * 2 + round - ps.height ), screenBounds ); } else if ( preferredAlignment == PopOverAlignment.trailing ) { return checkBottomCollision ( new Point ( sp.x + cw, sp.y - cw * 2 - round ), ps, screenBounds ); } } return null; } /** * Checks whether WebPopOver will collide with top screen border and modifies location accordingly. * * @param location approximate WebPopOver location * @param screenBounds screen bounds * @return either modified or unmodified WebPopOver location */ protected Point checkTopCollision ( final Point location, final Rectangle screenBounds ) { if ( location.y < screenBounds.y ) { location.y = screenBounds.y; } return location; } /** * Checks whether WebPopOver will collide with bottom screen border and modifies location accordingly. * * @param location approximate WebPopOver location * @param ps WebPopOver size without shade widths * @param screenBounds screen bounds * @return either modified or unmodified WebPopOver location */ protected Point checkBottomCollision ( final Point location, final Dimension ps, final Rectangle screenBounds ) { if ( location.y + ps.height > screenBounds.y + screenBounds.height ) { location.y = screenBounds.y + screenBounds.height - ps.height; } return location; } /** * Checks whether WebPopOver will collide with left screen border and modifies location accordingly. * * @param location approximate WebPopOver location * @param screenBounds screen bounds * @return either modified or unmodified WebPopOver location */ protected Point checkLeftCollision ( final Point location, final Rectangle screenBounds ) { if ( location.x < screenBounds.x ) { location.x = screenBounds.x; } return location; } /** * Checks whether WebPopOver will collide with right screen border and modifies location accordingly. * * @param location approximate WebPopOver location * @param ps WebPopOver size without shade widths * @param screenBounds screen bounds * @return either modified or unmodified WebPopOver location */ protected Point checkRightCollision ( final Point location, final Dimension ps, final Rectangle screenBounds ) { if ( location.x + ps.width > screenBounds.x + screenBounds.width ) { location.x = screenBounds.x + screenBounds.width - ps.width; } return location; } /** * Returns actual direction depending on preferred WebPopOver direction, its sizes and source point. * * @param ib invoker component bounds on screen * @param ltr whether LTR orientation is active or not * @param cw corner with * @param ps WebPopOver size without shade widths * @param screenBounds screen bounds * @return actual WebPopOver direction */ protected PopOverDirection getActualDirection ( final Rectangle ib, final boolean ltr, final int cw, final Dimension ps, final Rectangle screenBounds ) { for ( final PopOverDirection checkedDirection : preferredDirection.getPriority () ) { final Point sp = getActualSourcePoint ( ib, ltr, checkedDirection ); if ( checkedDirection == PopOverDirection.up ) { if ( sp.y - cw - ps.height > screenBounds.y ) { return checkedDirection; } } else if ( checkedDirection == PopOverDirection.down ) { if ( sp.y + cw + ps.height < screenBounds.y + screenBounds.height ) { return checkedDirection; } } else if ( checkedDirection == ( ltr ? PopOverDirection.left : PopOverDirection.right ) ) { if ( sp.x - cw - ps.width > screenBounds.x ) { return checkedDirection; } } else if ( checkedDirection == ( ltr ? PopOverDirection.right : PopOverDirection.left ) ) { if ( sp.x + cw + ps.width < screenBounds.x + screenBounds.width ) { return checkedDirection; } } } return preferredDirection; } /** * Returns actual source point depending on WebPopOver direction and invoker component location on screen. * * @param ib invoker component bounds on screen * @param ltr whether LTR orientation is active or not * @param direction WebPopOver direction @return actual source point */ protected Point getActualSourcePoint ( final Rectangle ib, final boolean ltr, final PopOverDirection direction ) { if ( popOverSourcePoint == PopOverSourcePoint.componentCenter ) { return new Point ( ib.x + ib.width / 2, ib.y + ib.height / 2 ); } else { if ( direction == PopOverDirection.up ) { return new Point ( ib.x + ib.width / 2, ib.y ); } else if ( direction == PopOverDirection.down ) { return new Point ( ib.x + ib.width / 2, ib.y + ib.height ); } else if ( direction == ( ltr ? PopOverDirection.left : PopOverDirection.right ) ) { return new Point ( ib.x, ib.y + ib.height / 2 ); } else if ( direction == ( ltr ? PopOverDirection.right : PopOverDirection.left ) ) { return new Point ( ib.x + ib.width, ib.y + ib.height / 2 ); } } return null; } /** * Returns preferred direction in which WebPopOver should be displayed. * * @return preferred direction in which WebPopOver should be displayed */ public PopOverDirection getPreferredDirection () { return preferredDirection; } /** * Sets preferred direction in which WebPopOver should be displayed. * * @param direction preferred direction in which WebPopOver should be displayed */ public void setPreferredDirection ( final PopOverDirection direction ) { this.preferredDirection = direction; } /** * Returns current display direction. * * @return current display direction */ public PopOverDirection getCurrentDirection () { return currentDirection; } /** * Sets current display direction. * * @param direction current display direction */ public void setCurrentDirection ( final PopOverDirection direction ) { this.currentDirection = direction; } /** * Adds pop-over listener. * * @param listener pop-over listener */ public void addPopOverListener ( final PopOverListener listener ) { popOverListeners.add ( listener ); } /** * Removes pop-over listener. * * @param listener pop-over listener */ public void removePopOverListener ( final PopOverListener listener ) { popOverListeners.remove ( listener ); } /** * Informs that this pop-over was detached from invoker component. */ public void firePopOverDetached () { for ( final PopOverListener listener : CollectionUtils.copy ( popOverListeners ) ) { listener.popOverDetached (); } } /** * Informs that this pop-over was closed. */ public void firePopOverClosed () { for ( final PopOverListener listener : CollectionUtils.copy ( popOverListeners ) ) { listener.popOverClosed (); } } }