/**
 * This software is released as part of the Pumpernickel project.
 * 
 * All com.pump resources in the Pumpernickel project are distributed under the
 * MIT License:
 * https://raw.githubusercontent.com/mickleness/pumpernickel/master/License.txt
 * 
 * More information about the Pumpernickel project is available here:
 * https://mickleness.github.io/pumpernickel/
 */
package com.pump.showcase;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.List;

import javax.swing.AbstractButton;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JToggleButton;
import javax.swing.SpinnerNumberModel;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.MouseInputAdapter;
import javax.swing.plaf.PanelUI;
import javax.swing.plaf.basic.BasicButtonUI;
import javax.swing.plaf.basic.BasicPanelUI;

import com.pump.blog.Blurb;
import com.pump.blog.ResourceSample;
import com.pump.icon.GlyphIcon;
import com.pump.plaf.DecoratedPanelUI;
import com.pump.plaf.PlafPaintUtils;
import com.pump.plaf.TexturePaintPanelUI;
import com.pump.swing.PartialLineBorder;
import com.pump.util.ObservableProperties.BoundsChecker;
import com.pump.util.ObservableProperties.Key;
import com.pump.util.ObservableProperties.NumberBoundsChecker;

/** A demo application/applet for the {@link DecoratedPanelUI}.
 *
 * 
 *
 * 
 * <!-- ======== START OF AUTOGENERATED SAMPLES ======== -->
 * <p><img src="https://raw.githubusercontent.com/mickleness/pumpernickel/master/pump-release/resources/samples/DecoratedPanelUIDemo/sample.png" alt="new&#160;com.bric.plaf.DecoratedPanelUIDemo()">
 * <!-- ======== END OF AUTOGENERATED SAMPLES ======== -->
 */
@ResourceSample( sample="new com.bric.plaf.DecoratedPanelUIDemo()" )
public class DecoratedPanelUIDemo extends JPanel {
	private static final long serialVersionUID = 1L;
	
	/** The panel the DecoratedPanelUI is applied to. */
	JPanel panel = new JPanel(new GridBagLayout());
	
	/** The current DecoreatedPanelUI. */
	DecoratedPanelUI ui = new DecoratedPanelUI();
	
	/** The panel the contains the <code>panel</code> field, with generous
	 * padding and a checkerboard background.
	 */
	JPanel panelContainer = new JPanel(new GridBagLayout());
	
	/** The inspector of controls on the left side of the window. */
	JPanel inspector = new JPanel(new GridBagLayout());
	
	/** The scrollpane containing the inspector. */
	JScrollPane inspectorScrollPane = new JScrollPane(inspector);

	AbstractButton plasticButton = createButton("Plastic", new Runnable() {
		public void run() {
			setUI( DecoratedPanelUI.createPlasticUI() );
		}
	});
	AbstractButton metalButton = createButton("Brushed Metal", new Runnable() {
		public void run() {
			setUI( DecoratedPanelUI.createBrushedMetalUI() );
		}
	});
	AbstractButton recessedButton = createButton("Dark Recessed", new Runnable() {
		public void run() {
			setUI( DecoratedPanelUI.createDarkRecessedUI() );
		}
	});
	AbstractButton roundedButton = createButton("Minimal Rounded", new Runnable() {
		public void run() {
			setUI( DecoratedPanelUI.createMinimalRoundedUI() );
		}
	});
	AbstractButton shadowButton = createButton("Subtle Shadow", new Runnable() {
		public void run() {
			setUI( DecoratedPanelUI.createSubtleShadowUI() );
		}
	});
	AbstractButton scribbleButton = createButton("Subtle Scribble", new Runnable() {
		public void run() {
			setUI( DecoratedPanelUI.createSubtleScribbleUI() );
		}
	});

