/*
* 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) 2007 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;
import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;
import javax.swing.JDesktopPane;
import javax.swing.LookAndFeel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import bibliothek.extension.gui.dock.theme.BubbleTheme;
import bibliothek.extension.gui.dock.theme.EclipseTheme;
import bibliothek.extension.gui.dock.theme.FlatTheme;
import bibliothek.extension.gui.dock.theme.SmoothTheme;
import bibliothek.gui.dock.DockFactory;
import bibliothek.gui.dock.action.DockAction;
import bibliothek.gui.dock.themes.BasicTheme;
import bibliothek.gui.dock.themes.NoStackTheme;
import bibliothek.gui.dock.themes.ThemeFactory;
import bibliothek.gui.dock.themes.ThemeProperties;
import bibliothek.gui.dock.themes.ThemePropertyFactory;
import bibliothek.gui.dock.util.IconManager;
import bibliothek.gui.dock.util.laf.DefaultLookAndFeelColors;
import bibliothek.gui.dock.util.laf.LookAndFeelColors;
import bibliothek.gui.dock.util.laf.LookAndFeelColorsListener;
import bibliothek.gui.dock.util.laf.Nimbus6u10;
import bibliothek.gui.dock.util.laf.Windows;
import bibliothek.util.container.Tuple;
/**
* A list of icons, text and methods used by the framework.
* @author Benjamin Sigg
*/
public class DockUI {
/** An instance of DockUI */
private static DockUI ui;
/**
* Key for an {@link Icon} stored in the {@link IconManager} for the action-overflow-menu. This menu is shown if there
* are too many {@link DockAction}s to show.
*/
public static final String OVERFLOW_MENU_ICON = "overflow.menu";
/** A list of all available themes */
private List<ThemeFactory> themes = new ArrayList<ThemeFactory>();
/** contains regex-LookAndFeelColor pairs */
private List<Tuple<String, LookAndFeelColors>> lookAndFeelColors = new ArrayList<Tuple<String,LookAndFeelColors>>();
/** the currently used {@link LookAndFeelColors} */
private LookAndFeelColors lookAndFeelColor;
/** a list of color listeners that is called from {@link #colorsListeners} */
private List<LookAndFeelColorsListener> colorsListeners = new ArrayList<LookAndFeelColorsListener>();
/** whether this is a secure environment where global {@link AWTEventListener}s are not allowed */
private Boolean secureEnvironment = null;
/** a listener added to {@link #lookAndFeelColor} */
private LookAndFeelColorsListener colorsListener = new LookAndFeelColorsListener(){
public void colorChanged( String key ) {
for( LookAndFeelColorsListener listener : colorsListeners.toArray( new LookAndFeelColorsListener[ colorsListeners.size()] ))
listener.colorChanged( key );
}
public void colorsChanged() {
for( LookAndFeelColorsListener listener : colorsListeners.toArray( new LookAndFeelColorsListener[ colorsListeners.size()] ))
listener.colorsChanged();
}
};
/**
* Gets the default instance of DockUI.
* @return the instance
*/
public static DockUI getDefaultDockUI(){
if( ui == null ){
synchronized( DockUI.class ){
if( ui == null ){
ui = new DockUI();
}
}
}
return ui;
}
/**
* Creates a new DockUI
*/
protected DockUI(){
registerThemes();
registerColors();
UIManager.addPropertyChangeListener( new PropertyChangeListener(){
public void propertyChange( PropertyChangeEvent evt ) {
if( "lookAndFeel".equals( evt.getPropertyName() )){
updateUI();
}
}
});
}
/**
* Called when the {@link LookAndFeel} changed.
*/
protected void updateUI(){
updateLookAndFeelColors();
}
private void registerThemes(){
registerTheme( BasicTheme.class );
registerTheme( FlatTheme.class );
registerTheme( SmoothTheme.class );
registerTheme( BubbleTheme.class );
registerTheme( EclipseTheme.class );
registerTheme( NoStackTheme.getFactory( BasicTheme.class ));
registerTheme( NoStackTheme.getFactory( FlatTheme.class ));
registerTheme( NoStackTheme.getFactory( SmoothTheme.class ));
registerTheme( NoStackTheme.getFactory( BubbleTheme.class ));
}
private void registerColors(){
registerColors( ".+", new DefaultLookAndFeelColors() );
String jvmVersionString = System.getProperty("java.specification.version");
int verIndex = jvmVersionString.indexOf("1.");
if (verIndex >= 0) { // handle Java 9
jvmVersionString = jvmVersionString.substring(verIndex + 2);
}
int major = Integer.parseInt(jvmVersionString);
if( major >= 7 ){
registerColors( "javax\\.swing\\.plaf\\.nimbus\\.NimbusLookAndFeel", new Nimbus6u10() );
}
else{
registerColors( "com\\.sun\\.java\\.swing\\.plaf\\.nimbus\\.NimbusLookAndFeel", new Nimbus6u10() );
}
registerColors( "com\\.sun\\.java\\.swing\\.plaf\\.windows\\.WindowsLookAndFeel", new Windows() );
}
/**
* Gets the default-theme to be used by all {@link DockController}s when
* nothing else is specified.
* @return the default-theme
*/
public ThemeFactory getDefaultTheme(){
return themes.get( 0 );
}
/**
* Gets the list of all available themes.
* @return the themes
*/
public ThemeFactory[] getThemes(){
return themes.toArray( new ThemeFactory[ themes.size() ] );
}
/**
* Registers a factory for <code>theme</code>.
* @param <T> the type of the {@link DockTheme}.
* @param theme A class which must have the annotation
* {@link ThemeProperties}
*/
public <T extends DockTheme> void registerTheme( Class<T> theme ){
registerTheme( new ThemePropertyFactory<T>( theme ));
}
/**
* Stores a new theme.
* @param factory the new theme
*/
public void registerTheme( ThemeFactory factory ){
if( factory == null )
throw new IllegalArgumentException( "Theme must not be null" );
themes.add( factory );
}
/**
* Removes an earlier added factory from the set of theme-factories.
* @param factory the factory to remove
*/
public void unregisterTheme( ThemeFactory factory ){
themes.remove( factory );
}
/**
* Registers a new {@link LookAndFeelColors}. The <code>lookAndFeelClassNameRegex</code>
* is a regular expression. If a {@link LookAndFeel} is active whose class name
* {@link String#matches(String) matches} <code>lookAndFeelClassNameRegex</code>,
* then <code>colors</code> becomes the selected source for colors. If more
* then one regex matches, the last one that was added to this {@link DockUI}
* is taken. So generally one would first add the most general regexes, and
* the more detailed ones later.
* @param lookAndFeelClassNameRegex a description of a class name
* @param colors the new set of colors
*/
public void registerColors( String lookAndFeelClassNameRegex, LookAndFeelColors colors ){
if( lookAndFeelClassNameRegex == null )
throw new IllegalArgumentException( "lookAndFeelClassNameRegex must not be null" );
if( colors == null )
throw new IllegalArgumentException( "colors must not be null" );
lookAndFeelColors.add( new Tuple<String, LookAndFeelColors>( lookAndFeelClassNameRegex, colors ));
updateLookAndFeelColors();
}
/**
* Adds a listener which gets informed when a color of the current
* {@link LookAndFeelColors} changes. This listener gets not informed
* about any changes when the {@link LookAndFeel} itself gets replaced.
* This listener will automatically be transferred when another
* {@link LookAndFeelColors} gets selected.
* @param listener the new listener, not <code>null</code>
*/
public void addLookAndFeelColorsListener( LookAndFeelColorsListener listener ){
if( listener == null )
throw new IllegalArgumentException( "listener must not be null" );
colorsListeners.add( listener );
}
/**
* Removes a listener from this {@link DockUI}.
* @param listener the listener to remove
*/
public void removeLookAndFeelColorsListener( LookAndFeelColorsListener listener ){
colorsListeners.remove( listener );
}
/**
* Updates the currently used {@link LookAndFeelColors} to the best
* matching colors.
*/
protected void updateLookAndFeelColors(){
LookAndFeelColors next = selectBestMatchingColors();
if( next != lookAndFeelColor ){
if( lookAndFeelColor != null ){
lookAndFeelColor.unbind();
lookAndFeelColor.removeListener( colorsListener );
}
lookAndFeelColor = next;
if( next != null ){
next.bind();
lookAndFeelColor.addListener( colorsListener );
}
colorsListener.colorsChanged();
}
}
/**
* Gets the {@link LookAndFeelColors} which matches the current
* {@link LookAndFeel} best.
* @return the current set of colors
*/
protected LookAndFeelColors selectBestMatchingColors(){
String className = UIManager.getLookAndFeel().getClass().getName();
for( int i = lookAndFeelColors.size()-1; i >= 0; i-- ){
if( className.matches( lookAndFeelColors.get( i ).getA() ))
return lookAndFeelColors.get( i ).getB();
}
return null;
}
/**
* Gets the current source of colors that depend on the {@link LookAndFeel}.
* @return the current source of colors
*/
public LookAndFeelColors getColors(){
return lookAndFeelColor;
}
/**
* Gets the color <code>key</code> where <code>key</code> is one of
* the keys specified in {@link LookAndFeelColors}.
* @param key the name of the color
* @return the color or <code>null</code>
*/
public static Color getColor( String key ){
return getDefaultDockUI().getColors().getColor( key );
}
/**
* Removes all children of <code>station</code> and then adds
* the children again. Reading the children ensures that all components are
* build up again with the current theme of the station
* @param <D> the type of the station
* @param <L> the type of the layout needed to describe the contents
* of the station
* @param station the station to update
* @param factory a factory used to remove and to add the elements
* @throws IOException if the factory throws an exception
*/
public static <D extends DockStation, L> void updateTheme( D station, DockFactory<D,?,L> factory ) throws IOException{
Map<Integer, Dockable> children = new HashMap<Integer, Dockable>();
Map<Dockable, Integer> ids = new HashMap<Dockable, Integer>();
for( int i = 0, n = station.getDockableCount(); i<n; i++ ){
Dockable child = station.getDockable(i);
children.put(i, child);
ids.put(child, i);
}
L layout = factory.getLayout( station, ids );
DockController controller = station.getController();
if( controller != null ){
controller.getRegister().setStalled( true );
controller.getHierarchyLock().setConcurrent( true );
}
try{
for( int i = station.getDockableCount()-1; i >= 0; i-- ){
station.drag( station.getDockable( i ));
}
factory.setLayout( station, layout, children, null );
}
finally{
if( controller != null ){
controller.getRegister().setStalled( false );
controller.getHierarchyLock().setConcurrent( false );
}
}
}
/**
* Searches the first {@link JDesktopPane} which either is <code>component</code>
* or a parent of <code>component</code>.
* @param component the component whose parent is searched
* @return the parent {@link JDesktopPane} or <code>null</code> if not found
*/
public static JDesktopPane getDesktopPane( Component component ){
while( component != null ){
if( component instanceof JDesktopPane ){
return ((JDesktopPane)component);
}
component = component.getParent();
}
return null;
}
/**
* Tells whether <code>above</code> overlaps <code>under</code>. This method
* assumes that both components have a mutual parent. The method checks the location
* and the z-order of both components.
* @param above the component that is supposed to be above <code>under</code>
* @param under the component that is supposed to be under <code>above</code>
* @return <code>true</code> is <code>above</code> is overlapping <code>under</code>
*/
public static boolean isOverlapping( Component above, Component under ){
if( SwingUtilities.isDescendingFrom( under, above )){
return false;
}
if( SwingUtilities.isDescendingFrom( above, under )){
return true;
}
if( above == under ){
return true;
}
Container parent = above.getParent();
while( parent != null ){
if( SwingUtilities.isDescendingFrom( under, parent )){
// found mutual parent
Point locationA = new Point( 0, 0 );
Point locationU = new Point( 0, 0 );
locationA = SwingUtilities.convertPoint( above, locationA, parent );
locationU = SwingUtilities.convertPoint( under, locationU, parent );
Rectangle boundsA = new Rectangle( locationA, above.getSize() );
Rectangle boundsU = new Rectangle( locationU, under.getSize() );
if( !boundsA.intersects( boundsU )){
return false;
}
Component pathA = firstOnPath( parent, above );
Component pathU = firstOnPath( parent, under );
int zA = parent.getComponentZOrder( pathA );
int zU = parent.getComponentZOrder( pathU );
return zA < zU;
}
parent = parent.getParent();
}
return false;
}
/**
* Tells whether this application runs in a restricted environment or not. This method
* only makes a guess and may return a false result.
* @return whether this is a restricted environment
*/
public boolean isSecureEnvironment(){
if( secureEnvironment != null ){
return secureEnvironment;
}
try{
Toolkit toolkit = Toolkit.getDefaultToolkit();
AWTEventListener listener = new AWTEventListener(){
public void eventDispatched( AWTEvent event ){
// ignore
}
};
toolkit.addAWTEventListener( listener, AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.MOUSE_WHEEL_EVENT_MASK );
toolkit.removeAWTEventListener( listener );
// SecurityManager security = System.getSecurityManager();
// if( security != null ){
// security.checkPermission(SecurityConstants.ALL_AWT_EVENTS_PERMISSION);
// }
}
catch( SecurityException ex ){
secureEnvironment = true;
return true;
}
secureEnvironment = false;
return false;
}
/**
* Overrides the result of {@link #isSecureEnvironment()}, any future call of that method will
* return <code>secureEnvironment</code>.
* @param secureEnvironment Whether global {@link AWTEventListener}s are allowed or not, a value of <code>true</code>
* indicates that listeners are not allowed
*/
public void setSecureEnvironment( boolean secureEnvironment ){
this.secureEnvironment = secureEnvironment;
}
private static Component firstOnPath( Container parent, Component child ){
Component result = child;
while( result.getParent() != parent ){
result = result.getParent();
}
return result;
}
}