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