	public DecoratedPanelUIDemo() {
		setLayout(new GridBagLayout());
		GridBagConstraints c = new GridBagConstraints();
		c.gridx = 0; c.gridy = 0; c.weightx = 0; c.weighty = 1;
		c.fill = GridBagConstraints.BOTH;
		add(inspectorScrollPane, c);
		c.gridx++; c.weightx = 1; c.fill = GridBagConstraints.BOTH;
		add( panelContainer, c );
		
		panelContainer.setUI(new TexturePaintPanelUI(PlafPaintUtils.getCheckerBoard(16, Color.white, new Color(0xF8F8F8))));
		
		
		//the corners are handled with special controls:
		final JToggleButton topLeft = new JToggleButton();
		final JToggleButton topRight = new JToggleButton();
		final JToggleButton bottomRight = new JToggleButton();
		final JToggleButton bottomLeft = new JToggleButton();
		JPanel cornerButtonPanel = new JPanel(new GridBagLayout());
		final JSlider cornerSizeSlider = new JSlider(0,100);
		final JSlider cornerCurvatureSlider = new JSlider(0,100);

		//most of the other controls are magically automated
		addHeader(inspector, "Presets:", c.gridy++);
		addInspectorRow(inspector, createButton(shadowButton, roundedButton), c.gridy++);
		addInspectorRow(inspector, createButton(plasticButton, recessedButton), c.gridy++);
		addInspectorRow(inspector, createButton(metalButton, scribbleButton), c.gridy++);
		addHeader(inspector, "Body:", c.gridy++);
		addColor(inspector, "Top Color:", DecoratedPanelUI.COLOR_TOP, c.gridy++);
		addColor(inspector, "Bottom Color:", DecoratedPanelUI.COLOR_BOTTOM, c.gridy++);
		addHeader(inspector, "Corners:", c.gridy++);
		addInspectorRow(inspector, new JLabel("Selected Corners:"), cornerButtonPanel, c.gridy++);
		addInspectorRow(inspector, new JLabel("Size:"), cornerSizeSlider, c.gridy++);
		addInspectorRow(inspector, new JLabel("Curvature:"), cornerCurvatureSlider, c.gridy++);
		addHeader(inspector, "Bevel:", c.gridy++);
		addSpinner(inspector, "Highlight Layers:", DecoratedPanelUI.Border.BEVEL_HIGHLIGHT_LAYER_COUNT, c.gridy++);
		addSlider(inspector, "Highlight Alpha:", DecoratedPanelUI.Border.BEVEL_HIGHLIGHT_MAX_BLEND, c.gridy++);
		addSpinner(inspector, "Shadow Layers:", DecoratedPanelUI.Border.BEVEL_SHADOW_LAYER_COUNT, c.gridy++);
		addSlider(inspector, "Shadow Alpha:", DecoratedPanelUI.Border.BEVEL_SHADOW_MAX_BLEND, c.gridy++);
		addSlider(inspector, "Angle:", DecoratedPanelUI.Border.BEVEL_SHADOW_THETA, 0, (float)(2*Math.PI), c.gridy++);
		addHeader(inspector, "Shadow:", c.gridy++);
		addSlider(inspector, "Opacity:", DecoratedPanelUI.Border.DROP_SHADOW_ALPHA, c.gridy++);
		addSpinner(inspector, "Layers:", DecoratedPanelUI.Border.DROP_SHADOW_LAYER_COUNT, c.gridy++);
		addHeader(inspector, "Border:", c.gridy++);
		addColor(inspector, "Color:", DecoratedPanelUI.Border.BORDER_PAINT, c.gridy++);
		addSlider(inspector, "Width:", DecoratedPanelUI.Border.STROKE_WIDTH, c.gridy++);
		addSlider(inspector, "Scribble:", DecoratedPanelUI.Border.SCRIBBLE_SIZE, c.gridy++);
		addHeader(inspector, "Glaze:", c.gridy++);
		addSlider(inspector, "Level:", DecoratedPanelUI.GLAZE_LEVEL, c.gridy++);
		addSlider(inspector, "Opacity:", DecoratedPanelUI.GLAZE_OPACITY, c.gridy++);
		
		c = new GridBagConstraints();
		c.gridx = 0; c.gridy = 0; c.weightx = 1; c.weighty = 1;
		cornerButtonPanel.add(topLeft, c);
		c.gridx++;
		cornerButtonPanel.add(topRight, c);
		c.gridx = 0; c.gridy++;
		cornerButtonPanel.add(bottomLeft, c);
		c.gridx++;
		cornerButtonPanel.add(bottomRight, c);

		installIcon(topLeft, '\u259B');
		installIcon(topRight, '\u259C');
		installIcon(bottomRight, '\u259F');
		installIcon(bottomLeft, '\u2599');

		ChangeListener cornerSliderListener = new ChangeListener() {
			
			private void install(Key<Float> key,JSlider slider) {
				float f = slider.getValue() - slider.getMinimum();
				float range = slider.getMaximum() - slider.getMinimum();
				f = f/range;
				
				BoundsChecker<?> b = key.getBoundsChecker();
				NumberBoundsChecker n = (NumberBoundsChecker)b;
				float targetRange = n.getMax().floatValue() - n.getMin().floatValue();
				f = f*targetRange;
				ui.getBorder().setProperty(key, f);
			}
			
			public void stateChanged(ChangeEvent e) {
				if(refreshing>0)
					return;
				if(e.getSource()==cornerSizeSlider) {
					if(bottomLeft.isSelected())
						install(DecoratedPanelUI.Border.CORNER_SIZE_LOWER_LEFT, cornerSizeSlider);
					if(bottomRight.isSelected())
						install(DecoratedPanelUI.Border.CORNER_SIZE_LOWER_RIGHT, cornerSizeSlider);
					if(topLeft.isSelected())
						install(DecoratedPanelUI.Border.CORNER_SIZE_UPPER_LEFT, cornerSizeSlider);
					if(topRight.isSelected())
						install(DecoratedPanelUI.Border.CORNER_SIZE_UPPER_RIGHT, cornerSizeSlider);
				} else if(e.getSource()==cornerCurvatureSlider) {
					if(bottomLeft.isSelected())
						install(DecoratedPanelUI.Border.CURVE_LOWER_LEFT, cornerCurvatureSlider);
					if(bottomRight.isSelected())
						install(DecoratedPanelUI.Border.CURVE_LOWER_RIGHT, cornerCurvatureSlider);
					if(topLeft.isSelected())
						install(DecoratedPanelUI.Border.CURVE_UPPER_LEFT, cornerCurvatureSlider);
					if(topRight.isSelected())
						install(DecoratedPanelUI.Border.CURVE_UPPER_RIGHT, cornerCurvatureSlider);
				}
			}
		};
		refreshRunnables.add(new Runnable() {
			public void run() {
				float curve = refresh(DecoratedPanelUI.Border.CURVE_UPPER_LEFT, cornerCurvatureSlider);
				float size = refresh(DecoratedPanelUI.Border.CORNER_SIZE_LOWER_LEFT, cornerSizeSlider);
				
				topLeft.setSelected( curve==ui.getBorder().getProperty(DecoratedPanelUI.Border.CURVE_UPPER_LEFT) &&
						size==ui.getBorder().getProperty(DecoratedPanelUI.Border.CORNER_SIZE_UPPER_LEFT) );
				topRight.setSelected( curve==ui.getBorder().getProperty(DecoratedPanelUI.Border.CURVE_UPPER_RIGHT) &&
						size==ui.getBorder().getProperty(DecoratedPanelUI.Border.CORNER_SIZE_UPPER_RIGHT) );
				bottomLeft.setSelected( curve==ui.getBorder().getProperty(DecoratedPanelUI.Border.CURVE_LOWER_LEFT) &&
						size==ui.getBorder().getProperty(DecoratedPanelUI.Border.CORNER_SIZE_LOWER_LEFT) );
				bottomRight.setSelected( curve==ui.getBorder().getProperty(DecoratedPanelUI.Border.CURVE_LOWER_RIGHT) &&
						size==ui.getBorder().getProperty(DecoratedPanelUI.Border.CORNER_SIZE_LOWER_RIGHT) );
			}
			
			private float refresh(Key<Float> key,JSlider slider) {
				BoundsChecker<?> b = key.getBoundsChecker();
				NumberBoundsChecker n = (NumberBoundsChecker)b;
				float f = ui.getBorder().getProperty(key) - n.getMin().floatValue();
				float range = n.getMax().floatValue() - n.getMin().floatValue();
				
				f = f/range;
				float targetRange = slider.getMaximum() - slider.getMinimum();
				f = f*targetRange + slider.getMinimum();
				slider.setValue( (int)(f + .5f));
				
				return ui.getBorder().getProperty(key);
			}
		});
		cornerSizeSlider.addChangeListener(cornerSliderListener);		
		cornerCurvatureSlider.addChangeListener(cornerSliderListener);
		
		ui.setTopColor(Color.lightGray);
		ui.setBottomColor(Color.gray);
		panel.setBackground(Color.white);
		panel.setOpaque(false);
		panelContainer.setOpaque(true);
		panelContainer.setBackground(Color.white);
		panelContainer.setBorder(new EmptyBorder(20, 20, 20, 20));
		
		c = new GridBagConstraints();
		c.gridx = 0; c.gridy = 0; c.weightx = 1; c.weighty = 1;
		c.fill = GridBagConstraints.BOTH;
		panelContainer.add(panel, c);
		panelContainer.setPreferredSize(new Dimension(200, 200));
		
		Dimension d = inspectorScrollPane.getPreferredSize();
		d.height = 10;
		d.width += 80;
		inspectorScrollPane.setPreferredSize(d);
		inspectorScrollPane.setMinimumSize(d);
		
		c = new GridBagConstraints();
		c.gridx = 0; c.gridy = 0; c.weightx = 1; c.weighty = 1;
		c.fill = GridBagConstraints.BOTH;
		panel.add(new MiddlePanel(GlyphIcon.WARNING), c);
		
		setPreferredSize(new Dimension(800, 600));
		
		setUI(ui);
		refresh();
	}
	
