/*
 * Copyright (c) 2005-2009 Substance Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of Substance Kirill Grouchnikov nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.jvnet.substance.utils;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.*;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.*;

import javax.swing.*;
import javax.swing.plaf.UIResource;

import org.jvnet.lafwidget.animation.effects.GhostPaintingUtils;
import org.jvnet.lafwidget.utils.RenderingUtils;
import org.jvnet.lafwidget.utils.TrackableThread;
import org.jvnet.substance.*;
import org.jvnet.substance.api.*;
import org.jvnet.substance.api.SubstanceConstants.SubstanceWidgetType;
import org.jvnet.substance.painter.decoration.DecorationAreaType;
import org.jvnet.substance.painter.utils.BackgroundPaintingUtils;
import org.jvnet.substance.skin.SkinInfo;
import org.jvnet.substance.utils.icon.SubstanceIconFactory;
import org.jvnet.substance.utils.icon.TransitionAwareIcon;

/**
 * Title pane for <b>Substance</b> look and feel.
 * 
 * @author Kirill Grouchnikov
 */
public class SubstanceTitlePane extends JComponent {
	/**
	 * PropertyChangeListener added to the JRootPane.
	 */
	private PropertyChangeListener propertyChangeListener;

	/**
	 * JMenuBar, typically renders the system menu items.
	 */
	protected JMenuBar menuBar;

	/**
	 * Action used to close the Window.
	 */
	private Action closeAction;

	/**
	 * Action used to iconify the Frame.
	 */
	private Action iconifyAction;

	/**
	 * Action to restore the Frame size.
	 */
	private Action restoreAction;

	/**
	 * Action to restore the Frame size.
	 */
	private Action maximizeAction;

	/**
	 * Button used to maximize or restore the frame.
	 */
	protected JButton toggleButton;

	/**
	 * Button used to minimize the frame
	 */
	protected JButton minimizeButton;

	/**
	 * Button used to close the frame.
	 */
	protected JButton closeButton;

	/**
	 * Listens for changes in the state of the Window listener to update the
	 * state of the widgets.
	 */
	private WindowListener windowListener;

	/**
	 * Window we're currently in.
	 */
	protected Window window;

	/**
	 * JRootPane rendering for.
	 */
	protected JRootPane rootPane;

	/**
	 * Buffered Frame.state property. As state isn't bound, this is kept to
	 * determine when to avoid updating widgets.
	 */
	private int state;

	/**
	 * SubstanceRootPaneUI that created us.
	 */
	private SubstanceRootPaneUI rootPaneUI;

	/**
	 * The logfile name for the heap status panel. Can be <code>null</code> - in
	 * this case the {@link HeapStatusThread} will not write heap information.
	 */
	private static String heapStatusLogfileName;

	/**
	 * The heap status panel of <code>this</code> title pane.
	 */
	protected HeapStatusPanel heapStatusPanel;

	/**
	 * The heap status toggle menu item of <code>this</code> title pane.
	 */
	protected JCheckBoxMenuItem heapStatusMenuItem;

	/**
	 * Listens on changes to <code>componentOrientation</code> and
	 * {@link SubstanceLookAndFeel#WINDOW_MODIFIED} properties.
	 */
	protected PropertyChangeListener propertyListener;

	/**
	 * Client property to mark every child to be either leading or trailing. The
	 * value must be one of {@link ExtraComponentKind}.
	 * 
	 * @see #markExtraComponent(JComponent, ExtraComponentKind)
	 * @see #getTitleTextRectangle()
	 */
	protected static final String EXTRA_COMPONENT_KIND = "substancelaf.internal.titlePane.extraComponentKind";

	/**
	 * The application icon to be displayed.
	 */
	protected Image appIcon;

	/**
	 * Enumerates the types of children components.
	 * 
	 * @author Kirill Grouchnikov
	 */
	protected enum ExtraComponentKind {
		/**
		 * Leading child components (left on LTR and right on RTL).
		 */
		LEADING,

		/**
		 * Trailing child components (right on LTR and left on RTL).
		 */
		TRAILING
	}

	/**
	 * Panel that shows heap status and allows running the garbage collector.
	 * 
	 * @author Kirill Grouchnikov
	 */
	public static class HeapStatusPanel extends JPanel {
		/**
		 * The current heap size in kilobytes.
		 */
		private int currHeapSizeKB;

		/**
		 * The current used portion of heap in kilobytes.
		 */
		private int currTakenHeapSizeKB;

		/**
		 * History of used heap portion (in percents). Each value is in 0.0-1.0
		 * range.
		 */
		private LinkedList<Double> graphValues;

		/**
		 * Creates new heap status panel.
		 */
		public HeapStatusPanel() {
			this.graphValues = new LinkedList<Double>();
			HeapStatusThread.getInstance();
		}

		/**
		 * Updates the values for <code>this</code> heap status panel.
		 * 
		 * @param currHeapSizeKB
		 *            The current heap size in kilobytes.
		 * @param currTakenHeapSizeKB
		 *            The current used portion of heap in kilobytes.
		 */
		public synchronized void updateStatus(int currHeapSizeKB,
				int currTakenHeapSizeKB) {
			this.currHeapSizeKB = currHeapSizeKB;
			this.currTakenHeapSizeKB = currTakenHeapSizeKB;
			double newGraphValue = (double) currTakenHeapSizeKB
					/ (double) currHeapSizeKB;
			this.graphValues.addLast(newGraphValue);
			this.repaint();
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.swing.JComponent#paint(java.awt.Graphics)
		 */
		@Override
		public synchronized void paint(Graphics g) {
			Graphics2D graphics = (Graphics2D) g.create();

			SubstanceColorScheme scheme = SubstanceCoreUtilities.getSkin(this)
					.getMainActiveColorScheme(
							DecorationAreaType.PRIMARY_TITLE_PANE);

			graphics.setColor(scheme.getDarkColor());
			int w = this.getWidth();
			int h = this.getHeight();

			graphics.drawRect(0, 0, w - 1, h - 1);

			graphics.setColor(scheme.getExtraLightColor());
			graphics.fillRect(1, 1, w - 2, h - 2);

			while (this.graphValues.size() > (w - 2))
				this.graphValues.removeFirst();

			int xOff = w - this.graphValues.size() - 1;
			graphics.setColor(scheme.getMidColor());
			int count = 0;
			for (double value : this.graphValues) {
				int valueH = (int) (value * (h - 2));
				graphics.drawLine(xOff + count, h - 1 - valueH, xOff + count,
						h - 2);
				count++;
			}

			graphics.setFont(UIManager.getFont("Panel.font"));
			FontMetrics fm = graphics.getFontMetrics();

			StringBuffer longFormat = new StringBuffer();
			Formatter longFormatter = new Formatter(longFormat);
			longFormatter.format("%.1fMB / %.1fMB",
					this.currTakenHeapSizeKB / 1024.f,
					this.currHeapSizeKB / 1024.f);
			int strW = fm.stringWidth(longFormat.toString());
			int strH = fm.getAscent() + fm.getDescent();

			graphics.setColor(scheme.getForegroundColor());
			RenderingUtils.installDesktopHints(graphics, this);
			if (strW < (w - 5)) {
				graphics.drawString(longFormat.toString(), (w - strW) / 2,
						(h + strH) / 2 - 2);
			} else {
				String shortFormat = (this.currTakenHeapSizeKB / 1024)
						+ "MB / " + (this.currHeapSizeKB / 1024) + "MB";
				strW = fm.stringWidth(shortFormat);
				graphics.drawString(shortFormat, (w - strW) / 2,
						(h + strH) / 2 - 2);
			}

			graphics.dispose();
		}

		/**
		 * Returns the preferred width of this panel.
		 * 
		 * @return Preferred width of this panel.
		 */
		public int getPreferredWidth() {
			BufferedImage dummy = new BufferedImage(1, 1,
					BufferedImage.TYPE_INT_ARGB);
			Graphics2D g2d = dummy.createGraphics();
			RenderingUtils.installDesktopHints(g2d, this);
			g2d.setFont(UIManager.getFont("Panel.font"));
			FontMetrics fm = g2d.getFontMetrics();
			int result = fm.stringWidth("100.9MB / 200.9MB");
			g2d.dispose();
			return result;
		}
	}

