View Javadoc

1   package com.melloware.jukes.gui.view.validation;
2   
3   import java.awt.Component;
4   import java.awt.Container;
5   import java.awt.Dimension;
6   import java.awt.LayoutManager;
7   import java.awt.Point;
8   import java.beans.PropertyChangeEvent;
9   import java.beans.PropertyChangeListener;
10  import java.util.Map;
11  
12  import javax.swing.Icon;
13  import javax.swing.JComboBox;
14  import javax.swing.JComponent;
15  import javax.swing.JLabel;
16  import javax.swing.JLayeredPane;
17  import javax.swing.JScrollPane;
18  import javax.swing.JViewport;
19  import javax.swing.text.JTextComponent;
20  
21  import com.jgoodies.validation.ValidationResult;
22  import com.jgoodies.validation.ValidationResultModel;
23  import com.jgoodies.validation.view.ValidationComponentUtils;
24  import com.jgoodies.validation.view.ValidationResultViewFactory;
25  
26  /**
27   * Can display validation feedback icons "over" a content panel. It observes a
28   * ValidationResultModel and creates icon labels in a feedback layer of a
29   * {@link JLayeredPane} on top of the content layer. To position the feedback
30   * labels, the content pane is traversed and searched for text components that
31   * match a validation message key in this panel's observed
32   * ValidationResultModel.
33   * <p>
34   * <strong>Note:</strong> This panel doesn't reserve space for the portion used
35   * to display the overlayed feedback components. It has been designed to not
36   * change the layout of the wrapped content. Therefore you must reserve this
37   * space, or in other words, you must ensure that the wrapped content provides
38   * enough space to display the overlayed components. Since the current
39   * implementation positions the overlay components in the lower left, just make
40   * sure that there are about 6 pixel to the left and bottom of the input
41   * components that can be marked.
42   * <p>
43   * This panel handles two event types:
44   * <ol>
45   * <li>the ValidationResultModel changes; in this case the set of visible
46   * feedback components shall mark the input components that match the new
47   * validation result. This is done by this class' internal
48   * <code>ValidationResultChangeHandler</code> which in turn invokes
49   * <code>#updateFeedbackComponents</code>.
50   * <li>the content layout changes; the feedback components must then be
51   * repositioned to reflect the position of the overlayed input components. This
52   * is done by overriding <code>#validateTree</code> and invoking
53   * <code>#repositionFeedBackComponents</code> after the child tree has been
54   * layed out. The current simple but expensive implementation updates all
55   * components.
56   * </ol>
57   * <p>
58   * <p>
59   * Copyright (c) 1999-2007 Melloware, Inc. <http://www.melloware.com>
60   * @author Emil A. Lefkof III <info@melloware.com>
61   * @author Karsten Lentzsch
62   * @author Martin Skopp
63   * @version 4.0
64   */
65  public final class IconFeedbackPanel extends JLayeredPane {
66  
67     private static final int CONTENT_LAYER = 1;
68     private static final int FEEDBACK_LAYER = 2;
69  
70     /**
71      * Refers to the content panel that holds the content components.
72      */
73     private final JComponent content;
74  
75     /**
76      * Holds the ValidationResult and reports changes in that result. Used to
77      * update the state of the feedback components.
78      */
79     private final ValidationResultModel model;
80  
81     /**
82      * Creates an IconFeedbackPanel on the given ValidationResultModel using the
83      * specified content panel.
84      * <p>
85      * <strong>Note:</strong> Typically you should wrap component trees with
86      * {@link #getWrappedComponentTree(ValidationResultModel, JComponent)}, not
87      * this constructor.
88      * <p>
89      * <strong>Note:</strong> You must not add or remove components from the
90      * content once this constructor has been invoked.
91      * @param model the ValidationResultModel to observe
92      * @param content the panel that contains the content components
93      * @throws NullPointerException if model or content is <code>null</code>.
94      */
95     public IconFeedbackPanel(ValidationResultModel model, JComponent content) {
96        if (model == null) {
97           throw new NullPointerException("The validation result model must not be null.");
98        }
99        if (content == null) {
100          throw new NullPointerException("The content must not be null.");
101       }
102 
103       this.model = model;
104       this.content = content;
105       setLayout(new SimpleLayout());
106       add(content, CONTENT_LAYER);
107       initEventHandling();
108    }
109 
110    /**
111     * Wraps the components in the given component tree with instances of
112     * IconFeedbackPanel where necessary. Such a wrapper is required for all
113     * JScrollPanes that contain multiple children and for the root - unless it's
114     * a JScrollPane with multiple children.
115     * @param root the root of the component tree to wrap
116     * @return the wrapped component tree
117     */
118    public static JComponent getWrappedComponentTree(ValidationResultModel model, JComponent root) {
119       wrapComponentTree(model, root);
120       return isScrollPaneWithUnmarkableView(root) ? root : new IconFeedbackPanel(model, root);
121    }
122 
123    /**
124     * Recursively descends the container tree and recomputes the layout for any
125     * subtrees marked as needing it (those marked as invalid). In addition to
126     * the superclass behavior, we reposition the feedback components after the
127     * child components have been validated.
128     * <p>
129     * We reposition the feedback components only, if this panel is visible; if
130     * it becomes visible, #validateTree will be invoked.
131     * @see Container#validateTree()
132     * @see #validate()
133     * @see #invalidate()
134     * @see #doLayout()
135     * @see Component#setVisible(boolean)
136     * @see LayoutManager
137     */
138    @Override
139    protected void validateTree() {
140       super.validateTree();
141       if (isVisible()) {
142          repositionFeedbackComponents();
143       }
144    }
145 
146    /**
147     * Returns the ValidationResult associated with the given component using the
148     * specified validation result key map.
149     * @param comp the component may be marked with a validation message key
150     * @param keyMap maps validation message keys to ValidationResults
151     * @return the ValidationResult associated with the given component as
152     *         provided by the specified validation key map
153     */
154    private static ValidationResult getAssociatedResult(JComponent comp, Map keyMap) {
155       Object[] messageKeys = ValidationComponentUtils.getMessageKeys(comp);
156       if ((messageKeys == null) || (messageKeys.length == 0)) {
157          return ValidationResult.EMPTY;
158       }
159       Object messageKey = messageKeys[0];
160       if ((messageKey == null) || (keyMap == null)) {
161          return ValidationResult.EMPTY;
162       }
163       ValidationResult result = (ValidationResult) keyMap.get(messageKey);
164       return (result == null) ? ValidationResult.EMPTY : result;
165    }
166 
167    /**
168     * Checks and answers if the given component can be marked or not.
169     * <p>
170     * @param component the component to be checked
171     * @return true if the given component can be marked, false if not
172     */
173    private static boolean isMarkable(Component component) {
174       return (component instanceof JTextComponent) || (component instanceof JComboBox);
175    }
176 
177    private static boolean isScrollPaneView(Component c) {
178       Container container = c.getParent();
179       Container containerParent = container.getParent();
180       return (container instanceof JViewport) && (containerParent instanceof JScrollPane);
181    }
182 
183    private static boolean isScrollPaneWithUnmarkableView(Component c) {
184       if (!(c instanceof JScrollPane)) {
185          return false;
186       }
187       JScrollPane scrollPane = (JScrollPane) c;
188       JViewport viewport = scrollPane.getViewport();
189       JComponent view = (JComponent) viewport.getView();
190       return !isMarkable(view);
191    }
192 
193    private static void wrapComponentTree(ValidationResultModel model, Container container) {
194       if (!(container instanceof JScrollPane)) {
195          int componentCount = container.getComponentCount();
196          for (int i = 0; i < componentCount; i++) {
197             Component child = container.getComponent(i);
198             if (child instanceof Container) {
199                wrapComponentTree(model, (Container) child);
200             }
201          }
202          return;
203       }
204       JScrollPane scrollPane = (JScrollPane) container;
205       JViewport viewport = scrollPane.getViewport();
206       JComponent view = (JComponent) viewport.getView();
207       if (isMarkable(view)) {
208          return;
209       }
210       // the view must not be an IconFeedbackPanel
211       Component wrappedView = new IconFeedbackPanel(model, view);
212       viewport.setView(wrappedView);
213       wrapComponentTree(model, view);
214    }
215 
216    /**
217     * Computes and returns the origin of the given feedback component using the
218     * content component's origin.
219     * <p>
220     * This implementation returns a JLabel. The validation result's severity is
221     * used to lookup the label's icon; the result's message text is set as the
222     * label's tooltip text.
223     * <p>
224     * @param feedbackComponent the component that overlays the content
225     * @param contentComponent the component to get overlayed feedback
226     * @return the feedback component's origin *
227     * @throws NullPointerException if the feedback component or content
228     *            component is <code>null</code>
229     */
230    private Point getFeedbackComponentOrigin(JComponent feedbackComponent, Component contentComponent) {
231       int x = contentComponent.getX() - (feedbackComponent.getWidth() / 2);
232       int y = contentComponent.getY() + contentComponent.getHeight() - feedbackComponent.getHeight() + 2;
233 
234       return new Point(x, y);
235    }
236 
237    private void addFeedbackComponent(Component contentComponent, JComponent messageComponent, Map keyMap, int xOffset,
238             int yOffset) {
239       ValidationResult result = getAssociatedResult(messageComponent, keyMap);
240       JComponent feedbackComponent = createFeedbackComponent(result, contentComponent);
241       if (feedbackComponent == null) {
242          return;
243       }
244       add(feedbackComponent, Integer.valueOf(FEEDBACK_LAYER));
245       Point overlayPosition = getFeedbackComponentOrigin(feedbackComponent, contentComponent);
246       overlayPosition.translate(xOffset, yOffset);
247       feedbackComponent.setLocation(overlayPosition);
248    }
249 
250    /**
251     * Creates and returns a validation feedback component that shall overlay the
252     * specified content component.
253     * <p>
254     * This implementation returns a JLabel. The validation result's severity is
255     * used to lookup the label's icon; the result's message text is set as the
256     * label's tooltip text.
257     * <p>
258     * @param result determines the label's icon and tooltip text
259     * @param contentComponent the component to get overlayed feedback
260     * @return the feedback component that overlays the content component
261     * @throws NullPointerException if the result is <code>null</code>
262     */
263    private JComponent createFeedbackComponent(ValidationResult result, Component contentComponent) { // NOPMD
264       Icon icon = ValidationResultViewFactory.getSmallIcon(result.getSeverity());
265       JLabel label = new JLabel(icon);
266       label.setToolTipText(result.getMessagesText());
267       label.setSize(label.getPreferredSize());
268       return label;
269    }
270 
271    /**
272     * Registers a listener with the validation result model that updates the
273     * feedback components.
274     */
275    private void initEventHandling() {
276       model.addPropertyChangeListener(ValidationResultModel.PROPERTYNAME_RESULT, new ValidationResultChangeHandler());
277    }
278 
279    private void removeAllFeedbackComponents() {
280       int componentCount = getComponentCount();
281       for (int i = componentCount - 1; i >= 0; i--) {
282          Component child = getComponent(i);
283          int layer = getLayer(child);
284          if (layer == FEEDBACK_LAYER) {
285             remove(i);
286          }
287       }
288    }
289 
290    /**
291     * Ensures that the feedback components are repositioned. Invoked by
292     * <code>#validate</code>, i. e. if this panel is layed out.
293     * <p>
294     */
295    private void repositionFeedbackComponents() {
296       updateFeedbackComponents();
297    }
298 
299    private void updateFeedbackComponents() {
300       removeAllFeedbackComponents();
301       visitComponentTree(content, model.getResult().keyMap(), 0, 0);
302       repaint();
303    }
304 
305    /**
306     * Traverses the component tree starting at the given container and creates a
307     * feedback component for each JTextComponent that is associated with a
308     * message in the specified <code>keyMap</code>.
309     * <p>
310     * The arguments passed to the feedback component creation method are the
311     * visited component and its associated validation subresult. This subresult
312     * is requested from the specified <code>keyMap</code> using the visited
313     * component's message key.
314     * @param container the component tree root
315     * @param keyMap maps messages keys to associated validation results
316     */
317    private void visitComponentTree(Container container, Map keyMap, int xOffset, 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          }
324          if (isMarkable(child)) {
325             if (isScrollPaneView(child)) {
326                Component containerParent = container.getParent();
327                addFeedbackComponent(containerParent, (JComponent) child, keyMap, xOffset - containerParent.getX(),
328                         yOffset - containerParent.getY());
329             } else {
330                addFeedbackComponent(child, (JComponent) child, keyMap, xOffset, yOffset);
331             }
332          } else if (child instanceof Container) {
333             visitComponentTree((Container) child, keyMap, xOffset + child.getX(), yOffset + child.getY());
334          }
335       }
336    }
337 
338    /**
339     * Used to lay out the content layer in the icon feedback JLayeredPane. The
340     * content fills the parent's space; minimum and preferred size of this
341     * layout are requested from the content panel.
342     */
343    private class SimpleLayout implements LayoutManager {
344 
345       /**
346        * If the layout manager uses a per-component string, adds the component
347        * <code>comp</code> to the layout, associating it with the string
348        * specified by <code>name</code>.
349        * @param name the string to be associated with the component
350        * @param comp the component to be added
351        */
352       public void addLayoutComponent(String name, Component comp) {
353          // components are well known by the container
354       }
355 
356       /**
357        * Lays out the specified container.
358        * @param parent the container to be laid out
359        */
360       public void layoutContainer(Container parent) {
361          Dimension size = parent.getSize();
362          content.setBounds(0, 0, size.width, size.height);
363       }
364 
365       /**
366        * Calculates the minimum size dimensions for the specified container,
367        * given the components it contains.
368        * @param parent the component to be laid out
369        * @return the minimum size of the given container
370        * @see #preferredLayoutSize(Container)
371        */
372       public Dimension minimumLayoutSize(Container parent) {
373          return content.getMinimumSize();
374       }
375 
376       /**
377        * Calculates the preferred size dimensions for the specified container,
378        * given the components it contains.
379        * @param parent the container to be laid out
380        * @return the preferred size of the given container
381        * @see #minimumLayoutSize(Container)
382        */
383       public Dimension preferredLayoutSize(Container parent) {
384          return content.getPreferredSize();
385       }
386 
387       /**
388        * Removes the specified component from the layout.
389        * @param comp the component to be removed
390        */
391       public void removeLayoutComponent(Component comp) {
392          // components are well known by the container
393       }
394 
395    }
396 
397    /**
398     * Gets notified when the ValidationResult changed and updates the feedback
399     * components.
400     */
401    private class ValidationResultChangeHandler implements PropertyChangeListener {
402 
403       public void propertyChange(PropertyChangeEvent evt) {
404          updateFeedbackComponents();
405       }
406 
407    }
408 
409 }