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 }