	/**
	 * Thread for heap status panel.
	 */
	public static class HeapStatusThread extends TrackableThread {
		/**
		 * Current heap size in kilobytes.
		 */
		private int heapSizeKB;

		/**
		 * Current used portion of heap in kilobytes.
		 */
		private int takenHeapSizeKB;

		/**
		 * All heap status panels.
		 */
		private static Set<WeakReference<HeapStatusPanel>> panels = new HashSet<WeakReference<HeapStatusPanel>>();

		/**
		 * Single instance of <code>this</code> thread.
		 */
		private static HeapStatusThread instance;

		/**
		 * Formatter object (for logfile).
		 */
		private SimpleDateFormat format;

		/**
		 * Signifies whether a stop request has been issued on <code>this</code>
		 * thread using the {@link #requestStop()} call.
		 */
		private boolean isStopRequested;

		/**
		 * Simple constructor. Defined private for singleton.
		 * 
		 * @see #getInstance()
		 */
		private HeapStatusThread() {
			this.format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss.SSS");
			this.isStopRequested = false;
			this.setName("Substance heap status");
		}

		/**
		 * Gets singleton instance of <code>this</code> thread.
		 * 
		 * @return Singleton instance of <code>this</code> thread.
		 */
		public synchronized static HeapStatusThread getInstance() {
			if (HeapStatusThread.instance == null) {
				HeapStatusThread.instance = new HeapStatusThread();
				HeapStatusThread.instance.start();
			}
			return HeapStatusThread.instance;
		}

		/**
		 * Registers new heap status panel with <code>this</code> thread.
		 * 
		 * @param panel
		 *            Heap statuc panel.
		 */
		public static synchronized void registerPanel(HeapStatusPanel panel) {
			panels.add(new WeakReference<HeapStatusPanel>(panel));
		}

		/**
		 * Unregisters new heap status panel from <code>this</code> thread.
		 * 
		 * @param panel
		 *            Heap statuc panel.
		 */
		public static synchronized void unregisterPanel(HeapStatusPanel panel) {
			for (Iterator<WeakReference<HeapStatusPanel>> it = panels
					.iterator(); it.hasNext();) {
				WeakReference<HeapStatusPanel> ref = it.next();
				HeapStatusPanel currPanel = ref.get();
				if (panel == currPanel) {
					it.remove();
					return;
				}
			}
		}

		/**
		 * Updates the values of heap status.
		 */
		private synchronized void updateHeapCounts() {
			long heapSize = Runtime.getRuntime().totalMemory();
			long heapFreeSize = Runtime.getRuntime().freeMemory();

			this.heapSizeKB = (int) (heapSize / 1024);
			this.takenHeapSizeKB = (int) ((heapSize - heapFreeSize) / 1024);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Thread#run()
		 */
		@Override
		public void run() {
			while (!this.isStopRequested) {
				try {
					// update every 0.5 seconds
					Thread.sleep(500);
				} catch (InterruptedException ie) {
				}
				if (!SubstanceWidgetManager.getInstance().isAllowedAnywhere(
						SubstanceWidgetType.TITLE_PANE_HEAP_STATUS))
					continue;
				this.updateHeapCounts();
				for (Iterator<WeakReference<HeapStatusPanel>> it = panels
						.iterator(); it.hasNext();) {
					WeakReference<HeapStatusPanel> refPanel = it.next();
					HeapStatusPanel panel = refPanel.get();
					if (panel == null) {
						// prune
						it.remove();
						continue;
					}

					panel.updateStatus(this.heapSizeKB, this.takenHeapSizeKB);
				}
				// see if need to put info in log file
				if (SubstanceTitlePane.heapStatusLogfileName != null) {
					PrintWriter pw = null;
					try {
						pw = new PrintWriter(new FileWriter(
								SubstanceTitlePane.heapStatusLogfileName, true));
						pw.println(this.format.format(new Date()) + " "
								+ this.takenHeapSizeKB + "KB / "
								+ this.heapSizeKB + "KB");
					} catch (IOException ioe) {

					} finally {
						if (pw != null) {
							pw.close();
						}
					}
				}
			}
		}

		@Override
		protected void requestStop() {
			this.isStopRequested = true;
			HeapStatusThread.instance = null;
		}
	}

	/**
	 * Creates a new title pane.
	 * 
	 * @param root
	 *            Root pane.
	 * @param ui
	 *            Root pane UI.
	 */
	public SubstanceTitlePane(JRootPane root, SubstanceRootPaneUI ui) {
		this.rootPane = root;
		this.rootPaneUI = ui;

		this.state = -1;

		this.installSubcomponents();
		this.installDefaults();

		this.setLayout(this.createLayout());

		this.setToolTipText(this.getTitle());

		SubstanceLookAndFeel.setDecorationType(this,
				DecorationAreaType.PRIMARY_TITLE_PANE);
		this.setForeground(SubstanceColorUtilities
				.getForegroundColor(SubstanceColorSchemeUtilities
						.getColorScheme(this, ComponentState.ACTIVE)));
	}