	/** This sits inside the <code>panel</code> as decoration; it is
	 * meant to better demonstrate what the DecoratedPanelUI looks like
	 * when it is a backdrop to real content.
	 * <p>Right-click this panel to change its contents.
	 * <p>Mouseover this panel to show the border boundaries.
	 */
	protected static class MiddlePanel extends JPanel {
		private static final long serialVersionUID = 1L;

		AttributedString[] attrString = null;
		GlyphIcon icon = GlyphIcon.WARNING;
		PanelUI ui = new BasicPanelUI() {
			boolean mouseInside = false;
			
			MouseInputAdapter mouseListener = new MouseInputAdapter() {

				@Override
				public void mouseClicked(MouseEvent e) {
					if(e.isPopupTrigger())
						showPopup(e);
				}

				@Override
				public void mousePressed(MouseEvent e) {
					if(e.isPopupTrigger())
						showPopup(e);
				}

				@Override
				public void mouseReleased(MouseEvent e) {
					if(e.isPopupTrigger())
						showPopup(e);
				}

				@Override
				public void mouseEntered(MouseEvent e) {
					mouseMoved(e);
				}

				@Override
				public void mouseExited(MouseEvent e) {
					mouseInside = false;
					repaint();
				}

				@Override
				public void mouseMoved(MouseEvent e) {
					mouseInside = true;
					repaint();
				}

				JPopupMenu popup = null;
				private void showPopup(MouseEvent e) {
					if(popup==null) {
						popup = new JPopupMenu();
						popup.add(createMenuItem("Warning Icon",GlyphIcon.WARNING));
						popup.add(createMenuItem("Recycle Icon",GlyphIcon.RECYCLE));
						popup.add(createMenuItem("Writing Icon",GlyphIcon.WRITING_HAND));
						popup.add(createMenuItem("Note Icon",GlyphIcon.NOTE));
						popup.add(createMenuItem("Flower Icon",GlyphIcon.FLOWER));
						popup.add(createTextMenuItem());
					}
					popup.show(MiddlePanel.this, e.getX(), e.getY());
				}
				
				private JMenuItem createTextMenuItem() {
					JMenuItem item = new JMenuItem("Show Text");
					item.addActionListener(new ActionListener(){
						public void actionPerformed(ActionEvent e) {
							installText();
						}
					});
					return item;
					
				}
				
				private JMenuItem createMenuItem(String text,final GlyphIcon g) {
					JMenuItem item = new JMenuItem(text);
					item.addActionListener(new ActionListener(){
						public void actionPerformed(ActionEvent e) {
							icon = g;
							attrString = null;
							repaint();
						}
					});
					return item;
				}
				
			};
			
			@Override
			public void paint(Graphics g, JComponent c) {
				Graphics2D g2 = (Graphics2D)g.create();
				try {
					if(icon!=null) {
						Dimension d = icon.getPreferredSize(c.getWidth()*3/4, c.getHeight()*3/4);
						icon.paintIcon(c, g2, c.getWidth()/2 - d.width/2, c.getHeight()/2 - d.height/2, d.width, d.height);
					}
					
					g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
					if(attrString!=null) {
						float y = 0;
						for(int a = 0; a<attrString.length; a++) {
							LineBreakMeasurer lbm = new LineBreakMeasurer(attrString[a].getIterator(), g2.getFontRenderContext());
							TextLayout tl = lbm.nextLayout(c.getWidth());
							while(tl!=null) {
								y += tl.getAscent();
								tl.draw(g2, 0, y);
								y += tl.getDescent() + tl.getLeading();
								
								tl = lbm.nextLayout(c.getWidth());
							}
						}
					}
					
					if(mouseInside) {
						g2.setStroke(new BasicStroke(2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10, new float[] {4, 4}, 0));
						g2.draw(new Rectangle(0, 0, c.getWidth(), c.getHeight()));
					}
				} finally {
					g2.dispose();
				}
			}

			@Override
			public void installUI(JComponent c) {
				super.installUI(c);
				addMouseListener(mouseListener);
				addMouseMotionListener(mouseListener);
			}

			@Override
			public void uninstallUI(JComponent c) {
				super.uninstallUI(c);
				removeMouseListener(mouseListener);
				removeMouseMotionListener(mouseListener);
			}
			
		};
		
