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 }