/*
* Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved.
* Copyright (C) 2011 Peransin Nicolas. All rights reserved.
* Use is subject to license terms.
*/
package org.mypsycho.swing.app;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Window;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JToolBar;
import javax.swing.RootPaneContainer;
import org.mypsycho.swing.app.utils.SwingHelper;
/**
* A View encapsulates a top-level Application GUI component, like a JFrame
* or an Applet, and its main GUI elements: a menu bar, tool bar, component,
* and a status bar. All of the elements are optional (although a View without
* a main component would be unusual). Views have a {@code JRootPane}, which
* is the root component for all of the Swing Window types as well as JApplet.
* Setting a View property, like {@code menuBar} or {@code toolBar}, just
* adds a component to the rootPane in a way that's defined by the View subclass.
* By default the View elements are arranged in a conventional way:
* <ul>
* <li> {@code menuBar} - becomes the rootPane's JMenuBar
* <li> {@code toolBar} - added to {@code BorderLayout.NORTH} of the rootPane's contentPane
* <li> {@code component} - added to {@code BorderLayout.CENTER} of the rootPane's contentPane
* <li> {@code statusBar} - added to {@code BorderLayout.SOUTH} of the rootPane's contentPane
* </ul>
* <p>
* To show or hide a View you call the corresponding Application methods. Here's a simple
* example:
* <pre>
* class MyApplication extends SingleFrameApplication {
* @ppOverride protected void startup() {
* View view = getMainView();
* view.setComponent(createMainComponent());
* view.setMenuBar(createMenuBar());
* show(view);
* }
* }
* </pre>
* <p>
* The advantage of Views over just configuring a JFrame or JApplet
* directly, is that a View is more easily moved to an alternative
* top level container, like a docking framework.
*
* @see JRootPane
* @see Application#show(View)
* @see Application#hide(View)
*/
public class View extends SwingBean {
public static final String PROP_NAME = "view";
public static final String VIEW_MARKER = "Application.view";
public static final String COMPONENT_PROP = "component";
public static final String MENUBAR_PROP = "menubar";
public static final String TOOL_BAR_PROP = "toolbar";
public static final String TOOL_BARS_PROP = "toolbars";
public static final String STATUS_BAR_PROP = "statusBar";
private final Application application;
private JRootPane rootPane = null;
private JComponent component = null;
private JMenuBar menuBar = null;
private List<JToolBar> toolBars = Collections.emptyList();
private JComponent toolBarsPanel = null;
private JComponent statusBar = null;
private ViewBehaviour behaviour = null;
private final HierarchyListener visibilityListener = new HierarchyListener() {
@Override
public void hierarchyChanged(HierarchyEvent e) {
if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
if (getRootPane().isShowing()) {
manage();
} else {
release();
}
}
}
};
private final PropertyChangeListener localeListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
Component root = getRootPane();
if (root.getParent() != null) { // get the window
root = root.getParent();
}
Locale old = (Locale) evt.getOldValue();
if (Locales.isSwing(root)) {
if (!Locales.isForced(root)) {
root.setLocale(getApplication().getLocale());
}
} else if (old.equals(root.getLocale())) {
root.setLocale(getApplication().getLocale());
}
}
};
/**
* Construct an empty View object for the specified Application.
*
* @param application the Application responsible for showing/hiding this View
* @see Application#show(View)
* @see Application#hide(View)
*/
public View(Application application) {
this(application, null);
}
/**
* Construct an empty View object for the specified Application.
*
* @param application the Application responsible for showing/hiding this View
* @see Application#show(View)
* @see Application#hide(View)
*/
public View(Application application, JRootPane root) {
SwingHelper.assertNotNull("application", application);
this.application = application;
if (root != null) {
register(root);
}
}
public void register(ViewBehaviour pBehaviour) {
behaviour = pBehaviour;
}
protected synchronized void register(JRootPane root) {
if (rootPane != null) {
throw new IllegalStateException("View already manage rootPane");
}
rootPane = root;
/*
* Although it would have been simpler to listen for changes in
* the secondary window's visibility per either a
* PropertyChangeEvent on the "visible" property or a change in
* visibility per ComponentListener, neither listener is notified
* if the secondary window is disposed.
* HierarchyEvent.SHOWING_CHANGED does report the change in all
* cases, so we use that.
*/
if (!Arrays.asList(root.getHierarchyListeners()).contains(visibilityListener)) {
rootPane.addHierarchyListener(visibilityListener);
}
}
/**
* Insert the window into the component manager
* <p>
* By default, inject the context of the application. This method can be
* overriden to inject context.
* </p>
*
* @param window
*/
protected void injectProperties(Window window) {
// Structural injection
getContext().getComponentManager().register(window);
// Contextual injection
String viewName = getViewProperty(window);
if (viewName != null) {
getContext().getResourceManager().inject(getApplication(), window.getLocale(),
viewName, window);
}
}
protected void manage() {
JComponent root = getRootPane();
// These initializations are only done once
if (root.getClientProperty(VIEW_MARKER) == this) {
return;
}
root.putClientProperty(VIEW_MARKER, this); // set view !!
// Inject resources
Container parent = root.getParent(); // <=> c ??
if (!(parent instanceof Window)) {
getContext().getComponentManager().register(root);
return;
}
Window window = (Window) parent;
// Structural injection
getContext().getComponentManager().register(window);
// Contextual injection
String viewName = getViewProperty(window);
if (viewName != null) {
getContext().getResourceManager().inject(getApplication(), window.getLocale(),
viewName, window);
}
getApplication().addPropertyChangeListener(Locales.LOCALE_PROP, localeListener);
// If this is a JFrame monitor "normal" (not maximized) bounds
if (window instanceof JFrame) {
window.addComponentListener(SwingHelper.FRAME_BOUND_LISTENER);
}
// If the window's bounds don't appear to have been set, do it
if (!window.isValid() || (window.getWidth() == 0) || (window.getHeight() == 0)) {
window.pack();
}
if (behaviour != null) {
behaviour.onManage(this);
}
// If window location is default and size is not too big
// the window should be centered
Point defaultLocation = SwingHelper.defaultLocation(window);
if (!window.isLocationByPlatform() && window.getLocation().equals(defaultLocation)) {
Dimension screenSize = window.getToolkit().getScreenSize();
Dimension windowSize = window.getSize();
double[] ratio = { // ...
screenSize.getWidth() / windowSize.getWidth(),
screenSize.getHeight() / windowSize.getHeight() };
if (ratio[0] > 1.25 && ratio[1] > 1.25) {
Component owner = window.getOwner(); // maybe null
window.setLocationRelativeTo(owner); // center the window
}
}
}
/**
* Return the 'view(<window.name>)'.
* <p>
* <window.name> is not used as SingleFrameApplication will create the frame at the wrong
* time, ('create' instead of 'startup') because of analyse by reflection. (lesser bad)
* </p>
*
* @param windowName
* @return
*/
protected String getViewProperty(Window window) {
if (window == null) {
return null;
}
String name = window.getName();
if (name == null) {
return null;
}
return PROP_NAME + "(" + name + ")";
}
protected void release() {
JComponent root = getRootPane();
if (root.getClientProperty(VIEW_MARKER) != this) {
return;
}
// Note: release if before cleaning component as behaviour need to id
if (behaviour != null) {
behaviour.onRelease(this);
}
// Clean application context from component
getApplication().removePropertyChangeListener(Locales.LOCALE_PROP, localeListener);
root.putClientProperty(VIEW_MARKER, null); // set view !!
getContext().getComponentManager().dispose(getRootPane());
// root.getParent().dispose // ? usefull or required ?
}
/**
* Shows the application {@code View}
*
* @param view - View to show
* @see View
*/
protected void show() {
manage();
Window window = (Window) getRootPane().getParent();
if (window != null) {
window.setVisible(true);
}
}
/**
* Hides the application {@code View}
*
* @param view
* @see View
*/
protected void hide() {
release();
getRootPane().getParent().setVisible(false);
}
/**
* Returns the {@code Application} that's responsible for showing/hiding this View.
*
* @return the Application that owns this View
* @see #getContext
* @see Application#show(View)
* @see Application#hide(View)
*/
public final Application getApplication() {
return application;
}
/**
* Gets the {@code ApplicationContext} for the {@code
* Application} that's responsible for showing/hiding this View.
* This method is just shorthand for {@code getApplication().getContext()}.
*
* @return the Application that owns this View
* @see #getApplication
* @see Application#show(View)
* @see Application#hide(View)
*/
public final ApplicationContext getContext() {
return getApplication().getContext();
}
/**
* Gets the {@code JRootPane} for this View. All of the components for this
* View must be added to its rootPane. Most applications will do so
* by setting the View's {@code component}, {@code menuBar}, {@code toolBar},
* and {@code statusBar} properties.
*
* @return The {@code rootPane} for this View
* @see #setComponent
* @see #setMenuBar
* @see #setToolBar
* @see #setStatusBar
*/
public JRootPane getRootPane() {
if (rootPane == null) {
register(new JRootPane());
rootPane.setOpaque(true);
}
return rootPane;
}
private void replaceChild(JComponent oldChild, JComponent newChild, String constraint, String name) {
Container contentPane = getRootPane().getContentPane();
if (oldChild != null) {
contentPane.remove(oldChild);
}
if (newChild != null) {
contentPane.add(newChild, constraint);
}
firePropertyChange(name, oldChild, newChild);
}
/**
* Returns the main {@link JComponent} for this View.
*
* @return The {@code component} for this View
* @see #setComponent
*/
public JComponent getComponent() {
return component;
}
/**
* Sets the single main Component for this View. It's added to the
* {@code BorderLayout.CENTER} of the rootPane's contentPane. If
* the component property was already set, the old component is removed
* first.
* <p>
* This is a bound property. The default value is null.
*
* @param component The {@code component} for this View
* @see #getComponent
*/
public void setComponent(JComponent component) {
JComponent oldValue = this.component;
this.component = component;
if (component != null && component.getName() == null) {
component.setName(COMPONENT_PROP);
}
replaceChild(oldValue, this.component, BorderLayout.CENTER, COMPONENT_PROP);
}
/**
* Returns the main {@link JMenuBar} for this View.
*
* @return The {@code menuBar} for this View
* @see #setMenuBar
*/
public JMenuBar getMenuBar() {
return menuBar;
}
/**
* Sets the menu bar for this View.
* <p>
* This is a bound property. The default value is null.
*
* @param menuBar The {@code menuBar} for this View
* @see #getMenuBar
*/
public void setMenuBar(JMenuBar menuBar) {
JMenuBar oldValue = getMenuBar();
this.menuBar = menuBar;
getRootPane().setJMenuBar(menuBar);
firePropertyChange(MENUBAR_PROP, oldValue, menuBar);
}
/**
* Returns the list of tool bars for this View
*
* @return The list of tool bars
*/
public List<JToolBar> getToolBars() {
return toolBars;
}
/**
* Sets the tool bars for this View
* <p>
* This is a bound property. The default value is an empty list.
*
* @param toolBars
* @see #setToolBar(JToolBar)
* @see #getToolBars()
*/
public void setToolBars(List<? extends JToolBar> toolBars) {
SwingHelper.assertNotNull(TOOL_BARS_PROP, toolBars);
// List<JToolBar> oldValue = getToolBars();
this.toolBars = Collections.unmodifiableList(new ArrayList<JToolBar>(toolBars));
JComponent oldToolBarsPanel = toolBarsPanel;
JComponent newToolBarsPanel = null;
if (this.toolBars.size() == 1) {
newToolBarsPanel = toolBars.get(0);
} else if (this.toolBars.size() > 1) {
newToolBarsPanel = new JPanel(); // FlowLayout
newToolBarsPanel.setName(TOOL_BARS_PROP);
for (JComponent toolBar : this.toolBars) {
newToolBarsPanel.add(toolBar);
}
}
replaceChild(oldToolBarsPanel, newToolBarsPanel, BorderLayout.PAGE_START, TOOL_BARS_PROP);
}
/**
* Gets the first tool bar for this View
*
* @return The first {@link JToolBar} for this View
* @see #setToolBars
* @see #getToolBars
* @see #setToolBar
*/
public final JToolBar getToolBar() {
List<JToolBar> toolBars = getToolBars();
return (toolBars.size() == 0) ? null : toolBars.get(0);
}
/**
* Sets the only tool bar for this View.
* <p>
* This is a bound property.
*
* @param toolBar The {@link JToolBar} for this view. If {@code null} resets the tool bar.
* @see #getToolBar()
* @see #setToolBars(List)
* @see #getToolBars()
*/
public final void setToolBar(JToolBar toolBar) {
if (toolBar != null) {
if (toolBar.getName() == null) {
toolBar.setName(TOOL_BAR_PROP);
}
setToolBars(Collections.singletonList(toolBar));
} else {
setToolBars(Collections.<JToolBar>emptyList());
}
}
/**
* Returns the Status bar for this View.
*
* @return The status bar {@link JComponent} for this View
*/
public JComponent getStatusBar() {
return statusBar;
}
/**
* Sets the status bar for this View. The status bar is a generic {@link JComponent}.
*
* @param statusBar The status bar {@link JComponent} for this View
*/
public void setStatusBar(JComponent statusBar) {
JComponent oldValue = this.statusBar;
this.statusBar = statusBar;
replaceChild(oldValue, this.statusBar, BorderLayout.PAGE_END, STATUS_BAR_PROP);
}
/**
* Return all of the visible JWindows, JDialogs, and JFrames per
* Window.getWindows() on Java SE 6
*/
public static List<View> getViews(Application app) {
List<View> rv = new ArrayList<View>();
for (Window window : Window.getWindows()) {
View view = View.getView(window);
boolean visible = SwingHelper.isVisibleWindow(window);
if ((view != null) && visible && (view.getApplication() == app)) {
rv.add(view);
}
}
return rv;
}
public static View getView(RootPaneContainer comp) {
return getView(comp.getRootPane());
}
public static View getView(Component comp) {
if (comp == null) {
return null;
}
JComponent root = null;
if (comp instanceof RootPaneContainer) {
root = ((RootPaneContainer) comp).getRootPane();
} else if (comp instanceof JComponent) {
root = (JComponent) comp;
}
if (root != null) {
Object marker = root.getClientProperty(VIEW_MARKER);
if (marker instanceof View) {
return (View) marker;
}
}
return getView(comp.getParent());
}
}