		MiddlePanel(GlyphIcon defaultIcon) {
			icon = defaultIcon;
			setUI(ui);
			setOpaque(false);
		}
		
		protected void installText() {
			String text = "Lorem ipsum\nLorem ipsum dolor sit amet, causae audire vel at, magna legendos evertitur te vel, vis ad regione tritani. Ad assum dolorem tincidunt nec. Agam apeirian eos te, graeco reprehendunt sit et. Congue dissentiet neglegentur vel cu, eum an postea euismod.\n"+
					"Ad vivendo detraxit qui, sint timeam prodesset et mei. Vix at labore quaestio, ea ipsum liberavisse vix. Officiis abhorreant et vel, qui ad tota docendi, legimus consectetuer an pri. Et vivendum voluptaria pri, in facilisi instructior eos. Mea ea alii elit, agam malorum reformidans vel te, ex malorum discere facilis nec. Elit commune corrumpit et mei.\n"+
					"Ne choro eripuit cotidieque vel. Ea stet honestatis eam, no nec sint maiorum. Elitr commodo pri in, vis at quot intellegat, posse doctus aliquip sit id. Ne est rebum dicat gloriatur, ea eam deserunt intellegat. An omnis labores feugait usu.\n"+
					"An per illum accommodare, impetus omittam et has. Ornatus insolens pertinacia eos ea, vel an oblique delectus expetendis. At sit autem dolore mentitum, cu quo noster dicunt verear. Pro quas maiorum periculis ex. Ad nibh integre ancillae ius.\n"+
					"Qui at putant volumus elaboraret. Sea ut alii verterem consetetur. Et has utamur offendit. Cum cu ridens labitur, has omnes congue accumsan et.\n"+
					"No nullam noster reformidans sed, et pro augue ignota veritus. Tritani euismod cum ad, ea per iudico albucius legendos. Feugiat legimus adipiscing te eum. Mucius ponderum pro no. Brute dicta omnesque sit ad. Iuvaret nominavi ius ex, in quodsi prodesset vim.\n"+
					"Idque efficiantur ea sed, pro et sensibus inimicus consectetuer. Error mollis vim eu. Doming repudiare ius at, te pri altera dolores propriae. Soluta reprehendunt cu sit. Mel mutat putent ei, his quod hendrerit posidonium cu. Ad velit delicatissimi has, et falli accusam vix.\n"+
					"Qui eu ferri corpora, id populo accusamus mel, omnis signiferumque ad pri. Sea ad singulis sensibus volutpat, ei veniam sententiae eos, everti iuvaret offendit cu qui. Invenire platonem pericula vix id. Quo lorem fugit adipiscing ei, vel ei saepe accusam consequuntur.\n"+
					"Eu omnis fugit usu. Sea no quod doctus tritani. Ex tantas reformidans liberavisse usu. Ei facilis elaboraret vis, eu invidunt posidonium nam, ne has idque laoreet. Pri ceteros praesent deterruisset ei.\n"+
					"Putant blandit cu pri, veniam feugait pri in. Ut homero veritus eam, graece oporteat est an. Vide prompta dolorum cum no. No pri inani maluisset, odio dolorem assentior sed ex, ei nostro principes consetetur pro.";
			icon = null;
			String[] lines = text.split("\n");
			attrString = new AttributedString[lines.length];
			for(int a = 0; a<lines.length; a++) {
				attrString[a] = new AttributedString(lines[a]);
				if(a==0) {
					attrString[a].addAttribute(TextAttribute.SIZE, new Float(40f), 0, lines[a].length());
				} else {
					attrString[a].addAttribute(TextAttribute.SIZE, new Float(14f), 0, lines[a].length());
				}
			}
			repaint();
		}
	}
	
