/* * Bibliothek - DockingFrames * Library built on Java/Swing, allows the user to "drag and drop" * panels containing any Swing-Component the developer likes to add. * * Copyright (C) 2008 Benjamin Sigg * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Benjamin Sigg * benjamin_sigg@gmx.ch * CH - Switzerland */ package bibliothek.gui.dock.station.screen.window; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.GridLayout; import java.awt.Insets; import java.awt.LayoutManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.SwingUtilities; import bibliothek.gui.DockController; import bibliothek.gui.Dockable; import bibliothek.gui.dock.ScreenDockStation; import bibliothek.gui.dock.event.DockableAdapter; import bibliothek.gui.dock.event.DockableListener; import bibliothek.gui.dock.security.SecureContainer; import bibliothek.gui.dock.station.DockableDisplayer; import bibliothek.gui.dock.station.StationPaint; import bibliothek.gui.dock.station.screen.ScreenDockWindow; import bibliothek.gui.dock.station.screen.ScreenDockWindowListener; import bibliothek.gui.dock.station.screen.magnet.MagnetizedOperation; import bibliothek.gui.dock.station.screen.window.ScreenDockWindowBorder.Position; import bibliothek.gui.dock.station.support.CombinerTarget; import bibliothek.gui.dock.themes.ThemeManager; import bibliothek.gui.dock.themes.border.BorderForwarder; import bibliothek.gui.dock.util.BackgroundAlgorithm; import bibliothek.gui.dock.util.BackgroundPanel; import bibliothek.gui.dock.util.Transparency; import bibliothek.util.Workarounds; /** * This abstract implementation of {@link ScreenDockWindow} uses a {@link DockableDisplayer} * to show the {@link Dockable}. It can operate with any window. Clients need to * call {@link #init(Component, Container, WindowConfiguration, boolean)} to put this object onto some * {@link Container}. * @author Benjamin Sigg */ public abstract class AbstractScreenDockWindow extends DisplayerScreenDockWindow{ /** the component which represents the window */ private Component window; /** the elements to display */ private DockableDisplayer displayer; /** the parent of {@link #displayer}, used to paint */ private SecureContainer content; /** how to paint a combination */ private CombinerTarget combination; /** whether a drag and drop operation currently removes the child of this window */ private boolean removal = false; /** the explicit set icon */ private Icon titleIcon = null; /** the explicit set text */ private String titleText = null; /** the background algorithm for this window */ private BackgroundAlgorithm background; /** the panel which paints the background */ private BackgroundPanel contentBackground; /** the default border of this window */ private ScreenDockWindowBorder border; /** the current modifier of the border */ private WindowBorder borderModifier; /** responsible for updating the shape of this window */ private ScreenWindowShapeAdapter shape; /** a listener added to the <code>Dockable</code> of this window, updates icon and title text */ private DockableListener listener = new DockableAdapter(){ @Override public void titleIconChanged( Dockable dockable, Icon oldIcon, Icon newIcon ) { updateTitleIcon(); } @Override public void titleTextChanged( Dockable dockable, String oldTitle, String newTitle ) { updateTitleText(); } }; /** a listener added to this window, will inform other listeners about a changed fullscreen mode when this window is resized */ private ScreenDockWindowListener windowListener = new ScreenDockWindowListener() { /** the last remembered state */ private boolean remembered = false; public void visibilityChanged( ScreenDockWindow window ) { if( isFullscreen() != remembered ){ fireFullscreenChanged(); } } public void shapeChanged( ScreenDockWindow window ) { if( isFullscreen() != remembered ){ fireFullscreenChanged(); } } public void fullscreenStateChanged( ScreenDockWindow window ) { remembered = isFullscreen(); } public void windowClosing( ScreenDockWindow window ){ // ignore } }; /** * Creates a new window. Subclasses must call {@link #init(Component, Container, WindowConfiguration, boolean)} * when using this constructor. * @param station the owner of this window * @param configuration the configuration to apply during creation of this window */ protected AbstractScreenDockWindow( ScreenDockStation station, WindowConfiguration configuration ){ super( station, configuration ); } /** * Creates a new window. * @param station the owner of this window * @param configuration the configuration to apply during creation of this window * @param window the root component of this window * @param contentParent the container onto which the contents of this window will be put * @see #init(Component, Container, WindowConfiguration, boolean) */ public AbstractScreenDockWindow( ScreenDockStation station, WindowConfiguration configuration, Component window, Container contentParent ){ super( station, configuration ); init( window, contentParent, configuration, configuration.isResizeable() ); } /** * Initializes this window. * @param window the component which represents the window. This component * will be used when calling methods like {@link #setWindowBounds(Rectangle, boolean)}. It * is the root of this whole window. * @param contentParent the container which will be used as parent for the * contents of this window. This method will change the {@link LayoutManager} * and add a child to <code>contentParent</code>. This component can be * the same as <code>window</code>. * @param borderAllowed If <code>true</code> and if {@link WindowConfiguration#isResizeable()}, then a new border is installed * for the {@link #getDisplayerParent() displayer parent}, and some {@link MouseListener}s * are installed. When the mouse is over the border it will change the cursor * and the user can resize or move the window. If <code>false</code> * nothing happens and the resizing system has to be implemented by the * subclass. */ protected void init( Component window, Container contentParent, WindowConfiguration configuration, boolean borderAllowed ){ if( window == null ) throw new IllegalArgumentException( "window must not be null" ); if( contentParent == null ) throw new IllegalArgumentException( "contentParent must not be null" ); this.window = window; content = createContent( configuration ); content.setController( getController() ); if( configuration.isResizeable() ){ contentParent.setLayout( new GridLayout( 1, 1 ) ); } else{ contentParent.setLayout( new ResizingLayoutManager( this, window ) ); } contentParent.add( content ); Container parent = getDisplayerParent(); parent.setLayout( new GridLayout( 1, 1 )); if( (configuration.isResizeable() || configuration.isMoveOnBorder()) && borderAllowed ){ if( parent instanceof JComponent && configuration.getBorderFactory() != null ){ border = configuration.getBorderFactory().create( this, (JComponent)parent ); border.setController( getController() ); borderModifier = new WindowBorder( (JComponent)parent ); borderModifier.setBorder( border ); borderModifier.setController( getController() ); } Listener listener = new Listener(); parent.addMouseListener( listener ); parent.addMouseMotionListener( listener ); parent.addComponentListener( listener ); } window.addComponentListener( new ComponentAdapter() { @Override public void componentResized( ComponentEvent e ) { fireShapeChanged(); } @Override public void componentMoved( ComponentEvent e ) { fireShapeChanged(); } }); addScreenDockWindowListener( windowListener ); } @Override protected WindowMover createTitleMover(){ return new WindowMover( this ){ @Override protected void convertPointToScreen( Point point, Component component ){ AbstractScreenDockWindow.this.convertPointToScreen( point, component ); } }; } @Override public void setController( DockController controller ){ super.setController( controller ); content.setController( controller ); if( border != null ){ border.setController( controller ); } if( borderModifier != null ){ borderModifier.setController( controller ); } } /** * Sets the algorithm which is responsible for updating the shape of this window. The algorithm remains * active until {@link #destroy()} is called.<br> * Note: This method does nothing if {@link Workarounds#supportsTransparency(Window)} returns <code>false</code>. * @param window the {@link Window} representing <code>this</code>, it should be {@link #getWindowComponent()} * @param shape the algorithm defining the new shape */ protected void setShape( Window window, ScreenWindowShape shape ){ if( Workarounds.getDefault().supportsTransparency( window )){ if( this.shape != null ){ this.shape.disable(); this.shape = null; } if( shape != null ){ this.shape = new ScreenWindowShapeAdapter( this, window ); this.shape.setShape( shape ); } } } @Override protected Component getWindowComponent() { return window; } @Override protected void setBackground( BackgroundAlgorithm background ){ this.background = background; if( contentBackground != null ){ contentBackground.setBackground( background ); } window.repaint(); } @Override public void setDockable( Dockable dockable ) { Dockable old = getDockable(); if( old != null ){ old.removeDockableListener( listener ); } super.setDockable( dockable ); if( dockable != null ){ dockable.addDockableListener( listener ); } updateTitleIcon(); updateTitleText(); } @Override protected void showDisplayer( DockableDisplayer displayer ) { if( this.displayer != displayer ){ if( this.displayer != null ){ getDisplayerParent().remove( this.displayer.getComponent() ); } this.displayer = displayer; if( displayer != null ){ getDisplayerParent().add( displayer.getComponent() ); } } validate(); } /** * Explicitly sets the icon of the title. * @param titleIcon the new icon or <code>null</code> to use the * {@link Dockable}s icon. */ public void setTitleIcon( Icon titleIcon ) { this.titleIcon = titleIcon; updateTitleIcon(); } /** * Gets the icon which should be used in the title. * @return the icon */ protected Icon getTitleIcon(){ if( titleIcon != null ) return titleIcon; Dockable dockable = getDockable(); if( dockable == null ) return null; return dockable.getTitleIcon(); } /** * Called when the icon of the title should be updated. * @see #getTitleIcon() */ protected void updateTitleIcon(){ // nothing to do } /** * Explicitly sets the text of the title. * @param titleText the new text or <code>null</code> to use * the {@link Dockable}s title text. */ public void setTitleText( String titleText ) { this.titleText = titleText; updateTitleText(); } /** * Gets the text which should be used in the title. * @return the text, might be <code>null</code> */ protected String getTitleText(){ if( titleText != null ) return titleText; Dockable dockable = getDockable(); if( dockable == null ) return null; return dockable.getTitleText(); } /** * Called when the text of the title should be updated. * @see #getTitleText() */ protected void updateTitleText(){ // nothing to do } public Rectangle getWindowBounds() { return window.getBounds(); } public Component getComponent() { return window; } public void setPaintCombining( CombinerTarget target ){ this.combination = target; window.repaint(); } public void setPaintRemoval( boolean removal ){ this.removal = removal; window.repaint(); } public void setVisible( boolean visible ) { if( visible != isVisible() ){ window.setVisible( visible ); fireVisibilityChanged(); checkWindowBoundsAsync(); } } public boolean isVisible() { return window.isVisible(); } /** * Sets the boundaries of this window. If the boundaries are not valid, then this method tries to ensure that * the edge or side <i>opposite</i> to <code>position</code> gets at the intended position. This is a convenient * method for resizing the window when the mouse is dragging the edge or side at <code>position</code>. * @param bounds the new bounds * @param position the <i>opposite</i> of the edge or side that is fixed */ public void setWindowBounds( Rectangle bounds, Position position ){ Rectangle valid = getStation().getBoundaryRestriction().check( this, bounds ); if( valid == null ){ setWindowBounds( bounds ); } else{ switch( position ){ case E: bounds = new Rectangle( bounds.x, valid.y, valid.width, valid.height ); break; case N: bounds = new Rectangle( valid.x, bounds.y + bounds.height - valid.height, valid.width, valid.height ); break; case S: bounds = new Rectangle( valid.x, bounds.y, valid.width, valid.height ); break; case W: bounds = new Rectangle( bounds.x + bounds.width - valid.width, bounds.y, valid.width, valid.height ); break; case NE: bounds = new Rectangle( bounds.x, bounds.y + bounds.height - valid.height, valid.width, valid.height ); break; case NW: bounds = new Rectangle( bounds.x + bounds.width - valid.width, bounds.y + bounds.height - valid.height, valid.width, valid.height ); break; case SE: bounds = new Rectangle( bounds.x, bounds.y, valid.width, valid.height ); break; case SW: bounds = new Rectangle( bounds.x + bounds.width - valid.width, bounds.y, valid.width, valid.height ); break; default: bounds = valid; break; } setWindowBounds( bounds ); } } public void setWindowBounds( Rectangle bounds ){ Rectangle valid = getStation().getBoundaryRestriction().check( this, bounds ); if( valid != null ){ bounds = valid; } if( !window.getBounds().equals( bounds )){ window.setBounds( bounds ); invalidate(); validate(); } } /** * Adds an event into the EDT that calls {@link #checkWindowBounds()} at a later time, * the boundaries will only be checked if this window is visible. */ public void checkWindowBoundsAsync(){ SwingUtilities.invokeLater( new Runnable() { public void run() { if( isVisible() ){ checkWindowBounds(); } } }); } public void checkWindowBounds() { Rectangle valid = getStation().getBoundaryRestriction().check( this ); if( valid != null ){ window.setBounds( valid ); } } /** * Invalidates the layout of this window. * @see Component#invalidate() */ public void invalidate(){ window.invalidate(); } public void validate() { window.validate(); } /** * Sets the current cursor of this window * @param cursor the cursor * @see Component#setCursor(Cursor) */ protected void setCursor( Cursor cursor ){ window.setCursor( cursor ); } /** * Converts <code>point</code> which is relative to <code>component</code> to a point on the screen. * @param point the point to modify * @param component specifies the coordinate system * @see SwingUtilities#convertPointToScreen(Point, Component) */ protected void convertPointToScreen( Point point, Component component ){ SwingUtilities.convertPointToScreen( point, component ); } /** * Makes a guess how big the insets of the {@link Dockable} compared to * the whole dialog are. * @return the insets, only a guess */ public Insets getDockableInsets(){ Container parent = getDisplayerParent(); Insets parentInsets = parent.getInsets(); if( parentInsets == null ) parentInsets = new Insets( 0, 0, 0, 0 ); Point zero = new Point( 0, 0 ); zero = SwingUtilities.convertPoint( parent, zero, window ); parentInsets.left += zero.x; parentInsets.top += zero.y; parentInsets.right += window.getWidth() - parent.getWidth() - zero.x; parentInsets.bottom += window.getHeight() - parent.getHeight() - zero.y; if( displayer == null ) return parentInsets; Insets insets = displayer.getDockableInsets(); parentInsets.top += insets.top; parentInsets.bottom += insets.bottom; parentInsets.left += insets.left; parentInsets.right += insets.right; return parentInsets; } /** * Creates the component that will be used as * {@link JDialog#setContentPane(Container) content-pane}. * This method is invoked by the constructor. * @param configuration the configuration of this window * @return the new content pane */ protected SecureContainer createContent( WindowConfiguration configuration ){ if( configuration.isTransparent() ){ contentBackground = new BackgroundPanel( Transparency.TRANSPARENT ){ @Override protected void configure( Transparency transparency ){ setTransparency( transparency ); } @Override protected void setupRenderingHints( Graphics g ) { // ignore } }; } else{ contentBackground = new BackgroundPanel( Transparency.DEFAULT ){ @Override protected void configure( Transparency transparency ){ // does not support transparency as this is a root component } @Override protected void setupRenderingHints( Graphics g ) { // ignore } }; } contentBackground.setBackground( background ); SecureContainer panel = new SecureContainer(){ @Override protected void paintOverlay( Graphics g ) { boolean removal = AbstractScreenDockWindow.this.removal; if( isMoveOnTitleGrab() ){ removal = false; } if( combination != null || removal ){ ScreenDockStation station = getStation(); StationPaint paint = station.getPaint().get(); if( paint != null ){ Insets insets = getInsets(); Rectangle bounds = new Rectangle( 0, 0, getWidth(), getHeight() ); Rectangle insert = new Rectangle( 2*insets.left, 2*insets.top, getWidth() - 2*(insets.left+insets.right), getHeight() - 2*(insets.top+insets.bottom )); if( combination != null ){ combination.paint( g, contentBackground, paint, bounds, insert ); } else if( removal ){ paint.drawRemoval( g, station, bounds, insert ); } } } } }; if( configuration.isTransparent() ){ panel.setSolid( false ); } panel.setContentPane( contentBackground ); panel.getBasePane().setLayout( new BorderLayout(){ private Dimension lastMinimumSize; @Override public void layoutContainer( Container target ){ Dimension minimumSize = minimumLayoutSize( target ); if( lastMinimumSize == null || !lastMinimumSize.equals( minimumSize )){ lastMinimumSize = minimumSize; checkWindowBounds(); } super.layoutContainer( target ); } } ); panel.getBasePane().add( contentBackground, BorderLayout.CENTER ); return panel; } /** * Gets the container on which the displayer is shown. * @return the parent of the displayer */ protected Container getDisplayerParent(){ return content.getContentPane(); } /** * Gets the displayer which is shown on this dialog. * @return The displayer, may be <code>null</code> * @see #showDisplayer(DockableDisplayer) */ public DockableDisplayer getDisplayer() { return displayer; } public void destroy(){ if( shape != null ){ shape.disable(); } } /** * Represents the border of this window * @author Benjamin Sigg */ private class WindowBorder extends BorderForwarder implements ScreenDockWindowDockBorder{ public WindowBorder( JComponent target ){ super( ScreenDockWindowDockBorder.KIND, ThemeManager.BORDER_MODIFIER + ".screen.window", target ); } public ScreenDockWindow getWindow(){ return AbstractScreenDockWindow.this; } } private class Listener implements MouseListener, MouseMotionListener, ComponentListener{ private boolean pressed = false; private Position position = Position.NOTHING; private Point start; private Rectangle bounds; private MagnetizedOperation attraction; private void updateBorder(){ if( border != null ){ if( pressed ){ border.setMousePressed( position ); border.setMouseOver( null ); } else{ border.setMousePressed( null ); border.setMouseOver( position ); } border.setCornerSize( corner() ); WindowConfiguration configuration = getConfiguration(); if( configuration == null ){ border.setMoveable( false ); border.setResizeable( false ); border.setMoveSize( 0 ); border.setCornerSize( 0 ); } else{ border.setMoveable( configuration.isMoveOnBorder() ); border.setResizeable( configuration.isResizeable() ); if( configuration.isMoveOnBorder() ){ border.setMoveSize( getDisplayerParent().getWidth()/3 ); } else{ border.setMoveSize( 0 ); } } } } public void componentHidden( ComponentEvent e ){ // ignore } public void componentMoved( ComponentEvent e ){ // ignore } public void componentResized( ComponentEvent e ){ updateBorder(); } public void componentShown( ComponentEvent e ){ // ignore } private int corner(){ WindowConfiguration configuration = getConfiguration(); if( configuration == null || !configuration.isResizeable() ){ return 0; } Container component = getDisplayerParent(); Insets insets = component.getInsets(); int corner = Math.max( Math.max( insets.top, insets.bottom ), Math.max( insets.left, insets.right ) ) * 5; corner = Math.max( 25, Math.min( 50, corner ) ); corner = Math.min( Math.min( component.getHeight()/2, component.getWidth()/3 ), corner ); return corner; } public void mouseClicked( MouseEvent e ) { // do nothing } public void mousePressed( MouseEvent e ) { updateCursor( e ); if( !pressed ){ if( e.getButton() == MouseEvent.BUTTON1 ){ pressed = true; start = e.getPoint(); convertPointToScreen( start, e.getComponent() ); bounds = getWindowBounds(); updateBorder(); attraction = getStation().getMagnetController().start( AbstractScreenDockWindow.this ); } } } public void mouseReleased( MouseEvent e ) { if( pressed && e.getButton() == MouseEvent.BUTTON1 ){ pressed = false; updateCursor( e ); checkWindowBounds(); invalidate(); validate(); attraction.stop(); attraction = null; } } public void mouseEntered( MouseEvent e ) { if( !pressed && e.getButton() == MouseEvent.NOBUTTON ){ updateCursor( e ); position = Position.NOTHING; } } public void mouseExited( MouseEvent e ) { if( !pressed && e.getButton() == MouseEvent.NOBUTTON ){ setCursor( Cursor.getDefaultCursor() ); position = Position.NOTHING; updateBorder(); } } public void mouseDragged( MouseEvent e ) { if( pressed ){ Point point = e.getPoint(); convertPointToScreen( point, e.getComponent() ); int dx = point.x - start.x; int dy = point.y - start.y; Rectangle bounds = new Rectangle( this.bounds ); int min = 10; if( position == Position.N || position == Position.NE || position == Position.NW ){ bounds.height -= dy; bounds.y += dy; if( bounds.height < min ){ bounds.y -= min - bounds.height; bounds.height = min; } } if( position == Position.E || position == Position.NE || position == Position.SE ){ bounds.width += dx; if( bounds.width < min ) bounds.width = min; } if( position == Position.S || position == Position.SE || position == Position.SW ){ bounds.height += dy; if( bounds.height < min ) bounds.height = min; } if( position == Position.W || position == Position.SW || position == Position.NW ){ bounds.width -= dx; bounds.x += dx; if( bounds.width < min ){ bounds.x -= min - bounds.width; bounds.width = min; } } if( position == Position.MOVE ){ bounds.x += dx; bounds.y += dy; } bounds = attraction.attract( bounds ); setWindowBounds( bounds, position ); updateBorder(); invalidate(); validate(); } } public void mouseMoved( MouseEvent e ) { if( !pressed && e.getButton() == MouseEvent.NOBUTTON ) updateCursor( e ); } private void updateCursor( MouseEvent e ){ Container component = getDisplayerParent(); Insets insets = component.getInsets(); boolean valid = e.getComponent() == component && e.getY() <= insets.top || e.getY() >= component.getHeight() - insets.bottom || e.getX() <= insets.left || e.getX() >= component.getWidth() - insets.right; if( valid ){ WindowConfiguration configuration = getConfiguration(); if( configuration.isMoveOnBorder() && !configuration.isResizeable()){ setCursor( Cursor.getPredefinedCursor( Cursor.MOVE_CURSOR )); position = Position.MOVE; } else{ int corner = corner(); boolean top = e.getY() <= corner; boolean left = e.getX() <= corner; boolean bottom = e.getY() >= component.getHeight() - corner; boolean right = e.getX() >= component.getWidth() - corner; if( top && left ){ setCursor( Cursor.getPredefinedCursor( Cursor.NW_RESIZE_CURSOR ) ); position = Position.NW; } else if( top && right ){ setCursor( Cursor.getPredefinedCursor( Cursor.NE_RESIZE_CURSOR ) ); position = Position.NE; } else if( bottom && right ){ setCursor( Cursor.getPredefinedCursor( Cursor.SE_RESIZE_CURSOR ) ); position = Position.SE; } else if( bottom && left ){ setCursor( Cursor.getPredefinedCursor( Cursor.SW_RESIZE_CURSOR ) ); position = Position.SW; } else if( top ){ int width = component.getWidth(); if( configuration.isMoveOnBorder() && e.getX() > width / 3 && e.getX() < width / 3 * 2 ){ setCursor( Cursor.getPredefinedCursor( Cursor.MOVE_CURSOR )); position = Position.MOVE; } else{ setCursor( Cursor.getPredefinedCursor( Cursor.N_RESIZE_CURSOR ) ); position = Position.N; } } else if( bottom ){ setCursor( Cursor.getPredefinedCursor( Cursor.S_RESIZE_CURSOR ) ); position = Position.S; } else if( left ){ setCursor( Cursor.getPredefinedCursor( Cursor.W_RESIZE_CURSOR ) ); position = Position.W; } else if( right ){ setCursor( Cursor.getPredefinedCursor( Cursor.E_RESIZE_CURSOR ) ); position = Position.E; } else{ setCursor( Cursor.getDefaultCursor() ); position = Position.NOTHING; } } } else{ setCursor( Cursor.getDefaultCursor() ); position = Position.NOTHING; } updateBorder(); } } }