001: /*
002: $Id: ColorDistributionParameter.java,v 1.8 2005/02/14 12:06:19 vauclair Exp $
003:
004: Copyright (C) 2002-2005 Sebastien Vauclair
005:
006: This file is part of Extensible Java Profiler.
007:
008: Extensible Java Profiler is free software; you can redistribute it and/or
009: modify it under the terms of the GNU General Public License as published by
010: the Free Software Foundation; either version 2 of the License, or
011: (at your option) any later version.
012:
013: Extensible Java Profiler is distributed in the hope that it will be useful,
014: but WITHOUT ANY WARRANTY; without even the implied warranty of
015: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016: GNU General Public License for more details.
017:
018: You should have received a copy of the GNU General Public License
019: along with Extensible Java Profiler; if not, write to the Free Software
020: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
021: */
022:
023: package ejp.presenter.api.filters.parameters;
024:
025: import java.awt.Color;
026: import java.awt.Component;
027: import java.awt.Dimension;
028: import java.awt.Graphics;
029: import java.awt.event.ActionEvent;
030: import java.awt.event.ActionListener;
031: import java.awt.event.InputEvent;
032: import java.awt.event.MouseAdapter;
033: import java.awt.event.MouseEvent;
034: import java.awt.image.BufferedImage;
035: import java.text.DecimalFormat;
036: import java.util.ArrayList;
037: import java.util.StringTokenizer;
038:
039: import javax.swing.DefaultListCellRenderer;
040: import javax.swing.JComboBox;
041: import javax.swing.JComponent;
042: import javax.swing.JList;
043: import javax.swing.JPanel;
044: import javax.swing.JSpinner;
045: import javax.swing.SpinnerNumberModel;
046: import javax.swing.event.ChangeEvent;
047: import javax.swing.event.ChangeListener;
048:
049: import ejp.presenter.api.util.CustomLogger;
050: import ejp.presenter.gui.ColorChooserDialog;
051: import ejp.presenter.gui.Utils;
052:
053: /**
054: * A color distribution parameter, which associates a color to every value in
055: * the <code>[0, 1]</code> interval.
056: *
057: * The interval is actually split into by a user-provided divider and a color
058: * can be associated to every sub-range. A convenience system allows to
059: * automatically build up a gradient of colors between the first and the last
060: * ones.
061: *
062: * <p>
063: * <b>Known issue </b>: due to Swing's error #4497301, the combo box might not
064: * be selectable the second time the dialog is showed. There is currently no
065: * acceptable workaround to this. The solution used here consists on limiting
066: * the count of rows to a maximum.
067: *
068: * @author Sebastien Vauclair
069: * @version 1.0
070: */
071: public class ColorDistributionParameter extends AbstractParameter
072: implements ActionListener, ChangeListener {
073: // ///////////////////////////////////////////////////////////////////////////
074: // CONSTANTS
075: // ///////////////////////////////////////////////////////////////////////////
076:
077: /**
078: * Decimal numbers formatter used to render percents.
079: */
080: public static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat(
081: "##0.0%");
082:
083: /**
084: * Separator character between colors in text descriptions.
085: */
086: public static final String COLORS_SEPARATOR = ";";
087:
088: // ///////////////////////////////////////////////////////////////////////////
089: // MEMBERS
090: // ///////////////////////////////////////////////////////////////////////////
091:
092: /**
093: * Current list of colors.
094: *
095: * <p>
096: * Additional colors (after index <code>divisions</code> are kept in memory
097: * for possible later usage.
098: */
099: protected final ArrayList colors = new ArrayList();
100:
101: /**
102: * Number of divisions.
103: *
104: * <p>
105: * <code>number of divisions = number of colors - 1</code>.
106: */
107: protected int divisions = 0;
108:
109: /**
110: * Spinner model.
111: */
112: protected final SpinnerNumberModel spinnerModel = new SpinnerNumberModel(
113: divisions /* value */, 0 /* minimum */,
114: 1000 /* maximum */, 1 /* step size */);
115:
116: /**
117: * Avoids reentrancy into <code>divisionsUpdated()</code>.
118: */
119: protected boolean divisionsUpdatedIsWorking = false;
120:
121: /**
122: * Current read-only state of the parameter.
123: */
124: protected boolean readOnly = false;
125:
126: // ///////////////////////////////////////////////////////////////////////////
127: // COMPONENTS
128: // ///////////////////////////////////////////////////////////////////////////
129:
130: /**
131: * Divisions spinner.
132: */
133: protected final JSpinner jsDivisions;
134:
135: /**
136: * Intervals combo box.
137: */
138: protected final JComboBox jcbIntervals;
139:
140: /**
141: * Panel that displays current color distribution.
142: */
143: protected final ColorsDisplay cdSamples;
144:
145: // ///////////////////////////////////////////////////////////////////////////
146: // CONSTRUCTOR
147: // ///////////////////////////////////////////////////////////////////////////
148:
149: /**
150: * Creates a new color distribution parameter instance.
151: *
152: * @param aName
153: * the parameter's name.
154: * @param aTitle
155: * parameter's caption.
156: * @param aToolTipText
157: * a tool-tip text.
158: */
159: public ColorDistributionParameter(String aName, String aTitle,
160: String aToolTipText) {
161: super (aName, aTitle, aToolTipText);
162:
163: // default color
164: colors.add(Color.black);
165:
166: jsDivisions = new JSpinner(spinnerModel);
167: Utils.setCommonProperties(jsDivisions);
168: jsDivisions.addChangeListener(this );
169: addLine("Number of divisions",
170: "Number of separations of the interval.", jsDivisions,
171: null);
172:
173: jcbIntervals = new JComboBox();
174: Utils.setCommonProperties(jcbIntervals);
175: jcbIntervals.setRenderer(new ComboCellRenderer());
176: jcbIntervals
177: .setPrototypeDisplayValue(new ComboItem(1, 1, null));
178: jcbIntervals.addActionListener(this );
179:
180: // Workaround for Swing's error #4497301
181: // The maximum count of rows is choosen so that the popup does not
182: // exceed the dialog's (ParametersDialog) bounds, so that a light-weight
183: // popup can be used by Swing.
184: jcbIntervals.setMaximumRowCount(3);
185:
186: cdSamples = new ColorsDisplay(ColorsDisplay.DEFAULT_WIDTH,
187: (int) jcbIntervals.getPreferredSize().getHeight());
188: cdSamples
189: .setToolTipText("Double-click to automatically build a gradient");
190: cdSamples.addMouseListener(new MouseAdapter() {
191: public void mouseClicked(MouseEvent aEvent) {
192: if (!readOnly
193: && aEvent.getButton() == MouseEvent.BUTTON1
194: && aEvent.getClickCount() == 2)
195: buildGradient();
196: }
197: });
198:
199: addLine("Colors", "Colors of each sub-range.", jcbIntervals,
200: cdSamples);
201:
202: divisionsUpdated();
203: }
204:
205: // ///////////////////////////////////////////////////////////////////////////
206: // READ-ONLY
207: // ///////////////////////////////////////////////////////////////////////////
208:
209: /**
210: * Sets the paramter as read-only.
211: *
212: * <p>
213: * Locks divisions spinner and no longer popups a color selection dialog when
214: * an interval is selected.
215: */
216: public void setReadOnly() {
217: readOnly = true;
218: jsDivisions.setEnabled(false);
219:
220: cdSamples.setToolTipText(null); // remove tool tip text
221: jcbIntervals.setRenderer(new ComboCellRenderer()); // remove tool tip text
222: // (note - this is
223: // dirty!)
224: }
225:
226: // ///////////////////////////////////////////////////////////////////////////
227: // ACCESSORS
228: // ///////////////////////////////////////////////////////////////////////////
229:
230: /**
231: * Sets value from object.
232: *
233: * Wrapper to <code>setValue(Color[])</code>.
234: */
235: public void setValue(Object aObject) throws ClassCastException {
236: setValue((Color[]) aObject);
237: }
238:
239: /**
240: * Sets value from a color array.
241: *
242: * Updates divisions and intervals values.
243: *
244: * @param aColors
245: * a non-empty <code>Color[]</code> value.
246: */
247: public void setValue(Color[] aColors) {
248: int nb = aColors.length;
249:
250: if (nb < 1)
251: throw new IllegalArgumentException("Set of colors is empty");
252:
253: divisions = nb - 1;
254: spinnerModel.setValue(new Integer(divisions));
255:
256: for (int i = 0; i < nb; i++)
257: if (i < colors.size())
258: colors.set(i, aColors[i]);
259: else
260: colors.add(aColors[i]);
261:
262: divisionsUpdated();
263: }
264:
265: /**
266: * Sets value from text.
267: *
268: * <p>
269: * Input text is first tokenized using <code>COLORS_SEPARATOR</code>, then
270: * each token is processed using <code>Color.decode(String)</code>.
271: *
272: * @exception NumberFormatException
273: * if a number cannot be parsed to a color.
274: * @exception IllegalStateException
275: * if an error occurs in the tokenizer.
276: */
277: public void setValueAsText(String aTextValue)
278: throws NumberFormatException, IllegalStateException {
279: StringTokenizer st = new StringTokenizer(aTextValue,
280: COLORS_SEPARATOR, false);
281: int nb = st.countTokens();
282:
283: Color[] tmpColors = new Color[nb];
284: int i = 0;
285: while (st.hasMoreTokens())
286: tmpColors[i++] = Color.decode(st.nextToken()); // throws NFE
287:
288: if (i != nb)
289: throw new IllegalStateException(
290: "Actual count of colors is smaller than expected");
291:
292: setValue(tmpColors);
293: }
294:
295: /**
296: * Gets value as an object.
297: */
298: public Object getValue() {
299: return getColorsValue();
300: }
301:
302: /**
303: * Gets value as text.
304: *
305: * <p>
306: * Colors are exported as integers using <code>Color.getRGB()</code>. They
307: * are separated by <code>COLORS_SEPARATOR</code> character.
308: */
309: public String getValueAsText() {
310: StringBuffer buffer = new StringBuffer();
311: for (int i = 0; i <= divisions; i++) {
312: if (i > 0)
313: buffer.append(COLORS_SEPARATOR);
314: buffer.append(((Color) colors.get(i)).getRGB());
315: }
316: return buffer.toString();
317: }
318:
319: /**
320: * Get current value of the parameter.
321: *
322: * @return a <code>Color[]</code> value.
323: */
324: public Color[] getColorsValue() {
325: Color[] result = new Color[divisions + 1];
326: for (int i = 0; i <= divisions; i++)
327: result[i] = (Color) colors.get(i);
328: return result;
329: }
330:
331: /**
332: * Gets the color currently associated to a real number.
333: *
334: * @param aRatio
335: * a <code>double</code> value in the <code>[0, 1]</code>
336: * interval.
337: * @return the associated <code>Color</code> value.
338: */
339: public Color colorForRatio(double aRatio) {
340: int index = (aRatio == 1d ? divisions // last color
341: : (int) (aRatio * (divisions + 1)));
342:
343: return (Color) colors.get(index);
344: }
345:
346: // ///////////////////////////////////////////////////////////////////////////
347: // PROTECTED MEMBERS
348: // ///////////////////////////////////////////////////////////////////////////
349:
350: /**
351: * Builds up a gradient between the first and last colors.
352: */
353: protected void buildGradient() {
354: if (divisions < 2)
355: return;
356:
357: Color first = (Color) colors.get(0);
358: Color last = (Color) colors.get(divisions);
359:
360: int firstR = first.getRed();
361: int firstG = first.getGreen();
362: int firstB = first.getBlue();
363:
364: double deltaR = ((double) (last.getRed() - firstR))
365: / ((double) divisions);
366: double deltaG = ((double) (last.getGreen() - firstG))
367: / ((double) divisions);
368: double deltaB = ((double) (last.getBlue() - firstB))
369: / ((double) divisions);
370:
371: for (int i = 1; i <= divisions - 1; i++)
372: colors.set(i, new Color(firstR + (int) (deltaR * i), firstG
373: + (int) (deltaG * i), firstB + (int) (deltaB * i)));
374:
375: divisionsUpdated();
376: }
377:
378: /**
379: * Updates all fields to match a new divisions number.
380: *
381: * <p>
382: * When the number of divisions grows, previously selected colors are used or
383: * if there are none, last color is repeated.
384: *
385: * <p>
386: * This method updates fields and calls <code>divisionsUpdated()</code> to
387: * update the graphical representation.
388: *
389: * @param aNewDivisions
390: * new <code>int</code> value.
391: */
392: protected void updateDivisions(int aNewDivisions) {
393: int diff = aNewDivisions - divisions;
394: if (diff == 0)
395: return;
396:
397: if (diff > 0) {
398: Object last = colors.get(divisions);
399: for (int i = 0; i < diff; i++)
400: if (divisions + i + 1 >= colors.size())
401: colors.add(last);
402: }
403:
404: divisions = aNewDivisions;
405: divisionsUpdated();
406: }
407:
408: /**
409: * Updates graphical representation of color intervals.
410: *
411: * <p>
412: * Uses <code>divisions</code> and <code>colors</code>.
413: */
414: protected void divisionsUpdated() {
415: divisionsUpdatedIsWorking = true;
416:
417: // build combo items
418: int index = jcbIntervals.getSelectedIndex();
419: jcbIntervals.removeAllItems();
420: for (int i = 0; i <= divisions; i++) {
421: double floor = ((double) i) / ((double) (divisions + 1));
422: double ceil = ((double) (i + 1))
423: / ((double) (divisions + 1));
424: jcbIntervals.addItem(new ComboItem(floor, ceil,
425: (Color) colors.get(i)));
426: }
427: jcbIntervals.setSelectedIndex(Math.max(0, Math.min(index,
428: divisions)));
429: updateSelectedColor();
430:
431: // render colors display
432: cdSamples.invalidateGraphics();
433:
434: divisionsUpdatedIsWorking = false;
435: }
436:
437: /**
438: * Updates combo box to render currently selected colors.
439: *
440: * <p>
441: * This must be done because of an error in Swing's Metal UI, which does call
442: * the renderer for the selected item but then overrides the foreground color.
443: */
444: protected void updateSelectedColor() {
445: ComboItem item = (ComboItem) jcbIntervals.getSelectedItem();
446:
447: // update display of selected item
448: jcbIntervals.setForeground(item.color);
449: }
450:
451: // ///////////////////////////////////////////////////////////////////////////
452: // ChangeListener INTERFACE
453: // ///////////////////////////////////////////////////////////////////////////
454:
455: /**
456: * Called when the spinner's value is considered as changed.
457: *
458: * <p>
459: * Updates intervals.
460: */
461: public void stateChanged(ChangeEvent aEvent) {
462: Object src = aEvent.getSource();
463: if (src == jsDivisions)
464: updateDivisions(spinnerModel.getNumber().intValue());
465: else
466: CustomLogger.INSTANCE
467: .warning("Unable to handle state change event from unknwon source");
468: }
469:
470: // ///////////////////////////////////////////////////////////////////////////
471: // ActionListener INTERFACE
472: // ///////////////////////////////////////////////////////////////////////////
473:
474: /**
475: * Called when an interval is selected in combo box.
476: */
477: public void actionPerformed(ActionEvent aEvent) {
478: Object src = aEvent.getSource();
479: if (src == jcbIntervals) {
480: if (!divisionsUpdatedIsWorking
481: && ((aEvent.getModifiers() & InputEvent.BUTTON1_MASK) != 0)) {
482: if (!readOnly) {
483: ComboItem item = (ComboItem) jcbIntervals
484: .getSelectedItem();
485: ColorChooserDialog ccd = new ColorChooserDialog(
486: dialog /* parent */, item.color /*
487: * initial
488: * color
489: */);
490: Color newColor = ccd.showDialog();
491: if (newColor != null) {
492: colors.set(jcbIntervals.getSelectedIndex(),
493: newColor);
494: item.setColor(newColor);
495:
496: cdSamples.invalidateGraphics();
497: }
498: }
499:
500: updateSelectedColor();
501: }
502: } else
503: CustomLogger.INSTANCE
504: .warning("Unable to handle action event from unknown source");
505: }
506:
507: // ///////////////////////////////////////////////////////////////////////////
508: // NESTED CLASSES - ColorsDisplay
509: // ///////////////////////////////////////////////////////////////////////////
510:
511: /**
512: * Graphical panel that displays the current color distribution.
513: */
514: protected class ColorsDisplay extends JPanel {
515: /**
516: * Default preferred width.
517: */
518: public static final int DEFAULT_WIDTH = 102;
519:
520: /**
521: * Current width.
522: */
523: protected final int width;
524:
525: /**
526: * Current height.
527: */
528: protected final int height;
529:
530: /**
531: * Current dimension.
532: */
533: protected final Dimension dimension;
534:
535: /**
536: * Image used to hold paint of color distribution.
537: */
538: protected final BufferedImage image;
539:
540: /**
541: * Graphics used to paint color distribution.
542: *
543: */
544: protected Graphics graphics;
545:
546: /**
547: * Creates a new <code>ColorsDisplay</code> instance.
548: *
549: * @param aWidth
550: * preferred width.
551: * @param aHeight
552: * preferred height.
553: */
554: public ColorsDisplay(int aWidth, int aHeight) {
555: width = aWidth;
556: height = aHeight;
557: dimension = new Dimension(aWidth, aHeight);
558: image = new BufferedImage(aWidth, aHeight,
559: BufferedImage.TYPE_INT_RGB);
560: }
561:
562: /**
563: * Implementation of minimum size.
564: */
565: public Dimension getMinimumSize() {
566: return dimension;
567: }
568:
569: /**
570: * Implementation of preferred size.
571: */
572: public Dimension getPreferredSize() {
573: return dimension;
574: }
575:
576: /**
577: * Implementation of maximum size.
578: */
579: public Dimension getMaximumSize() {
580: return dimension;
581: }
582:
583: /**
584: * Called to paint the panel to the specified graphics.
585: *
586: * @param aGraphics
587: * a <code>Graphics</code> value.
588: */
589: protected void paintComponent(Graphics aGraphics) {
590: super .paintComponent(aGraphics); // paint background
591:
592: if (graphics == null) {
593: graphics = image.createGraphics();
594:
595: graphics.setColor(Color.black);
596: graphics.drawRect(0, 0, width - 1, height - 1);
597:
598: int bottomY = height - 2; // constant
599: for (int x = 1; x <= width - 2; x++) {
600: int index = (int) (((double) x)
601: / ((double) (width - 1)) * (divisions + 1));
602:
603: Color color = (Color) colors.get(index);
604: graphics.setColor(color);
605: graphics.drawLine(x, 1, x, bottomY);
606: }
607: }
608:
609: aGraphics
610: .drawImage(image, 0, 0, null /* image observer */);
611: }
612:
613: /**
614: * Invalidates current graphics and repaints it.
615: */
616: public void invalidateGraphics() {
617: graphics = null;
618: repaint();
619: }
620: }
621:
622: // ///////////////////////////////////////////////////////////////////////////
623: // NESTED CLASSES - ComboItem
624: // ///////////////////////////////////////////////////////////////////////////
625:
626: /**
627: * An item (interval and associated color) of the combo box.
628: */
629: protected class ComboItem {
630: /**
631: * Floor value of the interval.
632: */
633: public final double floor;
634:
635: /**
636: * Ceil value of the interval.
637: */
638: public final double ceil;
639:
640: /**
641: * Text defining the interval.
642: *
643: */
644: public final String text;
645:
646: /**
647: * Current color of the item.
648: */
649: protected Color color;
650:
651: /**
652: * Creates a new <code>ComboItem</code> instance.
653: *
654: * @param aFloor
655: * floor value.
656: * @param aCeil
657: * ceil value.
658: * @param aColor
659: * default color.
660: */
661: public ComboItem(double aFloor, double aCeil, Color aColor) {
662: floor = aFloor;
663: ceil = aCeil;
664: color = aColor;
665: text = renderRatio(aFloor) + " \u2264 value "
666: + (aCeil == 1.0d ? "\u2264" : "<") + " "
667: + renderRatio(aCeil);
668: }
669:
670: /**
671: * Sets current color of the item.
672: *
673: * @param aColor
674: * a <code>Color</code> value.
675: */
676: public void setColor(Color aColor) {
677: color = aColor;
678: }
679:
680: /**
681: * Gets current color of the item.
682: *
683: * @return a <code>Color</code> value.
684: */
685: public Color getColor() {
686: return color;
687: }
688:
689: /**
690: * Returns the caption of the item.
691: *
692: * @return a <code>String</code> value.
693: */
694: public String toString() {
695: return text;
696: }
697:
698: /**
699: * Convenience method to display a decimal ratio as a percent.
700: *
701: * @param aDouble
702: * a <code>double</code> value.
703: * @return a <code>String</code> value.
704: */
705: protected String renderRatio(double aDouble) {
706: return DECIMAL_FORMAT.format(aDouble);
707: }
708: }
709:
710: // ///////////////////////////////////////////////////////////////////////////
711: // NESTED CLASSES - ComboCellRenderer
712: // ///////////////////////////////////////////////////////////////////////////
713:
714: /**
715: * Renderer for combo box cells (intervals).
716: */
717: protected class ComboCellRenderer extends DefaultListCellRenderer {
718: /**
719: * Renderer implementation.
720: */
721: public Component getListCellRendererComponent(JList aList,
722: Object aValue, int aIndex, boolean aIsSelected,
723: boolean aCellHasFocus) {
724: Component result = super .getListCellRendererComponent(
725: aList, aValue, aIndex, aIsSelected, aCellHasFocus);
726: result.setForeground(((ComboItem) aValue).color);
727:
728: // note - when in readOnly mode, the tooltip should be changed
729: if (!readOnly && result instanceof JComponent)
730: ((JComponent) result)
731: .setToolTipText("Left-click this item to change its color");
732: return result;
733: }
734: }
735: }
|