	/** Create a transparent panel that contains a few buttons in a FlowLayout. */
	private JPanel createButton(JComponent... jc) {
		JPanel newPanel = new JPanel(new FlowLayout());
		newPanel.setOpaque(false);
		for(JComponent c : jc) {
			newPanel.add(c);
		}
		return newPanel;
	}
	
	/** Update the DecoratedPanelUI this app is currently demoing. */
	private void setUI(DecoratedPanelUI ui) {
		this.ui = ui;
		if(false) {
			panel.setBorder(ui.getBorder());
		} else {
			this.panel.setUI(ui);
		}
		ui.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent evt) {
				System.out.println(DecoratedPanelUIDemo.this.ui);
				panel.revalidate();
				panel.validate();
				panel.invalidate();
			}
		});
	}
	
	/** Create a button that executes a runnable when clicked. */
	private AbstractButton createButton(String buttonName,final Runnable runnable) {
		JButton button = new JButton(buttonName);
		button.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				runnable.run();
				refresh();
			}
		});
		return button;
	}
	
	/** Add a header to the inspector. */
	private void addHeader(JPanel container, String name,int rowIndex) {
		JLabel label = new JLabel(name);
		label.setBackground(Color.lightGray);
		label.setOpaque(true);
		label.setBorder(
				new CompoundBorder(new PartialLineBorder(Color.gray, new Insets(1,0,1,0)),
				new EmptyBorder(4,4,4,4) )
				);
		addInspectorRow(container, label, rowIndex);
	}

	/** Add a single JComponent that stretches to fill a row of the inspector. */
	private void addInspectorRow(JPanel container,JComponent singleElement,int rowIndex) {
		GridBagConstraints c = new GridBagConstraints();
		c.gridx = 0; c.gridy = rowIndex; c.weightx = 1; c.weighty = 0;
		c.fill = GridBagConstraints.HORIZONTAL;
		c.gridwidth = GridBagConstraints.REMAINDER;
		container.add(singleElement, c);
	}
	
	/** Install a GlyphIcon with normal/rollover/pressed icon colors. */
	private void installIcon(AbstractButton button,char ch) {
		Font defaultFont = new Font("Default", 0, 14);
		button.setIcon(new GlyphIcon(defaultFont, ch, 10, Color.gray));
		button.setSelectedIcon(new GlyphIcon(defaultFont, ch, 10, Color.black));
		button.setRolloverIcon(new GlyphIcon(defaultFont, ch, 10, Color.lightGray));
		button.setUI(new BasicButtonUI());
	}
	
	/** A series of Runnables to execute that refresh the inspector UI
	 * when the DecoratedPanelUI changes.
	 */
	private List<Runnable> refreshRunnables = new ArrayList<Runnable>();
	
	/** When non-zero, we're in the processing of refreshing the inspector
	 * and listeners should stand down.
	 */
	private int refreshing = 0;
	
	/** Refresh the inspector controls to sync up with the DecoratedPanelUI in use. */
	public void refresh() {
		refreshing++;
		try {
			for(Runnable r : refreshRunnables) {
				r.run();
			}
		} finally {
			refreshing--;
		}
	}
	
	/** Add a row to the inspector to modify a Color. This uses a slider
	 * to express shades of gray.
	 * 
	 * @param key the type is unspecified here so it can be either Color or Paint
	 */
	private void addColor(JPanel container,String name,final Key key,int rowIndex) {
		final JSlider colorSlider = new JSlider(0, 255);
		
		colorSlider.addChangeListener(new ChangeListener() {
			@SuppressWarnings("unchecked")
			public void stateChanged(ChangeEvent e) {
				if(refreshing>0) return;
				
				int gray = colorSlider.getValue();
				Color color = new Color( gray, gray, gray);
				if(DecoratedPanelUI.isSupported(key)) {
					ui.setProperty(key, color);
				} else if(DecoratedPanelUI.Border.isSupported(key)) {
					ui.getBorder().setProperty(key, color);
				}
			}
		});
		refreshRunnables.add(new Runnable() {
			@SuppressWarnings("unchecked")
			public void run() {
				Color color = null;
				if(DecoratedPanelUI.isSupported(key)) {
					color = (Color)ui.getProperty(key);
				} else if(DecoratedPanelUI.Border.isSupported(key)) {
					color = (Color)ui.getBorder().getProperty(key);
				}
				int gray = (color.getRed()+color.getGreen()+color.getBlue())/3;
				colorSlider.setValue(gray);
			}
		});
		
		addInspectorRow(container, new JLabel(name), colorSlider, rowIndex);
	}
	
	/** Add a labeled slider to the inspector. This requires the key is equipped
	 * with the NumberBoundsChecker to define the range of values.
	 */
	private void addSlider(JPanel container,String name, final Key<Float> key,int rowIndex) {
		BoundsChecker<?> b = (BoundsChecker)key.getBoundsChecker();
		NumberBoundsChecker n = (NumberBoundsChecker)b;
		addSlider(container, name, key, n.getMin().floatValue(), n.getMax().floatValue(), rowIndex);
	}

	/** Add a labeled slider to the inspector. */
	private void addSlider(JPanel container,String name, final Key<Float> key,final float min,final float max,int rowIndex) {
		final JSlider slider = new JSlider(0, 100);
		final float range = slider.getMaximum() - slider.getMinimum();
		slider.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent e) {
				if(refreshing>0) return;
				
				float v = slider.getValue();
				v = v/range;
				v = v*(max - min) + min;
				
				if(DecoratedPanelUI.isSupported(key)) {
					ui.setProperty(key, v);
				} else if(DecoratedPanelUI.Border.isSupported(key)) {
					ui.getBorder().setProperty(key, v);
				}
			}
		});
		refreshRunnables.add(new Runnable() {
			public void run() {
				float v = 0;
				if(DecoratedPanelUI.isSupported(key)) {
					v = ui.getProperty(key);
				} else if(DecoratedPanelUI.Border.isSupported(key)) {
					v = ui.getBorder().getProperty(key);
				}
				v = (v-min)/(max-min);
				v = v*range;
				slider.setValue( (int)v );
			}
		});
		addInspectorRow(container, new JLabel(name), slider, rowIndex);
	}

	/** Add a labeled spinner to the inspector. This requires the key is equipped
	 * with the NumberBoundsChecker to define the range of values.
	 */ 
	private void addSpinner(JPanel container,String name,Key<Integer> key,int rowIndex) {
		BoundsChecker<?> b = (BoundsChecker)key.getBoundsChecker();
		NumberBoundsChecker n = (NumberBoundsChecker)b;
		addSpinner(container, name, key, n.getMin().intValue(), n.getMax().intValue(), rowIndex);
	}

	/** Add a labeled spinner to the inspector. */
	private void addSpinner(JPanel container,String name,final Key<Integer> key,final int min,final int max,int rowIndex) {
		final JSpinner spinner = new JSpinner(new SpinnerNumberModel(min, min, max, 1));
		spinner.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent e) {
				if(refreshing>0) return;
				
				int v = ((Number)spinner.getValue()).intValue();
				if(DecoratedPanelUI.isSupported(key)) {
					ui.setProperty(key, v);
				} else if(DecoratedPanelUI.Border.isSupported(key)) {
					ui.getBorder().setProperty(key, v);
				}
			}
		});
		refreshRunnables.add(new Runnable() {
			public void run() {
				int v = 0;
				if(DecoratedPanelUI.isSupported(key)) {
					v = ui.getProperty(key);
				} else if(DecoratedPanelUI.Border.isSupported(key)) {
					v = ui.getBorder().getProperty(key);
				}
				spinner.setValue( (int)v );
			}
		});
		addInspectorRow(container, new JLabel(name), spinner, rowIndex);
	}
	
	/** Add two controls to a row in the inspector. */
	private void addInspectorRow(JPanel container,JComponent id,JComponent control,int rowIndex) {
		GridBagConstraints c = new GridBagConstraints();
		c.gridx = 0; c.gridy = rowIndex; c.weightx = 0; c.weighty = 0;
		c.gridwidth = 1; c.anchor = GridBagConstraints.EAST;
		c.insets = new Insets(3,3,3,3);
		container.add(id, c);
		c.gridx++; c.anchor = GridBagConstraints.WEST;
		container.add(control, c);
	}
}