001: package abbot.editor.widgets;
002:
003: import java.awt.*;
004: import java.awt.event.ComponentAdapter;
005: import java.awt.event.ComponentEvent;
006: import java.awt.event.HierarchyEvent;
007: import java.awt.event.HierarchyListener;
008: import java.awt.event.MouseEvent;
009: import java.awt.geom.AffineTransform;
010: import java.awt.geom.Area;
011: import java.beans.PropertyChangeEvent;
012: import java.beans.PropertyChangeListener;
013: import java.lang.reflect.Field;
014: import java.util.ArrayList;
015: import java.util.Iterator;
016: import java.util.List;
017: import javax.swing.JComponent;
018: import javax.swing.JLayeredPane;
019: import javax.swing.RootPaneContainer;
020: import javax.swing.SwingUtilities;
021: import javax.swing.border.LineBorder;
022:
023: /** Provide a method for consistently augmenting the appearance of a given
024: * component by painting something on it <i>after</i> the component itself
025: * gets painted. If not explicitly removed via {@link #dispose}, an instance
026: * of this object will live as long as its target component.<p>
027: * By default, the decorator matches the location and size of the decorated
028: * component, but the bounds can be adjusted by overriding
029: * {@link #getDecorationBounds()}. The {@link #synch()} method should be called
030: * whenever the bounds returned by {@link #getDecorationBounds()} would change.
031: * <p>
032: * The decoration is clipped to the bounds set on the decoration, which does
033: * not necessarily need to be the same as the decorated component's bounds. The
034: * decoration may extend beyond the decorated component bounds, or it may be
035: * reduced to a smaller region.
036: */
037: // NOTE: OSX 1.6 lacks hierarchy events that w32 sends on layer changes
038: // TODO: should probably do some locking on Component.getTreeLock()
039: // TODO: synch underlying cursor when decorator covers more than
040: // one component; the cursor should change if the decoration exceeds the
041: // component's bounds (add mouse listener?)
042: public abstract class AbstractComponentDecorator {
043: public static final Rectangle DEFAULT_BOUNDS = null;
044: public static final int TOP = 0;
045: // Disabled for now, since it doesn't work properly
046: private static final int BOTTOM = -1;
047: /** Account for the difference between the decorator actual origin
048: * and the logical origin we want to pass to the {@link #paint} method.
049: */
050: private Point originOffset = new Point(0, 0);
051:
052: private Painter painter;
053: private JComponent component;
054: private Container parent;
055: private Component layerRoot;
056: private Listener listener;
057: private int layerOffset;
058: private int position;
059: private Rectangle bounds;
060:
061: /** Create a decorator for the given component. */
062: public AbstractComponentDecorator(JComponent c) {
063: this (c, 1);
064: }
065:
066: /** Create a decorator for the given component, indicating the layer
067: * offset from the target component. Negative values mean the decoration
068: * is painted <em>before</em> the target component is painted.
069: */
070: public AbstractComponentDecorator(JComponent c, int layerOffset) {
071: this (c, layerOffset, TOP);
072: }
073:
074: /** Create a decorator with the given position within its layer.
075: * Use {@link #TOP} to cover other decorations, or {@link #BOTTOM}
076: * to be covered by other decorations.<p>
077: * WARNING: BOTTOM doesn't currently work, probably a JLayeredPane bug
078: * in either the code or documentation.
079: * @see JLayeredPane
080: */
081: public AbstractComponentDecorator(JComponent c, int layerOffset,
082: int position) {
083: component = c;
084: this .layerOffset = layerOffset;
085: this .position = position;
086: this .bounds = DEFAULT_BOUNDS;
087: parent = c.getParent();
088: painter = new Painter();
089: listener = new Listener();
090: component.addHierarchyListener(listener);
091: component.addComponentListener(listener);
092: attach();
093: }
094:
095: /** Set the text to be displayed when the mouse is over the decoration.
096: * @see JComponent#setToolTipText(String)
097: */
098: public void setToolTipText(String text) {
099: painter.setToolTipText(text);
100: }
101:
102: /** Return the currently set default tooltip text.
103: * @see JComponent#setToolTipText
104: */
105: public String getToolTipText() {
106: return painter.getToolTipText();
107: }
108:
109: /** Provide for different tool tips depending on the actual location
110: * over the decoration. Note that if you <em>only</em> override this
111: * method, you must also invoke {@link #setToolTipText(String)} with
112: * a non-<span class="javakeyword">null</span> argument.
113: * @see JComponent#getToolTipText(MouseEvent)
114: */
115: public String getToolTipText(MouseEvent e) {
116: return getToolTipText();
117: }
118:
119: /** Indicate whether the decoration is showing. Its painter must be
120: * visible and showing (showing depends on window ancestor).
121: */
122: public boolean isShowing() {
123: return painter.isShowing();
124: }
125:
126: /** Indicate whether any of the decoration is visible. The decoration
127: * may be clipped by ancestor scroll panes or by being moved outside
128: * if the visible region of its parent window.
129: */
130: public boolean isVisible() {
131: return painter.isVisible();
132: }
133:
134: /** Use this to change the visibility of the decoration. */
135: public void setVisible(boolean visible) {
136: painter.setVisible(visible);
137: }
138:
139: protected void attach() {
140: if (layerRoot != null) {
141: layerRoot.removePropertyChangeListener(listener);
142: layerRoot = null;
143: }
144: RootPaneContainer rpc = (RootPaneContainer) SwingUtilities
145: .getAncestorOfClass(RootPaneContainer.class, component);
146: if (rpc != null
147: && SwingUtilities.isDescendingFrom(component, rpc
148: .getLayeredPane())) {
149: JLayeredPane lp = rpc.getLayeredPane();
150: Component layeredChild = component;
151: int layer = JLayeredPane.DRAG_LAYER.intValue();
152: if (this instanceof BackgroundPainter) {
153: layer = ((BackgroundPainter) this ).layer;
154: painter.setDecoratedLayer(layer);
155: } else if (layeredChild == lp) {
156: // Is the drag layer the best layer to use when decorating
157: // the layered pane?
158: painter.setDecoratedLayer(layer);
159: } else {
160: while (layeredChild.getParent() != lp) {
161: layeredChild = layeredChild.getParent();
162: }
163: int base = lp.getLayer(layeredChild);
164: // NOTE: JLayeredPane doesn't properly repaint an overlapping
165: // child when an obscured child calls repaint() if the two
166: // are in the same layer, so we use the next-higher layer
167: // instead of simply using a different position within the
168: // layer.
169: layer = base + layerOffset;
170: if (layerOffset < 0) {
171: BackgroundPainter bp = (BackgroundPainter) lp
172: .getClientProperty(BackgroundPainter
173: .key(base));
174: if (bp == null) {
175: bp = new BackgroundPainter(lp, base);
176: }
177: }
178: painter.setDecoratedLayer(base);
179: layerRoot = layeredChild;
180: layerRoot.addPropertyChangeListener(listener);
181: }
182: lp.add(painter, new Integer(layer), position);
183: } else {
184: // Always detach when the target component's window is null
185: // or is not a suitable container,
186: // otherwise we might prevent GC of the component
187: Container parent = painter.getParent();
188: if (parent != null) {
189: parent.remove(painter);
190: }
191: }
192: // Track size changes in the decorated component's parent
193: if (parent != null) {
194: parent.removeComponentListener(listener);
195: }
196: parent = component.getParent();
197: if (parent != null) {
198: parent.addComponentListener(listener);
199: }
200: synch();
201: }
202:
203: /** Ensure the size of the decorator matches the current
204: * decoration bounds with appropriate clipping to viewports.
205: */
206: protected void synch() {
207: Container painterParent = painter.getParent();
208: if (painterParent != null) {
209: Rectangle decorated = getDecorationBounds();
210: Rectangle clipRect = clipDecorationBounds(decorated);
211:
212: Point pt = SwingUtilities.convertPoint(component,
213: clipRect.x, clipRect.y, painterParent);
214: if (clipRect.width <= 0 || clipRect.height <= 0) {
215: setPainterBounds(-1, -1, 0, 0);
216: setVisible(false);
217: } else {
218: setPainterBounds(pt.x, pt.y, clipRect.width,
219: clipRect.height);
220: setVisible(true);
221: }
222: painterParent.repaint();
223: }
224: }
225:
226: /** Adjust the painting offsets and size of the decoration to
227: * account for ancestor clipping. This might be due to scroll panes
228: * or having the decoration lie outside the parent layered pane.
229: */
230: protected Rectangle clipDecorationBounds(Rectangle decorated) {
231: // Amount we have to translate the Graphics context
232: originOffset.x = decorated.x;
233: originOffset.y = decorated.y;
234: // If the the component is obscured (by a viewport or some
235: // other means), use the painter bounds to clip to the visible
236: // bounds. Doing may change the actual origin, so adjust our
237: // origin offset accordingly
238: Rectangle visible = getClippingRect(component, decorated);
239: Rectangle clipRect = decorated.intersection(visible);
240: if (decorated.x < visible.x)
241: originOffset.x += visible.x - decorated.x;
242: if (decorated.y < visible.y)
243: originOffset.y += visible.y - decorated.y;
244: return clipRect;
245: }
246:
247: /** Return any clipping rectangle detected above the given component,
248: * in the coordinate space of the given component. The given rectangle
249: * is desired to be visible.
250: */
251: private Rectangle getClippingRect(Container component,
252: Rectangle desired) {
253: Rectangle visible = component instanceof JComponent ? ((JComponent) component)
254: .getVisibleRect()
255: : new Rectangle(0, 0, component.getWidth(), component
256: .getHeight());
257: Rectangle clip = new Rectangle(desired);
258: if (desired.x >= visible.x
259: && desired.y >= visible.y
260: && desired.x + desired.width <= visible.x
261: + visible.width
262: && desired.y + desired.height <= visible.y
263: + visible.height) {
264: // desired rect is within the current clip rect
265: } else if (component.getParent() != null) {
266: // Only apply the clip if it is actually smaller than the
267: // component's visible area
268: if (component != painter.getParent()
269: && (visible.x > 0 || visible.y > 0
270: || visible.width < component.getWidth() || visible.height < component
271: .getHeight())) {
272: // Don't alter the original rectangle
273: desired = new Rectangle(desired);
274: desired.x = Math.max(desired.x, visible.x);
275: desired.y = Math.max(desired.y, visible.y);
276: desired.width = Math.min(desired.width, visible.x
277: + visible.width - desired.x);
278: desired.height = Math.min(desired.height, visible.y
279: + visible.height - desired.y);
280:
281: // Check for clipping further up the hierarchy
282: desired.x += component.getX();
283: desired.y += component.getY();
284: clip = getClippingRect(component.getParent(), desired);
285: clip.x -= component.getX();
286: clip.y -= component.getY();
287: }
288: }
289: return clip;
290: }
291:
292: /** Return the bounds, relative to the decorated component, of the
293: * decoration. The default covers the entire component. Note that
294: * this method will be called from the constructor, so be careful
295: * when overriding and referencing derived class state.
296: */
297: protected Rectangle getDecorationBounds() {
298: return bounds != DEFAULT_BOUNDS ? bounds : new Rectangle(0, 0,
299: component.getWidth(), component.getHeight());
300: }
301:
302: /** Change the bounds of the decoration, relative to the decorated
303: * component. The special value {@link #DEFAULT_BOUNDS} means the bounds
304: * will track the component bounds.
305: */
306: public void setDecorationBounds(Rectangle bounds) {
307: if (bounds == DEFAULT_BOUNDS) {
308: this .bounds = bounds;
309: } else {
310: this .bounds = new Rectangle(bounds);
311: }
312: synch();
313: }
314:
315: /** Change the bounds of the decoration, relative to the decorated
316: * component.
317: */
318: public void setDecorationBounds(int x, int y, int w, int h) {
319: setDecorationBounds(new Rectangle(x, y, w, h));
320: }
321:
322: protected void setPainterBounds(int x, int y, int w, int h) {
323: painter.setLocation(x, y);
324: painter.setSize(w, h);
325: repaint();
326: }
327:
328: /** Returns the decorated component. */
329: protected JComponent getComponent() {
330: return component;
331: }
332:
333: /** Returns the component used to paint the decoration and optionally
334: * track events.
335: */
336: protected JComponent getPainter() {
337: return painter;
338: }
339:
340: /** Set the cursor to appear anywhere over the decoration bounds.
341: * If null, the cursor of the decorated component will be used.
342: */
343: public void setCursor(Cursor cursor) {
344: painter.setCursor(cursor);
345: }
346:
347: /** Force a refresh of the underlying component and its decoration. */
348: public void repaint() {
349: JLayeredPane p = (JLayeredPane) painter.getParent();
350: if (p != null) {
351: p.repaint(painter.getBounds());
352: }
353: }
354:
355: /** Stop decorating. */
356: public void dispose() {
357: if (component == null)
358: return;
359:
360: // Disposal must occur on the EDT
361: if (!SwingUtilities.isEventDispatchThread()) {
362: SwingUtilities.invokeLater(new Runnable() {
363: public void run() {
364: dispose();
365: }
366: });
367: return;
368: }
369: component.removeHierarchyListener(listener);
370: component.removeComponentListener(listener);
371: if (parent != null) {
372: parent.removeComponentListener(listener);
373: parent = null;
374: }
375: if (layerRoot != null) {
376: layerRoot.removePropertyChangeListener(listener);
377: layerRoot = null;
378: }
379: Container painterParent = painter.getParent();
380: if (painterParent != null) {
381: Rectangle bounds = painter.getBounds();
382: painterParent.remove(painter);
383: painterParent.repaint(bounds.x, bounds.y, bounds.width,
384: bounds.height);
385: }
386: component.repaint();
387: component = null;
388: }
389:
390: /** Define the decoration's appearance. The point (0,0) represents
391: * the upper left corner of the decorated component.
392: * The default clip mask will be the extents of the decoration bounds, as
393: * indicated by {@link #getDecorationBounds()}, which defaults to the
394: * decorated component bounds.
395: */
396: public abstract void paint(Graphics g);
397:
398: public String toString() {
399: return super .toString() + " on " + getComponent();
400: }
401:
402: private static Field nComponents;
403: static {
404: try {
405: nComponents = Container.class
406: .getDeclaredField("ncomponents");
407: nComponents.setAccessible(true);
408: } catch (Exception e) {
409: nComponents = null;
410: }
411: }
412:
413: private static boolean useSimpleBackground() {
414: return nComponents == null;
415: }
416:
417: /** Used to hook into the Swing painting and event architecture. */
418: protected class Painter extends JComponent {
419: private int base;
420: private Cursor cursor;
421: {
422: setFocusable(false);
423: }
424:
425: public JComponent getComponent() {
426: return AbstractComponentDecorator.this .getComponent();
427: }
428:
429: public void setDecoratedLayer(int base) {
430: this .base = base;
431: }
432:
433: public int getDecoratedLayer() {
434: return base;
435: }
436:
437: public boolean isBackgroundDecoration() {
438: return layerOffset < 0;
439: }
440:
441: /** Set the cursor to something else. If null, the cursor of
442: * the decorated component will be used.
443: */
444: public void setCursor(Cursor cursor) {
445: Cursor oldCursor = getCursor();
446: // Make sure the cursor actually changed, otherwise
447: // we get cursor flicker (notably on w32 title bars)
448: if (oldCursor == null && cursor != null
449: || oldCursor != null && !oldCursor.equals(cursor)) {
450: this .cursor = cursor;
451: super .setCursor(cursor);
452: }
453: }
454:
455: /** Returns the cursor of the decorated component, or the last
456: * cursor set by {@link #setCursor}.
457: */
458: public Cursor getCursor() {
459: return cursor != null ? cursor : component.getCursor();
460: }
461:
462: /** Delegate to the containing decorator to perform the paint.
463: */
464: public void paintComponent(Graphics g) {
465: if (!component.isShowing())
466: return;
467: Graphics g2 = g.create();
468: g2.translate(-originOffset.x, -originOffset.y);
469: AbstractComponentDecorator.this .paint(g2);
470: g2.dispose();
471: }
472:
473: /** Provide a decorator-specific tooltip, shown when within the
474: * decorator's bounds.
475: */
476: public String getToolTipText(MouseEvent e) {
477: return AbstractComponentDecorator.this .getToolTipText(e);
478: }
479:
480: public String toString() {
481: return "Painter for " + AbstractComponentDecorator.this ;
482: }
483: }
484:
485: /** Provides a shared background painting mechanism for multiple
486: * decorations. This ensures that the background is only painted once
487: * if more than one background decorator is applied.
488: */
489: private static class BackgroundPainter extends
490: AbstractComponentDecorator {
491: private static String key(int layer) {
492: return "backgroundPainter for layer " + layer;
493: }
494:
495: private String key;
496: private int layer;
497:
498: public BackgroundPainter(JLayeredPane p, int layer) {
499: super (p, 0, TOP);
500: this .layer = layer;
501: key = key(layer);
502: p.putClientProperty(key, this );
503: }
504:
505: private int hideChildren(Container c) {
506: if (c == null)
507: return 0;
508: int value = c.getComponentCount();
509: try {
510: nComponents.set(c, new Integer(0));
511: } catch (Exception e) {
512: return c.getComponentCount();
513: }
514: return value;
515: }
516:
517: private void restoreChildren(Container c, int count) {
518: if (c != null) {
519: try {
520: nComponents.set(c, new Integer(count));
521: } catch (Exception e) {
522: }
523: }
524: }
525:
526: private void paintBackground(Graphics g, Component parent,
527: JComponent jc) {
528: int x = jc.getX();
529: int y = jc.getY();
530: int w = jc.getWidth();
531: int h = jc.getHeight();
532: paintBackground(g.create(x, y, w, h), jc);
533: }
534:
535: private void paintBackground(Graphics g, JComponent jc) {
536: if (jc.isOpaque()) {
537: if (useSimpleBackground()) {
538: g.setColor(jc.getBackground());
539: g.fillRect(0, 0, jc.getWidth(), jc.getHeight());
540: } else {
541: int count = hideChildren(jc);
542: boolean db = jc.isDoubleBuffered();
543: if (db)
544: jc.setDoubleBuffered(false);
545: jc.paint(g);
546: if (db)
547: jc.setDoubleBuffered(true);
548: restoreChildren(jc, count);
549: }
550: }
551: Component[] kids = jc.getComponents();
552: for (int i = 0; i < kids.length; i++) {
553: if (kids[i] instanceof JComponent) {
554: paintBackground(g, jc, (JComponent) kids[i]);
555: }
556: }
557: }
558:
559: private List findOpaque(Component root) {
560: List list = new ArrayList();
561: if (root.isOpaque() && root instanceof JComponent) {
562: list.add(root);
563: ((JComponent) root).setOpaque(false);
564: }
565: if (root instanceof Container) {
566: Component[] kids = ((Container) root).getComponents();
567: for (int i = 0; i < kids.length; i++) {
568: list.addAll(findOpaque(kids[i]));
569: }
570: }
571: return list;
572: }
573:
574: private List findDoubleBuffered(Component root) {
575: List list = new ArrayList();
576: if (root.isDoubleBuffered() && root instanceof JComponent) {
577: list.add(root);
578: ((JComponent) root).setDoubleBuffered(false);
579: }
580: if (root instanceof Container) {
581: Component[] kids = ((Container) root).getComponents();
582: for (int i = 0; i < kids.length; i++) {
583: list.addAll(findDoubleBuffered(kids[i]));
584: }
585: }
586: return list;
587: }
588:
589: private void paintForeground(Graphics g, JComponent jc) {
590: List opaque = findOpaque(jc);
591: List db = findDoubleBuffered(jc);
592: jc.paint(g);
593: for (Iterator i = opaque.iterator(); i.hasNext();) {
594: ((JComponent) i.next()).setOpaque(true);
595: }
596: for (Iterator i = db.iterator(); i.hasNext();) {
597: ((JComponent) i.next()).setDoubleBuffered(true);
598: }
599: }
600:
601: /** Walk the list of "background" decorators and paint them. */
602: public void paint(Graphics g) {
603:
604: JLayeredPane lp = (JLayeredPane) getComponent();
605: Component[] kids = lp.getComponents();
606: // Construct an area of the intersection of all decorators
607: Area area = new Area();
608: List painters = new ArrayList();
609: List components = new ArrayList();
610: for (int i = kids.length - 1; i >= 0; i--) {
611: if (kids[i] instanceof Painter) {
612: Painter p = (Painter) kids[i];
613: if (p.isBackgroundDecoration()
614: && p.getDecoratedLayer() == layer
615: && p.isShowing()) {
616: painters.add(p);
617: area.add(new Area(p.getBounds()));
618: }
619: } else if (lp.getLayer(kids[i]) == layer
620: && kids[i] instanceof JComponent) {
621: components.add(kids[i]);
622: }
623: }
624: if (painters.size() == 0) {
625: dispose();
626: return;
627: }
628: g.setClip(area);
629:
630: // Paint background for that area
631: for (Iterator i = components.iterator(); i.hasNext();) {
632: JComponent c = (JComponent) i.next();
633: paintBackground(g, lp, c);
634: }
635:
636: // Paint the bg decorators
637: for (Iterator i = painters.iterator(); i.hasNext();) {
638: Painter p = (Painter) i.next();
639: p.paint(g.create(p.getX(), p.getY(), p.getWidth(), p
640: .getHeight()));
641: }
642: // Paint foreground for the area
643: for (Iterator i = components.iterator(); i.hasNext();) {
644: JComponent c = (JComponent) i.next();
645: paintForeground(g.create(c.getX(), c.getY(), c
646: .getWidth(), c.getHeight()), c);
647: }
648: }
649:
650: public void dispose() {
651: getComponent().putClientProperty(key, null);
652: super .dispose();
653: }
654:
655: public String toString() {
656: return key + " on " + getComponent();
657: }
658: }
659:
660: /** Tracks changes to component configuration. */
661: private final class Listener extends ComponentAdapter implements
662: HierarchyListener, PropertyChangeListener {
663: // NOTE: OSX (1.6) doesn't generate these the same as w32
664: public void hierarchyChanged(HierarchyEvent e) {
665: if ((e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) != 0) {
666: attach();
667: }
668: }
669:
670: public void propertyChange(PropertyChangeEvent e) {
671: if (JLayeredPane.LAYER_PROPERTY.equals(e.getPropertyName())) {
672: attach();
673: }
674: }
675:
676: public void componentMoved(ComponentEvent e) {
677: // FIXME figure out why attach works and synch doesn't
678: // when painting a selection marquee over a decorated background
679: attach();
680: }
681:
682: public void componentResized(ComponentEvent e) {
683: // FIXME figure out why attach works and synch doesn't
684: // when painting a selection marquee over a decorated background
685: attach();
686: }
687:
688: public void componentHidden(ComponentEvent e) {
689: setVisible(false);
690: }
691:
692: public void componentShown(ComponentEvent e) {
693: setVisible(true);
694: }
695: }
696: }
|