	/**
	 * Uninstalls the necessary state.
	 */
	public void uninstall() {
		this.uninstallListeners();
		this.window = null;

		HeapStatusThread.unregisterPanel(this.heapStatusPanel);

		// Swing bug (?) - the updateComponentTree never gets to the
		// system menu (and in our case we have radio menu items with
		// rollover listeners). Fix for defect 109 - memory leak on skin
		// switch
		if ((this.menuBar != null) && (this.menuBar.getMenuCount() > 0)) {
			this.menuBar.getUI().uninstallUI(this.menuBar);
			SubstanceCoreUtilities.uninstallMenu(this.menuBar.getMenu(0));
		}

		if (this.heapStatusPanel != null) {
			for (MouseListener listener : this.heapStatusPanel
					.getMouseListeners())
				this.heapStatusPanel.removeMouseListener(listener);
			HeapStatusThread.unregisterPanel(this.heapStatusPanel);
			this.remove(this.heapStatusPanel);
		}

		if (this.menuBar != null)
			this.menuBar.removeAll();
		this.removeAll();
	}

	/**
	 * Installs the necessary listeners.
	 */
	private void installListeners() {
		if (this.window != null) {
			this.windowListener = new WindowHandler();
			this.window.addWindowListener(this.windowListener);
			this.propertyChangeListener = new PropertyChangeHandler();
			this.window.addPropertyChangeListener(this.propertyChangeListener);
		}

		// Property change listener for pulsating close button
		// when window has been marked as changed.
		// Fix for defect 109 - memory leak on skin change.
		this.propertyListener = new PropertyChangeListener() {
			public void propertyChange(final PropertyChangeEvent evt) {
				if (SubstanceLookAndFeel.WINDOW_MODIFIED.equals(evt
						.getPropertyName())) {
					syncCloseButtonTooltip();
					// if (Boolean.TRUE.equals(evt.getNewValue())) {
					// SubstanceTitlePane.this.closeButton
					// .setToolTipText(SubstanceLookAndFeel
					// .getLabelBundle().getString(
					// "SystemMenu.close")
					// + " ["
					// + SubstanceLookAndFeel
					// .getLabelBundle()
					// .getString(
					// "Tooltip.contentsNotSaved")
					// + "]");
					// } else {
					// SubstanceTitlePane.this.closeButton
					// .setToolTipText(SubstanceLookAndFeel
					// .getLabelBundle().getString(
					// "SystemMenu.close"));
					// }
					// SubstanceTitlePane.this.closeButton.repaint();
				}

				if ("componentOrientation".equals(evt.getPropertyName())) {
					SwingUtilities.invokeLater(new Runnable() {
						public void run() {
							if (SubstanceTitlePane.this.menuBar != null) {
								SubstanceTitlePane.this.menuBar
										.applyComponentOrientation((ComponentOrientation) evt
												.getNewValue());
							}
						}
					});
				}
			}
		};
		// Wire it on the frame itself and its root pane.
		this.rootPane.addPropertyChangeListener(this.propertyListener);
		if (this.getFrame() != null)
			this.getFrame().addPropertyChangeListener(this.propertyListener);
	}

	/**
	 * Uninstalls the necessary listeners.
	 */
	private void uninstallListeners() {
		if (this.window != null) {
			this.window.removeWindowListener(this.windowListener);
			this.windowListener = null;
			this.window
					.removePropertyChangeListener(this.propertyChangeListener);
			this.propertyChangeListener = null;
		}

		// Fix for defect 109 - memory leak on skin change.
		this.rootPane.removePropertyChangeListener(this.propertyListener);
		if (this.getFrame() != null)
			this.getFrame().removePropertyChangeListener(this.propertyListener);
		this.propertyListener = null;

	}

	/**
	 * Returns the <code>JRootPane</code> this was created for.
	 */
	@Override
	public JRootPane getRootPane() {
		return this.rootPane;
	}

