001: /*
002: * Copyright (c) 2003-2007 JGoodies Karsten Lentzsch. All Rights Reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * o Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: *
010: * o Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * o Neither the name of JGoodies Karsten Lentzsch nor the names of
015: * its contributors may be used to endorse or promote products derived
016: * from this software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020: * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021: * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
022: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025: * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026: * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
027: * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package com.jgoodies.validation.tutorial.util;
032:
033: import java.awt.*;
034: import java.beans.PropertyChangeEvent;
035: import java.beans.PropertyChangeListener;
036: import java.util.Map;
037:
038: import javax.swing.*;
039: import javax.swing.text.JTextComponent;
040:
041: import com.jgoodies.validation.ValidationMessage;
042: import com.jgoodies.validation.ValidationResult;
043: import com.jgoodies.validation.ValidationResultModel;
044: import com.jgoodies.validation.view.ValidationComponentUtils;
045: import com.jgoodies.validation.view.ValidationResultViewFactory;
046:
047: /**
048: * Can display validation feedback icons "over" a content panel.
049: * It observes a ValidationResultModel and creates icon labels
050: * in a feedback layer of a {@link JLayeredPane} on top of the content layer.
051: * To position the feedback labels, the content pane is traversed
052: * and searched for text components that match a validation message key
053: * in this panel's observed ValidationResultModel.<p>
054: *
055: * <strong>Note:</strong> This panel doesn't reserve space for the portion
056: * used to display the overlaid feedback components. It has been designed
057: * to not change the layout of the wrapped content. Therefore you must reserve
058: * this space, or in other words, you must ensure that the wrapped content
059: * provides enough space to display the overlaid components.
060: * Since the current implementation positions the overlay components
061: * in the lower left, just make sure that there are about 6 pixel to the left
062: * and bottom of the input components that can be marked.<p>
063: *
064: * This panel handles two event types: <ol>
065: * <li>the ValidationResultModel changes; in this case the set of visible
066: * feedback components shall mark the input components that match the
067: * new validation result. This is done by this class' internal
068: * <code>ValidationResultChangeHandler</code> which in turn invokes
069: * <code>#updateFeedbackComponents</code>.
070: * <li>the content layout changes; the feedback components must then be
071: * repositioned to reflect the position of the overlaid input components.
072: * This is done by overriding <code>#validateTree</code> and invoking
073: * <code>#repositionFeedBackComponents</code> after the child tree has
074: * been laid out. The current simple but expensive implementation
075: * updates all components.
076: * </ol><p>
077: *
078: * TODO: Check how the wrapping mechanism shall work with
079: * JSplitPanes, JTabbedPanes and CardPanels. At least provide
080: * guidelines, how to wrap these panel types, or how to handle
081: * these cases.<p>
082: *
083: * TODO: Turn this class into an abstract superclass.
084: * Subclasses shall implement the feedback component creation
085: * and specify where to locate the feedback component relative
086: * to the underlying content component.<p>
087: *
088: * TODO: Consider adding a mechanism, so that components can be added
089: * and removed later.
090: *
091: * @author Karsten Lentzsch
092: * @version $Revision: 1.21 $
093: */
094: public final class IconFeedbackPanel extends JLayeredPane {
095:
096: private static final int CONTENT_LAYER = 1;
097: private static final int FEEDBACK_LAYER = 2;
098:
099: /**
100: * Holds the ValidationResult and reports changes in that result.
101: * Used to update the state of the feedback components.
102: */
103: private final ValidationResultModel model;
104:
105: /**
106: * Refers to the content panel that holds the content components.
107: */
108: private final JComponent content;
109:
110: // Instance Creation ******************************************************
111:
112: /**
113: * Creates an IconFeedbackPanel on the given ValidationResultModel
114: * using the specified content panel.<p>
115: *
116: * <strong>Note:</strong> Typically you should wrap component trees with
117: * {@link #getWrappedComponentTree(ValidationResultModel, JComponent)},
118: * not this constructor.<p>
119: *
120: * <strong>Note:</strong> You must not add or remove components
121: * from the content once this constructor has been invoked.
122: *
123: * @param model the ValidationResultModel to observe
124: * @param content the panel that contains the content components
125: *
126: * @throws NullPointerException if model or content is <code>null</code>.
127: */
128: public IconFeedbackPanel(ValidationResultModel model,
129: JComponent content) {
130: if (model == null)
131: throw new NullPointerException(
132: "The validation result model must not be null.");
133: if (content == null)
134: throw new NullPointerException(
135: "The content must not be null.");
136:
137: this .model = model;
138: this .content = content;
139: setLayout(new SimpleLayout());
140: add(content, CONTENT_LAYER);
141: initEventHandling();
142: }
143:
144: // Convenience Code *******************************************************
145:
146: /**
147: * Wraps the components in the given component tree with instances
148: * of IconFeedbackPanel where necessary. Such a wrapper is required
149: * for all JScrollPanes that contain multiple children and
150: * for the root - unless it's a JScrollPane with multiple children.
151: *
152: * @param root the root of the component tree to wrap
153: * @return the wrapped component tree
154: */
155: public static JComponent getWrappedComponentTree(
156: ValidationResultModel model, JComponent root) {
157: wrapComponentTree(model, root);
158: return isScrollPaneWithUnmarkableView(root) ? root
159: : new IconFeedbackPanel(model, root);
160: }
161:
162: private static void wrapComponentTree(ValidationResultModel model,
163: Container container) {
164: if (!(container instanceof JScrollPane)) {
165: int componentCount = container.getComponentCount();
166: for (int i = 0; i < componentCount; i++) {
167: Component child = container.getComponent(i);
168: if (child instanceof Container)
169: wrapComponentTree(model, (Container) child);
170: }
171: return;
172: }
173: JScrollPane scrollPane = (JScrollPane) container;
174: JViewport viewport = scrollPane.getViewport();
175: JComponent view = (JComponent) viewport.getView();
176: if (isMarkable(view))
177: return;
178: // TODO: Consider adding the following sanity check:
179: // the view must not be an IconFeedbackPanel
180: Component wrappedView = new IconFeedbackPanel(model, view);
181: viewport.setView(wrappedView);
182: wrapComponentTree(model, view);
183: }
184:
185: private static boolean isScrollPaneWithUnmarkableView(Component c) {
186: if (!(c instanceof JScrollPane))
187: return false;
188: JScrollPane scrollPane = (JScrollPane) c;
189: JViewport viewport = scrollPane.getViewport();
190: JComponent view = (JComponent) viewport.getView();
191: return !isMarkable(view);
192: }
193:
194: // Initialization *********************************************************
195:
196: /**
197: * Registers a listener with the validation result model that updates
198: * the feedback components.
199: */
200: private void initEventHandling() {
201: model.addPropertyChangeListener(
202: ValidationResultModel.PROPERTYNAME_RESULT,
203: new ValidationResultChangeHandler());
204: }
205:
206: // Abstract Behavior ******************************************************
207:
208: /**
209: * Creates and returns a validation feedback component
210: * that shall overlay the specified content component.<p>
211: *
212: * This implementation returns a JLabel. The validation result's severity
213: * is used to lookup the label's icon; the result's message text is set
214: * as the label's tooltip text.<p>
215: *
216: * TODO: Turn this method into an abstract method if this class
217: * becomes an abstract superclass of general feedback overlay panels.
218: *
219: * @param result determines the label's icon and tooltip text
220: * @param contentComponent the component to get overlaid feedback
221: * @return the feedback component that overlays the content component
222: *
223: * @throws NullPointerException if the result is <code>null</code>
224: */
225: private JComponent createFeedbackComponent(ValidationResult result,
226: Component contentComponent) {
227: Icon icon = ValidationResultViewFactory.getSmallIcon(result
228: .getSeverity());
229: JLabel label = new JLabel(icon);
230: label.setToolTipText(getMessagesToolTipText(result));
231: label.setSize(label.getPreferredSize());
232: return label;
233: }
234:
235: /**
236: * Returns a string representation of the given validation result,
237: * intended for tool tips. Unlike {@link ValidationResult#getMessagesText()}
238: * this method returns an HTML string. It is invoked by
239: * {@link #createFeedbackComponent(ValidationResult, Component)}
240: * and the result won't be empty.
241: *
242: * @param result provides the ValidationMessages to iterate over
243: * @return an HTML representation of the given result
244: *
245: * @since 1.3.1
246: */
247: private static String getMessagesToolTipText(ValidationResult result) {
248: StringBuilder builder = new StringBuilder("<html>");
249: for (ValidationMessage message : result.getMessages()) {
250: if (builder.length() > 0) {
251: builder.append("<br>");
252: }
253: builder.append(message.formattedText());
254: }
255: builder.append("</html>");
256: return builder.toString();
257: }
258:
259: /**
260: * Computes and returns the origin of the given feedback component
261: * using the content component's origin.<p>
262: *
263: * This implementation returns a JLabel. The validation result's severity
264: * is used to lookup the label's icon; the result's message text is
265: * set as the label's tooltip text.<p>
266: *
267: * TODO: Turn this method into an abstract method if this class
268: * becomes an abstract superclass of general feedback overlay panels.
269: *
270: * @param feedbackComponent the component that overlays the content
271: * @param contentComponent the component to get overlaid feedback
272: * @return the feedback component's origin
273: *
274: * @throws NullPointerException if the feedback component or content component
275: * is <code>null</code>
276: */
277: private Point getFeedbackComponentOrigin(
278: JComponent feedbackComponent, Component contentComponent) {
279: boolean isLTR = contentComponent.getComponentOrientation()
280: .isLeftToRight();
281: int x = contentComponent.getX()
282: + (isLTR ? 0 : contentComponent.getWidth() - 1)
283: - feedbackComponent.getWidth() / 2;
284: int y = contentComponent.getY() + contentComponent.getHeight()
285: - feedbackComponent.getHeight() + 2;
286:
287: return new Point(x, y);
288: }
289:
290: // Updating the Overlay Components ****************************************
291:
292: private void removeAllFeedbackComponents() {
293: int componentCount = getComponentCount();
294: for (int i = componentCount - 1; i >= 0; i--) {
295: Component child = getComponent(i);
296: int layer = getLayer(child);
297: if (layer == FEEDBACK_LAYER)
298: remove(i);
299: }
300: }
301:
302: /**
303: * Traverses the component tree starting at the given container
304: * and creates a feedback component for each JTextComponent that
305: * is associated with a message in the specified <code>keyMap</code>.<p>
306: *
307: * The arguments passed to the feedback component creation method
308: * are the visited component and its associated validation sub result.
309: * This sub result is requested from the specified <code>keyMap</code>
310: * using the visited component's message key.
311: *
312: * @param container the component tree root
313: * @param keyMap maps messages keys to associated validation results
314: */
315: private void visitComponentTree(Container container,
316: Map<Object, ValidationResult> keyMap, int xOffset,
317: int yOffset) {
318: int componentCount = container.getComponentCount();
319: for (int i = 0; i < componentCount; i++) {
320: Component child = container.getComponent(i);
321: if (!child.isVisible())
322: continue;
323: if (isMarkable(child)) {
324: if (isScrollPaneView(child)) {
325: Component containerParent = container.getParent();
326: addFeedbackComponent(containerParent,
327: (JComponent) child, keyMap, xOffset
328: - containerParent.getX(), yOffset
329: - containerParent.getY());
330: } else {
331: addFeedbackComponent(child, (JComponent) child,
332: keyMap, xOffset, yOffset);
333: }
334: } else if (isScrollPaneView(child)) {
335: // Just do nothing.
336: } else if (child instanceof Container) {
337: visitComponentTree((Container) child, keyMap, xOffset
338: + child.getX(), yOffset + child.getY());
339: }
340: }
341: }
342:
343: private static boolean isScrollPaneView(Component c) {
344: Container container = c.getParent();
345: Container containerParent = container.getParent();
346: return (container instanceof JViewport)
347: && (containerParent instanceof JScrollPane);
348: }
349:
350: /**
351: * Checks and answers if the given component can be marked or not.<p>
352: *
353: * TODO: Check the combobox editable state.<p>
354: *
355: * TODO: Add the JSpinner to the list of markable components.
356: *
357: * @param component the component to be checked
358: * @return true if the given component can be marked, false if not
359: */
360: private static boolean isMarkable(Component component) {
361: return component instanceof JTextComponent
362: || component instanceof JComboBox;
363: }
364:
365: private void addFeedbackComponent(Component contentComponent,
366: JComponent messageComponent,
367: Map<Object, ValidationResult> keyMap, int xOffset,
368: int yOffset) {
369: ValidationResult result = getAssociatedResult(messageComponent,
370: keyMap);
371: JComponent feedbackComponent = createFeedbackComponent(result,
372: contentComponent);
373: if (feedbackComponent == null)
374: return;
375: add(feedbackComponent, Integer.valueOf(FEEDBACK_LAYER));
376: Point overlayPosition = getFeedbackComponentOrigin(
377: feedbackComponent, contentComponent);
378: overlayPosition.translate(xOffset, yOffset);
379: feedbackComponent.setLocation(overlayPosition);
380: }
381:
382: /**
383: * Returns the ValidationResult associated with the given component
384: * using the specified validation result key map. Unlike
385: * {@link ValidationComponentUtils#getAssociatedResult(JComponent, Map)}
386: * this method returns the empty result if the component has no keys set.
387: *
388: * @param comp the component may be marked with a validation message key
389: * @param keyMap maps validation message keys to ValidationResults
390: * @return the ValidationResult associated with the given component
391: * as provided by the specified validation key map
392: * or <code>ValidationResult.EMPTY</code> if the component has no message key set,
393: * or <code>ValidationResult.EMPTY</code> if no result is associated
394: * with the component
395: */
396: private static ValidationResult getAssociatedResult(
397: JComponent comp, Map<Object, ValidationResult> keyMap) {
398: ValidationResult result = ValidationComponentUtils
399: .getAssociatedResult(comp, keyMap);
400: return result == null ? ValidationResult.EMPTY : result;
401: }
402:
403: // Event Handling *********************************************************
404:
405: private void updateFeedbackComponents() {
406: removeAllFeedbackComponents();
407: visitComponentTree(content, model.getResult().keyMap(), 0, 0);
408: repaint();
409: }
410:
411: /**
412: * Ensures that the feedback components are repositioned.
413: * Invoked by <code>#validate</code>, i. e. if this panel is laid out.<p>
414: *
415: * TODO: Improve this implementation to set only positions.
416: * The current implementation removes all components and re-adds
417: * them later.
418: */
419: private void repositionFeedbackComponents() {
420: updateFeedbackComponents();
421: }
422:
423: /**
424: * Recursively descends the container tree and recomputes the
425: * layout for any subtrees marked as needing it (those marked as
426: * invalid). In addition to the superclass behavior, we reposition
427: * the feedback components after the child components have been
428: * validated.<p>
429: *
430: * We reposition the feedback components only, if this panel is visible;
431: * if it becomes visible, #validateTree will be invoked.
432: *
433: * @see Container#validateTree()
434: * @see #validate()
435: * @see #invalidate()
436: * @see #doLayout()
437: * @see Component#setVisible(boolean)
438: * @see LayoutManager
439: */
440: @Override
441: protected void validateTree() {
442: super .validateTree();
443: if (isVisible()) {
444: repositionFeedbackComponents();
445: }
446: }
447:
448: /**
449: * Gets notified when the ValidationResult changed and updates
450: * the feedback components.
451: */
452: private final class ValidationResultChangeHandler implements
453: PropertyChangeListener {
454:
455: public void propertyChange(PropertyChangeEvent evt) {
456: updateFeedbackComponents();
457: }
458:
459: }
460:
461: // Layout *****************************************************************
462:
463: /**
464: * Used to lay out the content layer in the icon feedback JLayeredPane.
465: * The content fills the parent's space; minimum and preferred size of
466: * this layout are requested from the content panel.
467: */
468: private final class SimpleLayout implements LayoutManager {
469:
470: /**
471: * If the layout manager uses a per-component string,
472: * adds the component <code>comp</code> to the layout,
473: * associating it
474: * with the string specified by <code>name</code>.
475: *
476: * @param name the string to be associated with the component
477: * @param comp the component to be added
478: */
479: public void addLayoutComponent(String name, Component comp) {
480: // components are well known by the container
481: }
482:
483: /**
484: * Removes the specified component from the layout.
485: * @param comp the component to be removed
486: */
487: public void removeLayoutComponent(Component comp) {
488: // components are well known by the container
489: }
490:
491: /**
492: * Calculates the preferred size dimensions for the specified
493: * container, given the components it contains.
494: *
495: * @param parent the container to be laid out
496: * @return the preferred size of the given container
497: * @see #minimumLayoutSize(Container)
498: */
499: public Dimension preferredLayoutSize(Container parent) {
500: return content.getPreferredSize();
501: }
502:
503: /**
504: * Calculates the minimum size dimensions for the specified
505: * container, given the components it contains.
506: *
507: * @param parent the component to be laid out
508: * @return the minimum size of the given container
509: * @see #preferredLayoutSize(Container)
510: */
511: public Dimension minimumLayoutSize(Container parent) {
512: return content.getMinimumSize();
513: }
514:
515: /**
516: * Lays out the specified container.
517: *
518: * @param parent the container to be laid out
519: */
520: public void layoutContainer(Container parent) {
521: Dimension size = parent.getSize();
522: content.setBounds(0, 0, size.width, size.height);
523: }
524:
525: }
526:
527: }
|