	/**
	 * Returns the decoration style of the <code>JRootPane</code>.
	 * 
	 * @return Decoration style of the <code>JRootPane</code>.
	 */
	protected int getWindowDecorationStyle() {
		return this.getRootPane().getWindowDecorationStyle();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.awt.Component#addNotify()
	 */
	@Override
	public void addNotify() {
		super.addNotify();

		this.uninstallListeners();

		this.window = SwingUtilities.getWindowAncestor(this);
		if (this.window != null) {
			this.setActive(this.window.isActive());
			if (this.window instanceof Frame) {
				this.setState(((Frame) this.window).getExtendedState());
			} else {
				this.setState(0);
			}
			if (this.getComponentCount() == 0) {
				// fix for issue 385 - add the sub-components uninstalled
				// in the removeNotify. This happens when a decorated
				// dialog has been disposed and then reshown.
				this.installSubcomponents();
			}
			this.installListeners();
		}
		this.setToolTipText(this.getTitle());
		this.updateAppIcon();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.awt.Component#removeNotify()
	 */
	@Override
	public void removeNotify() {
		super.removeNotify();

		this.uninstall();
		this.window = null;
	}

	/**
	 * Adds any sub-Components contained in the <code>SubstanceTitlePane</code>.
	 */
	private void installSubcomponents() {
		int decorationStyle = this.getWindowDecorationStyle();
		if (decorationStyle == JRootPane.FRAME) {
			this.createActions();
			this.menuBar = this.createMenuBar();
			if (this.menuBar != null) {
				this.add(this.menuBar);
			}
			this.createButtons();
			this.add(this.minimizeButton);
			this.add(this.toggleButton);
			this.add(this.closeButton);

			this.heapStatusPanel = new HeapStatusPanel();
			this.markExtraComponent(this.heapStatusPanel,
					ExtraComponentKind.TRAILING);
			this.add(this.heapStatusPanel);
			boolean isHeapStatusPanelShowing = SubstanceWidgetManager
					.getInstance().isAllowed(rootPane,
							SubstanceWidgetType.TITLE_PANE_HEAP_STATUS);
			this.heapStatusPanel.setVisible(isHeapStatusPanelShowing);
			this.heapStatusPanel.setPreferredSize(new Dimension(80, this
					.getPreferredSize().height));
			this.heapStatusPanel.setToolTipText(SubstanceCoreUtilities
					.getResourceBundle(rootPane).getString(
							"Tooltip.heapStatusPanel"));
			this.heapStatusPanel.addMouseListener(new MouseAdapter() {
				@Override
				public void mouseClicked(MouseEvent e) {
					System.gc();
				}
			});

			HeapStatusThread.registerPanel(this.heapStatusPanel);
		} else {
			if ((decorationStyle == JRootPane.PLAIN_DIALOG)
					|| (decorationStyle == JRootPane.INFORMATION_DIALOG)
					|| (decorationStyle == JRootPane.ERROR_DIALOG)
					|| (decorationStyle == JRootPane.COLOR_CHOOSER_DIALOG)
					|| (decorationStyle == JRootPane.FILE_CHOOSER_DIALOG)
					|| (decorationStyle == JRootPane.QUESTION_DIALOG)
					|| (decorationStyle == JRootPane.WARNING_DIALOG)) {
				this.createActions();
				this.createButtons();
				this.add(this.closeButton);
			}
		}
	}

	/**
	 * Installs the fonts and necessary properties.
	 */
	private void installDefaults() {
		this.setFont(UIManager.getFont("InternalFrame.titleFont", this
				.getLocale()));
	}

	/**
	 * Returns the <code>JMenuBar</code> displaying the appropriate system menu
	 * items.
	 * 
	 * @return <code>JMenuBar</code> displaying the appropriate system menu
	 *         items.
	 */
	protected JMenuBar createMenuBar() {
		this.menuBar = new SubstanceMenuBar();
		this.menuBar.setFocusable(false);
		this.menuBar.setBorderPainted(true);
		this.menuBar.add(this.createMenu());
		this.menuBar.setOpaque(false);
		// support for RTL
		this.menuBar.applyComponentOrientation(this.rootPane
				.getComponentOrientation());

		this.markExtraComponent(this.menuBar, ExtraComponentKind.LEADING);

		return this.menuBar;
	}

	/**
	 * Create the <code>Action</code>s that get associated with the buttons and
	 * menu items.
	 */
	private void createActions() {
		this.closeAction = new CloseAction();
		if (this.getWindowDecorationStyle() == JRootPane.FRAME) {
			this.iconifyAction = new IconifyAction();
			this.restoreAction = new RestoreAction();
			this.maximizeAction = new MaximizeAction();
		}
	}

	/**
	 * Returns the <code>JMenu</code> displaying the appropriate menu items for
	 * manipulating the Frame.
	 * 
	 * @return <code>JMenu</code> displaying the appropriate menu items for
	 *         manipulating the Frame.
	 */
	private JMenu createMenu() {
		JMenu menu = new JMenu("");
		menu.setOpaque(false);
		menu.setBackground(null);
		if (this.getWindowDecorationStyle() == JRootPane.FRAME) {
			this.addMenuItems(menu);
		}
		return menu;
	}

	/**
	 * Adds the necessary <code>JMenuItem</code>s to the specified menu.
	 * 
	 * @param menu
	 *            Menu.
	 */
	private void addMenuItems(JMenu menu) {
		menu.add(this.restoreAction);

		menu.add(this.iconifyAction);

		if (Toolkit.getDefaultToolkit().isFrameStateSupported(
				Frame.MAXIMIZED_BOTH)) {
			menu.add(this.maximizeAction);
		}

		if (SubstanceCoreUtilities.toShowExtraWidgets(rootPane)) {
			menu.addSeparator();
			JMenu skinMenu = new JMenu(SubstanceCoreUtilities
					.getResourceBundle(rootPane).getString("SystemMenu.skins"));
			Map<String, SkinInfo> allSkins = SubstanceLookAndFeel.getAllSkins();
			for (Map.Entry<String, SkinInfo> skinEntry : allSkins.entrySet()) {
				final String skinClassName = skinEntry.getValue()
						.getClassName();
				JMenuItem jmiSkin = new JMenuItem(skinEntry.getKey());
				jmiSkin.addActionListener(new ActionListener() {
					public void actionPerformed(ActionEvent e) {
						SwingUtilities.invokeLater(new Runnable() {
							public void run() {
								SubstanceLookAndFeel.setSkin(skinClassName);
							}
						});
					}
				});

				skinMenu.add(jmiSkin);
			}
			menu.add(skinMenu);
		}

		menu.addSeparator();

		menu.add(this.closeAction);
	}

	/**
	 * Returns a <code>JButton</code> appropriate for placement on the
	 * TitlePane.
	 * 
	 * @return Title button.
	 */
	private JButton createTitleButton() {
		JButton button = new SubstanceTitleButton();

		button.setFocusPainted(false);
		button.setFocusable(false);
		button.setOpaque(true);

		this.markExtraComponent(button, ExtraComponentKind.TRAILING);

		return button;
	}

	/**
	 * Creates the Buttons that will be placed on the TitlePane.
	 */
	private void createButtons() {
		this.closeButton = this.createTitleButton();
		this.closeButton.setAction(this.closeAction);
		this.closeButton.setText(null);
		this.closeButton.setBorder(null);
		// this.closeButton.setToolTipText(SubstanceLookAndFeel
		// .getLabelBundle().getString(
		// "SystemMenu.close"));

		Icon closeIcon = new TransitionAwareIcon(closeButton,
				new TransitionAwareIcon.Delegate() {
					public Icon getColorSchemeIcon(SubstanceColorScheme scheme) {
						return SubstanceIconFactory
								.getTitlePaneIcon(
										SubstanceIconFactory.IconKind.CLOSE,
										scheme,
										SubstanceCoreUtilities
												.getSkin(rootPane)
												.getBackgroundColorScheme(
														DecorationAreaType.PRIMARY_TITLE_PANE));
					}
				}, "substance.titlePane.closeIcon");
		this.closeButton.setIcon(closeIcon);

		this.closeButton.setFocusable(false);
		this.closeButton.putClientProperty(SubstanceLookAndFeel.FLAT_PROPERTY,
				Boolean.TRUE);

		this.closeButton.putClientProperty(
				SubstanceButtonUI.IS_TITLE_CLOSE_BUTTON, Boolean.TRUE);

		if (this.getWindowDecorationStyle() == JRootPane.FRAME) {
			this.minimizeButton = this.createTitleButton();
			this.minimizeButton.setAction(this.iconifyAction);
			this.minimizeButton.setText(null);
			this.minimizeButton.setBorder(null);

			Icon minIcon = new TransitionAwareIcon(this.minimizeButton,
					new TransitionAwareIcon.Delegate() {
						public Icon getColorSchemeIcon(
								SubstanceColorScheme scheme) {
							return SubstanceIconFactory
									.getTitlePaneIcon(
											SubstanceIconFactory.IconKind.MINIMIZE,
											scheme,
											SubstanceCoreUtilities
													.getSkin(rootPane)
													.getBackgroundColorScheme(
															DecorationAreaType.PRIMARY_TITLE_PANE));
						}
					}, "substance.titlePane.minIcon");
			this.minimizeButton.setIcon(minIcon);

			this.minimizeButton.setFocusable(false);
			this.minimizeButton.putClientProperty(
					SubstanceLookAndFeel.FLAT_PROPERTY, Boolean.TRUE);
			this.minimizeButton.setToolTipText(SubstanceCoreUtilities
					.getResourceBundle(rootPane)
					.getString("SystemMenu.iconify"));

			this.toggleButton = this.createTitleButton();
			this.toggleButton.setAction(this.restoreAction);
			this.toggleButton.setBorder(null);
			this.toggleButton.setText(null);

			Icon maxIcon = new TransitionAwareIcon(this.toggleButton,
					new TransitionAwareIcon.Delegate() {
						public Icon getColorSchemeIcon(
								SubstanceColorScheme scheme) {
							return SubstanceIconFactory
									.getTitlePaneIcon(
											SubstanceIconFactory.IconKind.MAXIMIZE,
											scheme,
											SubstanceCoreUtilities
													.getSkin(rootPane)
													.getBackgroundColorScheme(
															DecorationAreaType.PRIMARY_TITLE_PANE));
						}
					}, "substance.titlePane.maxIcon");
			this.toggleButton.setIcon(maxIcon);

			this.toggleButton.setToolTipText(SubstanceCoreUtilities
					.getResourceBundle(rootPane).getString(
							"SystemMenu.maximize"));
			this.toggleButton.setFocusable(false);
			this.toggleButton.putClientProperty(
					SubstanceLookAndFeel.FLAT_PROPERTY, Boolean.TRUE);

		}
		syncCloseButtonTooltip();
	}

	/**
	 * Returns the <code>LayoutManager</code> that should be installed on the
	 * <code>SubstanceTitlePane</code>.
	 * 
	 * @return Layout manager.
	 */
	protected LayoutManager createLayout() {
		return new TitlePaneLayout();
	}

	/**
	 * Updates state dependant upon the Window's active state.
	 * 
	 * @param isActive
	 *            if <code>true</code>, the window is in active state.
	 */
	private void setActive(boolean isActive) {
		this.getRootPane().repaint();
	}

	/**
	 * Sets the state of the Window.
	 * 
	 * @param state
	 *            Window state.
	 */
	private void setState(int state) {
		this.setState(state, false);
	}

	/**
	 * Sets the state of the window. If <code>updateRegardless</code> is true
	 * and the state has not changed, this will update anyway.
	 * 
	 * @param state
	 *            Window state.
	 * @param updateRegardless
	 *            if <code>true</code>, the update is done in any case.
	 */
	private void setState(int state, boolean updateRegardless) {
		Window w = this.getWindow();

		if ((w != null) && (this.getWindowDecorationStyle() == JRootPane.FRAME)) {
			if ((this.state == state) && !updateRegardless) {
				return;
			}
			Frame frame = this.getFrame();

			if (frame != null) {
				final JRootPane rootPane = this.getRootPane();

				if (((state & Frame.MAXIMIZED_BOTH) != 0)
						&& ((rootPane.getBorder() == null) || (rootPane
								.getBorder() instanceof UIResource))
						&& frame.isShowing()) {
					rootPane.setBorder(null);
				} else {
					if ((state & Frame.MAXIMIZED_BOTH) == 0) {
						// This is a croak, if state becomes bound, this can
						// be nuked.
						this.rootPaneUI.installBorder(rootPane);
					}
				}
				if (frame.isResizable()) {
					if ((state & Frame.MAXIMIZED_BOTH) != 0) {
						Icon restoreIcon = new TransitionAwareIcon(
								this.toggleButton,
								new TransitionAwareIcon.Delegate() {
									public Icon getColorSchemeIcon(
											SubstanceColorScheme scheme) {
										return SubstanceIconFactory
												.getTitlePaneIcon(
														SubstanceIconFactory.IconKind.RESTORE,
														scheme,
														SubstanceCoreUtilities
																.getSkin(
																		rootPane)
																.getBackgroundColorScheme(
																		DecorationAreaType.PRIMARY_TITLE_PANE));
									}
								}, "substance.titlePane.restoreIcon");
						this
								.updateToggleButton(this.restoreAction,
										restoreIcon);
						this.toggleButton.setToolTipText(SubstanceCoreUtilities
								.getResourceBundle(rootPane).getString(
										"SystemMenu.restore"));
						this.maximizeAction.setEnabled(false);
						this.restoreAction.setEnabled(true);
					} else {
						Icon maxIcon = new TransitionAwareIcon(
								this.toggleButton,
								new TransitionAwareIcon.Delegate() {
									public Icon getColorSchemeIcon(
											SubstanceColorScheme scheme) {
										return SubstanceIconFactory
												.getTitlePaneIcon(
														SubstanceIconFactory.IconKind.MAXIMIZE,
														scheme,
														SubstanceCoreUtilities
																.getSkin(
																		rootPane)
																.getBackgroundColorScheme(
																		DecorationAreaType.PRIMARY_TITLE_PANE));
									}
								}, "substance.titlePane.maxIcon");
						this.updateToggleButton(this.maximizeAction, maxIcon);
						this.toggleButton.setToolTipText(SubstanceCoreUtilities
								.getResourceBundle(rootPane).getString(
										"SystemMenu.maximize"));
						this.maximizeAction.setEnabled(true);
						this.restoreAction.setEnabled(false);
					}
					if ((this.toggleButton.getParent() == null)
							|| (this.minimizeButton.getParent() == null)) {
						this.add(this.toggleButton);
						this.add(this.minimizeButton);
						this.revalidate();
						this.repaint();
					}
					this.toggleButton.setText(null);
				} else {
					this.maximizeAction.setEnabled(false);
					this.restoreAction.setEnabled(false);
					if (this.toggleButton.getParent() != null) {
						this.remove(this.toggleButton);
						this.revalidate();
						this.repaint();
					}
				}
			} else {
				// Not contained in a Frame
				this.maximizeAction.setEnabled(false);
				this.restoreAction.setEnabled(false);
				this.iconifyAction.setEnabled(false);
				this.remove(this.toggleButton);
				this.remove(this.minimizeButton);
				this.revalidate();
				this.repaint();
			}
			this.closeAction.setEnabled(true);
			this.state = state;
		}
	}

	/**
	 * Updates the toggle button to contain the Icon <code>icon</code>, and
	 * Action <code>action</code>.
	 * 
	 * @param action
	 *            Action.
	 * @param icon
	 *            Icon.
	 */
	private void updateToggleButton(Action action, Icon icon) {
		this.toggleButton.setAction(action);
		this.toggleButton.setIcon(icon);
		this.toggleButton.setText(null);
	}

	/**
	 * Returns the Frame rendering in. This will return null if the
	 * <code>JRootPane</code> is not contained in a <code>Frame</code>.
	 * 
	 * @return Frame.
	 */
	private Frame getFrame() {
		Window window = this.getWindow();

		if (window instanceof Frame) {
			return (Frame) window;
		}
		return null;
	}

	/**
	 * Returns the <code>Window</code> the <code>JRootPane</code> is contained
	 * in. This will return null if there is no parent ancestor of the
	 * <code>JRootPane</code>.
	 * 
	 * @return Window.
	 */
	private Window getWindow() {
		return this.window;
	}

	/**
	 * Returns the String to display as the title.
	 * 
	 * @return Display title.
	 */
	private String getTitle() {
		Window w = this.getWindow();

		if (w instanceof Frame) {
			return ((Frame) w).getTitle();
		}
		if (w instanceof Dialog) {
			return ((Dialog) w).getTitle();
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
	 */
	@Override
	public void paintComponent(Graphics g) {
		// long start = System.nanoTime();
		// As state isn't bound, we need a convenience place to check
		// if it has changed. Changing the state typically changes the
		if (this.getFrame() != null) {
			this.setState(this.getFrame().getExtendedState());
		}
		final JRootPane rootPane = this.getRootPane();
		Window window = this.getWindow();
		boolean leftToRight = (window == null) ? rootPane
				.getComponentOrientation().isLeftToRight() : window
				.getComponentOrientation().isLeftToRight();
		int width = this.getWidth();
		int height = this.getHeight();

		SubstanceSkin skin = SubstanceCoreUtilities.getSkin(rootPane);
		if (skin == null) {
			SubstanceCoreUtilities
					.traceSubstanceApiUsage(this,
							"Substance delegate used when Substance is not the current LAF");
		}
		SubstanceColorScheme scheme = skin
				.getMainDefaultColorScheme(DecorationAreaType.PRIMARY_TITLE_PANE);

		int xOffset = 0;
		String theTitle = this.getTitle();

		if (theTitle != null) {
			Rectangle titleTextRect = this.getTitleTextRectangle();
			FontMetrics fm = rootPane.getFontMetrics(g.getFont());
			int titleWidth = titleTextRect.width - 20;
			String clippedTitle = SubstanceCoreUtilities.clipString(fm,
					titleWidth, theTitle);
			// show tooltip with full title only if necessary
			if (theTitle.equals(clippedTitle)) {
				this.setToolTipText(null);
			} else {
				this.setToolTipText(theTitle);
			}
			theTitle = clippedTitle;
			if (leftToRight)
				xOffset = titleTextRect.x;
			else
				xOffset = titleTextRect.x + titleTextRect.width
						- fm.stringWidth(theTitle);
		}

		Graphics2D graphics = (Graphics2D) g.create();
		Font font = SubstanceLookAndFeel.getFontPolicy().getFontSet(
				"Substance", null).getWindowTitleFont();
		graphics.setFont(font);

		BackgroundPaintingUtils
				.update(graphics, SubstanceTitlePane.this, false);
		// DecorationPainterUtils.paintDecorationBackground(graphics,
		// SubstanceTitlePane.this, false);

		// draw the title (if needed)
		if (theTitle != null) {
			FontMetrics fm = rootPane.getFontMetrics(graphics.getFont());
			int yOffset = ((height - fm.getHeight()) / 2) + fm.getAscent();

			SubstanceTextUtilities.paintTextWithDropShadow(this, graphics,
					SubstanceColorUtilities.getForegroundColor(scheme),
					theTitle, width, height, xOffset, yOffset);
		}

		GhostPaintingUtils.paintGhostImages(this, graphics);

		// long end = System.nanoTime();
		// System.out.println(end - start);
		graphics.dispose();
	}

	/**
	 * Computes the rectangle of the title text. This method looks at all the
	 * children components of the title pane, grouping them by leading and
	 * trailing (based on {@link #EXTRA_COMPONENT_KIND} client property). The
	 * title text rectangle is the space between the leading group and the
	 * trailing group.
	 * 
	 * @return Rectangle of the title text.
	 * @throws IllegalStateException
	 *             If at least one child component of this title pane is not
	 *             marked with the {@link #EXTRA_COMPONENT_KIND} client
	 *             property.
	 * @see #markExtraComponent(JComponent, ExtraComponentKind)
	 * @see #EXTRA_COMPONENT_KIND
	 */
	protected Rectangle getTitleTextRectangle() {
		JRootPane rootPane = this.getRootPane();
		Window window = this.getWindow();
		boolean leftToRight = (window == null) ? rootPane
				.getComponentOrientation().isLeftToRight() : window
				.getComponentOrientation().isLeftToRight();

		if (leftToRight) {
			int maxLeadingX = 0;
			int minTrailingX = this.getWidth();

			for (int i = 0; i < this.getComponentCount(); i++) {
				Component child = this.getComponent(i);
				if (!child.isVisible())
					continue;
				if (child instanceof JComponent) {
					ExtraComponentKind kind = (ExtraComponentKind) ((JComponent) child)
							.getClientProperty(EXTRA_COMPONENT_KIND);
					if (kind == null) {
						throw new IllegalStateException("Title pane child "
								+ child.getClass().getName()
								+ " is not marked as leading or trailing");
					}
					if (kind == ExtraComponentKind.LEADING) {
						int cx = child.getX() + child.getWidth();
						if (cx > maxLeadingX)
							maxLeadingX = cx;
					} else {
						int cx = child.getX();
						if (cx < minTrailingX)
							minTrailingX = cx;
					}
				}
			}

			int start = maxLeadingX + 10;
			int end = minTrailingX - 5;
			return new Rectangle(start, 0, end - start, this.getHeight());
		} else {
			int minLeadingX = this.getWidth();
			int maxTrailingX = 0;

			for (int i = 0; i < this.getComponentCount(); i++) {
				Component child = this.getComponent(i);
				if (!child.isVisible())
					continue;
				if (child instanceof JComponent) {
					ExtraComponentKind kind = (ExtraComponentKind) ((JComponent) child)
							.getClientProperty(EXTRA_COMPONENT_KIND);
					if (kind == null) {
						throw new IllegalStateException("Title pane child "
								+ child.getClass().getName()
								+ " is not marked as leading or trailing");
					}
					if (kind == ExtraComponentKind.LEADING) {
						int cx = child.getX();
						if (cx < minLeadingX)
							minLeadingX = cx;
					} else {
						int cx = child.getX() + child.getWidth();
						if (cx > maxTrailingX)
							maxTrailingX = cx;
					}
				}
			}

			int start = maxTrailingX + 5;
			int end = minLeadingX - 10;
			return new Rectangle(start, 0, end - start, this.getHeight());
		}
	}

	/**
	 * Actions used to <code>close</code> the <code>Window</code>.
	 */
	private class CloseAction extends AbstractAction {
		/**
		 * Creates a new close action.
		 */
		public CloseAction() {
			super(SubstanceCoreUtilities.getResourceBundle(rootPane).getString(
					"SystemMenu.close"), SubstanceImageCreator.getCloseIcon(
					SubstanceCoreUtilities.getSkin(rootPane)
							.getMainActiveColorScheme(
									DecorationAreaType.PRIMARY_TITLE_PANE),
					SubstanceCoreUtilities.getSkin(rootPane)
							.getBackgroundColorScheme(
									DecorationAreaType.PRIMARY_TITLE_PANE)));
		}

		public void actionPerformed(ActionEvent e) {
			Window window = SubstanceTitlePane.this.getWindow();

			if (window != null) {
				window.dispatchEvent(new WindowEvent(window,
						WindowEvent.WINDOW_CLOSING));
			}
		}
	}

	/**
	 * Actions used to <code>iconfiy</code> the <code>Frame</code>.
	 */
	private class IconifyAction extends AbstractAction {
		/**
		 * Creates a new iconify action.
		 */
		public IconifyAction() {
			super(
					SubstanceCoreUtilities.getResourceBundle(rootPane)
							.getString("SystemMenu.iconify"),
					SubstanceImageCreator
							.getMinimizeIcon(
									SubstanceCoreUtilities
											.getSkin(rootPane)
											.getMainActiveColorScheme(
													DecorationAreaType.PRIMARY_TITLE_PANE),
									SubstanceCoreUtilities
											.getSkin(rootPane)
											.getBackgroundColorScheme(
													DecorationAreaType.PRIMARY_TITLE_PANE)));
		}

		public void actionPerformed(ActionEvent e) {
			Frame frame = SubstanceTitlePane.this.getFrame();
			if (frame != null) {
				frame.setExtendedState(SubstanceTitlePane.this.state
						| Frame.ICONIFIED);
			}
		}
	}

	/**
	 * Actions used to <code>restore</code> the <code>Frame</code>.
	 */
	private class RestoreAction extends AbstractAction {
		/**
		 * Creates a new restore action.
		 */
		public RestoreAction() {
			super(
					SubstanceCoreUtilities.getResourceBundle(rootPane)
							.getString("SystemMenu.restore"),
					SubstanceImageCreator
							.getRestoreIcon(
									SubstanceCoreUtilities
											.getSkin(rootPane)
											.getMainActiveColorScheme(
													DecorationAreaType.PRIMARY_TITLE_PANE),
									SubstanceCoreUtilities
											.getSkin(rootPane)
											.getBackgroundColorScheme(
													DecorationAreaType.PRIMARY_TITLE_PANE)));
		}

		public void actionPerformed(ActionEvent e) {
			Frame frame = SubstanceTitlePane.this.getFrame();

			if (frame == null) {
				return;
			}

			if ((SubstanceTitlePane.this.state & Frame.ICONIFIED) != 0) {
				frame.setExtendedState(SubstanceTitlePane.this.state
						& ~Frame.ICONIFIED);
			} else {
				frame.setExtendedState(SubstanceTitlePane.this.state
						& ~Frame.MAXIMIZED_BOTH);
			}
		}
	}

	/**
	 * Actions used to <code>restore</code> the <code>Frame</code>.
	 */
	private class MaximizeAction extends AbstractAction {
		/**
		 * Creates a new maximize action.
		 */
		public MaximizeAction() {
			super(
					SubstanceCoreUtilities.getResourceBundle(rootPane)
							.getString("SystemMenu.maximize"),
					SubstanceImageCreator
							.getMaximizeIcon(
									SubstanceCoreUtilities
											.getSkin(rootPane)
											.getMainActiveColorScheme(
													DecorationAreaType.PRIMARY_TITLE_PANE),
									SubstanceCoreUtilities
											.getSkin(rootPane)
											.getMainDefaultColorScheme(
													DecorationAreaType.PRIMARY_TITLE_PANE)));
		}

		public void actionPerformed(ActionEvent e) {
			Frame frame = SubstanceTitlePane.this.getFrame();
			if (frame != null) {
				if (frame instanceof JFrame) {
					SubstanceRootPaneUI rpUI = (SubstanceRootPaneUI) ((JFrame) frame)
							.getRootPane().getUI();
					rpUI.setMaximized();
				}
				frame.setExtendedState(SubstanceTitlePane.this.state
						| Frame.MAXIMIZED_BOTH);
			}
		}
	}

	/**
	 * Class responsible for drawing the system menu. Looks up the image to draw
	 * from the Frame associated with the <code>JRootPane</code>.
	 */
	public class SubstanceMenuBar extends JMenuBar {
		@Override
		public void paint(Graphics g) {
			if (appIcon != null) {
				g.drawImage(appIcon, 0, 0, null);
			} else {
				Icon icon = UIManager.getIcon("InternalFrame.icon");
				if (icon != null) {
					icon.paintIcon(this, g, 0, 0);
				}
			}
		}

		@Override
		public Dimension getMinimumSize() {
			return this.getPreferredSize();
		}

		@Override
		public Dimension getPreferredSize() {
			Dimension size = super.getPreferredSize();

			int iSize = SubstanceSizeUtils.getTitlePaneIconSize();
			return new Dimension(Math.max(iSize, size.width), Math.max(
					size.height, iSize));
		}
	}

	/**
	 * Layout manager for the title pane.
	 * 
	 * @author Kirill Graphics
	 */
	protected class TitlePaneLayout implements LayoutManager {
		/*
		 * (non-Javadoc)
		 * 
		 * @see java.awt.LayoutManager#addLayoutComponent(java.lang.String,
		 * java.awt.Component)
		 */
		public void addLayoutComponent(String name, Component c) {
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.awt.LayoutManager#removeLayoutComponent(java.awt.Component)
		 */
		public void removeLayoutComponent(Component c) {
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.awt.LayoutManager#preferredLayoutSize(java.awt.Container)
		 */
		public Dimension preferredLayoutSize(Container c) {
			int height = this.computeHeight();
			return new Dimension(height, height);
		}

		/**
		 * Computes title pane height.
		 * 
		 * @return Title pane height.
		 */
		private int computeHeight() {
			FontMetrics fm = SubstanceTitlePane.this.rootPane
					.getFontMetrics(SubstanceTitlePane.this.getFont());
			int fontHeight = fm.getHeight();
			fontHeight += 7;
			int iconHeight = 0;
			if (SubstanceTitlePane.this.getWindowDecorationStyle() == JRootPane.FRAME) {
				iconHeight = SubstanceSizeUtils.getTitlePaneIconSize();
			}

			int finalHeight = Math.max(fontHeight, iconHeight);
			return finalHeight;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.awt.LayoutManager#minimumLayoutSize(java.awt.Container)
		 */
		public Dimension minimumLayoutSize(Container c) {
			return this.preferredLayoutSize(c);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.awt.LayoutManager#layoutContainer(java.awt.Container)
		 */
		public void layoutContainer(Container c) {
			boolean leftToRight = (SubstanceTitlePane.this.window == null) ? SubstanceTitlePane.this
					.getRootPane().getComponentOrientation().isLeftToRight()
					: SubstanceTitlePane.this.window.getComponentOrientation()
							.isLeftToRight();

			int w = SubstanceTitlePane.this.getWidth();
			int x;
			int y = 3;
			int spacing;
			int buttonHeight;
			int buttonWidth;

			if ((SubstanceTitlePane.this.closeButton != null)
					&& (SubstanceTitlePane.this.closeButton.getIcon() != null)) {
				buttonHeight = SubstanceTitlePane.this.closeButton.getIcon()
						.getIconHeight();
				buttonWidth = SubstanceTitlePane.this.closeButton.getIcon()
						.getIconWidth();
			} else {
				buttonHeight = SubstanceSizeUtils.getTitlePaneIconSize();
				buttonWidth = SubstanceSizeUtils.getTitlePaneIconSize();
			}

			y = (getHeight() - buttonHeight) / 2;

			// assumes all buttons have the same dimensions
			// these dimensions include the borders

			x = leftToRight ? w : 0;

			spacing = 5;
			x = leftToRight ? spacing : w - buttonWidth - spacing;
			if (SubstanceTitlePane.this.menuBar != null) {
				SubstanceTitlePane.this.menuBar.setBounds(x, y, buttonWidth,
						buttonHeight);
				// System.out.println(menuBar.getBounds());
			}

			x = leftToRight ? w : 0;
			spacing = 3;
			x += leftToRight ? -spacing - buttonWidth : spacing;
			if (SubstanceTitlePane.this.closeButton != null) {
				SubstanceTitlePane.this.closeButton.setBounds(x, y,
						buttonWidth, buttonHeight);
			}

			if (!leftToRight)
				x += buttonWidth;

			if (SubstanceTitlePane.this.getWindowDecorationStyle() == JRootPane.FRAME) {
				if (Toolkit.getDefaultToolkit().isFrameStateSupported(
						Frame.MAXIMIZED_BOTH)) {
					if (SubstanceTitlePane.this.toggleButton.getParent() != null) {
						spacing = 10;
						x += leftToRight ? -spacing - buttonWidth : spacing;
						SubstanceTitlePane.this.toggleButton.setBounds(x, y,
								buttonWidth, buttonHeight);
						if (!leftToRight) {
							x += buttonWidth;
						}
					}
				}

				if ((SubstanceTitlePane.this.minimizeButton != null)
						&& (SubstanceTitlePane.this.minimizeButton.getParent() != null)) {
					spacing = 2;
					x += leftToRight ? -spacing - buttonWidth : spacing;
					SubstanceTitlePane.this.minimizeButton.setBounds(x, y,
							buttonWidth, buttonHeight);
					if (!leftToRight) {
						x += buttonWidth;
					}
				}

				if ((SubstanceTitlePane.this.heapStatusPanel != null)
						&& SubstanceTitlePane.this.heapStatusPanel.isVisible()) {
					spacing = 5;
					x += leftToRight ? (-spacing - SubstanceTitlePane.this.heapStatusPanel
							.getPreferredWidth())
							: spacing;
					SubstanceTitlePane.this.heapStatusPanel.setBounds(x, 1,
							SubstanceTitlePane.this.heapStatusPanel
									.getPreferredWidth(),
							SubstanceTitlePane.this.getHeight() - 3);
				}
			}
			// buttonsWidth = leftToRight ? w - x : x;
		}

	}

	/**
	 * PropertyChangeListener installed on the Window. Updates the necessary
	 * state as the state of the Window changes.
	 */
	private class PropertyChangeHandler implements PropertyChangeListener {
		public void propertyChange(PropertyChangeEvent pce) {
			String name = pce.getPropertyName();

			// Frame.state isn't currently bound.
			if ("resizable".equals(name) || "state".equals(name)) {
				Frame frame = SubstanceTitlePane.this.getFrame();

				if (frame != null) {
					SubstanceTitlePane.this.setState(frame.getExtendedState(),
							true);
				}
				if ("resizable".equals(name)) {
					SubstanceTitlePane.this.getRootPane().repaint();
				}
			} else {
				if ("title".equals(name)) {
					SubstanceTitlePane.this.repaint();
					SubstanceTitlePane.this.setToolTipText((String) pce
							.getNewValue());
				} else if ("componentOrientation" == name) {
					revalidate();
					repaint();
				} else if ("iconImage" == name) {
					updateAppIcon();
					revalidate();
					repaint();
				}
			}
		}
	}

	/**
	 * WindowListener installed on the Window, updates the state as necessary.
	 */
	private class WindowHandler extends WindowAdapter {
		@Override
		public void windowActivated(WindowEvent ev) {
			SubstanceTitlePane.this.setActive(true);
		}

		@Override
		public void windowDeactivated(WindowEvent ev) {
			SubstanceTitlePane.this.setActive(false);
		}
	}

	/**
	 * Sets location for heap status logfile. Relevant if
	 * {@link #setCanHaveHeapStatusPanel(boolean)} was called with
	 * <code>true</code>.
	 * 
	 * @param heapStatusLogfileName
	 *            Logfile for the heap status panel.
	 */
	public static void setHeapStatusLogfileName(String heapStatusLogfileName) {
		SubstanceTitlePane.heapStatusLogfileName = heapStatusLogfileName;
	}

	/**
	 * Synchronizes the tooltip of the close button.
	 */
	protected void syncCloseButtonTooltip() {
		if (SubstanceCoreUtilities.isRootPaneModified(this.getRootPane())) {
			this.closeButton.setToolTipText(SubstanceCoreUtilities
					.getResourceBundle(rootPane).getString("SystemMenu.close")
					+ " ["
					+ SubstanceCoreUtilities.getResourceBundle(rootPane)
							.getString("Tooltip.contentsNotSaved") + "]");
		} else {
			this.closeButton.setToolTipText(SubstanceCoreUtilities
					.getResourceBundle(rootPane).getString("SystemMenu.close"));
		}
		this.closeButton.repaint();
	}

	/**
	 * Marks the specified child component with the specified extra component
	 * kind.
	 * 
	 * @param comp
	 *            Child component.
	 * @param kind
	 *            Extra kind.
	 * @see #getTitleTextRectangle()
	 * @see #EXTRA_COMPONENT_KIND
	 */
	protected void markExtraComponent(JComponent comp, ExtraComponentKind kind) {
		comp.putClientProperty(EXTRA_COMPONENT_KIND, kind);
	}

	/**
	 * Updates the application icon.
	 */
	private void updateAppIcon() {
		Window window = getWindow();
		if (window == null) {
			this.appIcon = null;
			return;
		}
		java.util.List<Image> icons = window.getIconImages();

		if (icons.size() == 0) {
			this.appIcon = null;
		} else {
			int prefSize = SubstanceSizeUtils.getTitlePaneIconSize();
			this.appIcon = SubstanceCoreUtilities.getScaledIconImage(icons,
					prefSize, prefSize);
		}
	}

}
