source: trunk/src/org/expeditee/gui/PopupManager.java@ 283

Last change on this file since 283 was 283, checked in by bjn8, 16 years ago

Popups can change border size and color

File size: 18.5 KB
Line 
1package org.expeditee.gui;
2
3import java.awt.BasicStroke;
4import java.awt.Color;
5import java.awt.Component;
6import java.awt.Graphics;
7import java.awt.Graphics2D;
8import java.awt.Point;
9import java.awt.Rectangle;
10import java.awt.Stroke;
11import java.awt.geom.Area;
12import java.util.HashMap;
13import java.util.HashSet;
14import java.util.LinkedList;
15
16import javax.swing.JLayeredPane;
17import javax.swing.SwingUtilities;
18
19/**
20 * A centralized container for all custom popups in expeditee.
21 *
22 * @author Brook Novak
23 */
24public final class PopupManager implements DisplayIOObserver {
25
26 /** Singleton */
27 private PopupManager() {}
28
29 /** Singleton */
30 private static PopupManager _instance = new PopupManager();
31
32 /**
33 * @return The singleton instance.
34 */
35 public static PopupManager getInstance() {
36 return _instance;
37 }
38
39 // popup->invoker
40 private HashMap<Popup, Component> _popups = new HashMap<Popup, Component>();
41 // quick ref to invokers
42 private HashSet<Component> _invokers = new HashSet<Component>();
43
44 private LinkedList<AnimatedPopup> _animatingPopups = new LinkedList<AnimatedPopup>();
45 private AnimationThread _animationThread = null;
46
47 private final int ANIMATION_DURATION = 180; // Tume its takes for a maximize . minimize to animate. In ms.
48 private final int ANIMATION_RATE = 30; // in ms
49
50 /**
51 * Determines whether a given point is over a popup.
52 *
53 * @param p
54 *
55 * @return True if p is over a popup
56 *
57 * @throws NullPointerException
58 * If p is null
59 */
60 public boolean isPointOverPopup(Point p) {
61 if (p == null) throw new NullPointerException("p");
62 for (Popup pp : _popups.keySet()) {
63 if (pp.getBounds().contains(p)) {
64 return true;
65 }
66 }
67
68 return false;
69 }
70
71 /**
72 * Tests a component to see if it is in invoker of an existing popup.
73 *
74 * @param c
75 * Must not be null.
76 *
77 * @return
78 * True if c is an invoker
79 *
80 * @throws NullPointerException
81 * If c is null
82 */
83 public boolean isInvoker(Component c) {
84 if (c == null) throw new NullPointerException("c");
85 return _invokers.contains(c);
86 }
87
88 /**
89 * Gets an invoker for a popup
90 *
91 * @param p
92 * The popup to get the invoker for.
93 *
94 * @return
95 * The invoker for the given popup.
96 * Null if popup does not exist.
97 */
98 public Component getInvoker(Popup p) {
99 if (p == null) throw new NullPointerException("p");
100 return _popups.get(p);
101 }
102
103 /**
104 * Use this instead of isVisible to determine if the popup is showing or not,
105 * since it considers animation.
106 *
107 * @return
108 * True if this popup is showing. <b>IMPORTANT:</b> This includes
109 * if the popup is not yet visible, but in an animation sequence for showing...
110 *
111 * @throws NullPointerException
112 * If p is null
113 */
114 public boolean isShowing(Popup p) {
115 if (p == null) throw new NullPointerException("p");
116 return _popups.containsKey(p);
117 }
118
119 /**
120 * @return
121 * True if a poup is showing. False otherwise.
122 */
123 public boolean isAnyPopupsShowing () {
124 return !_popups.isEmpty();
125 }
126
127 /**
128 * @return
129 * True if the mouse click event for going back a frame should be consumed
130 * Due to a popup requesting this event to be consumed currently showing.
131 */
132 public boolean shouldConsumeBackClick() {
133 for (Popup p : _popups.keySet()) {
134 if (p.shouldConsumeBackClick())
135 return true;
136 }
137
138 return false;
139 }
140
141 /**
142 * Clears all popups from the browser that are autohidden.
143 * Stops animations.
144 */
145 public void hideAutohidePopups() {
146
147
148 // Get rid of all animations that are not non-auto-hidden pops that are expanding
149 synchronized (_animatingPopups) {
150
151 LinkedList<AnimatedPopup> animationsToClear = new LinkedList<AnimatedPopup>();
152
153 for (AnimatedPopup ap : _animatingPopups) {
154 if (!(ap.popup != null && !ap.popup.doesAutoHide())) {
155 animationsToClear.add(ap);
156 }
157 }
158
159 _animatingPopups.removeAll(animationsToClear);
160
161 }
162
163 LinkedList<Popup> popupsToClear = new LinkedList<Popup>();
164 LinkedList<Component> invokersToClear = new LinkedList<Component>();
165
166 // Get ride of the actual popups
167 for (Popup p : _popups.keySet()) {
168 if (p.doesAutoHide()) {
169
170 popupsToClear.add(p);
171 invokersToClear.add(_popups.get(p));
172
173 p.invalidateAppearance();
174 p.setVisible(false);
175 Browser._theBrowser.getLayeredPane().remove(p);
176 p.onHide();
177 }
178 }
179
180 assert (popupsToClear.size() == invokersToClear.size());
181
182 for (int i = 0; i < popupsToClear.size(); i++) {
183 _popups.remove(popupsToClear.get(i));
184 _invokers.remove(invokersToClear.get(i));
185 }
186
187
188
189 }
190
191
192
193// public void hideAllPopups() {
194//
195// for (Popup p : _popups.keySet()) {
196// invalidatePopup(p);
197// p.setVisible(false);
198// Browser._theBrowser.getLayeredPane().remove(p);
199// p.onHide();
200// }
201// _popups.clear();
202// _invokers.clear();
203//
204// // Get rid of all animations
205// synchronized (_animatingPopups) {
206// _animatingPopups.clear();
207// }
208// }
209
210 public void frameChanged() {
211 // Remove any popups that are showing on the current frame
212 hideAutohidePopups();
213 }
214
215 /**
216 * Hides a popup - if not already hidden.
217 *
218 * @param p
219 * Must not be null.
220 *
221 * @throws NullPointerException
222 * If p is null
223 */
224 public void hidePopup(Popup p) {
225 if (p == null) throw new NullPointerException("p");
226
227 if (!isShowing(p) && (!p.isVisible() || p.getParent() == null)) return;
228
229
230 // Cancel any showing animations
231 synchronized (_animatingPopups) {
232 AnimatedPopup toRemove = null;
233 for (AnimatedPopup ap : _animatingPopups) {
234 if (ap.popup == p) {
235 toRemove = ap;
236 break;
237 }
238 }
239 if (toRemove != null)
240 _animatingPopups.remove(toRemove);
241 }
242
243 p.invalidateAppearance();
244 p.setVisible(false);
245 Browser._theBrowser.getLayeredPane().remove(p);
246 Component invoker = _popups.remove(p);
247 if (invoker != null) _invokers.remove(invoker);
248 p.onHide();
249
250 }
251
252 /**
253 * Hides a popup - with animation. - if not already hidden.
254 *
255 * @param p
256 * Must not be null.
257 * @param animator
258 * Must not be null.
259 *
260 * @throws NullPointerException
261 * If p or animator is null
262 *
263 */
264 public void hidePopup(Popup p, PopupAnimator animator) {
265
266 if (p == null) throw new NullPointerException("p");
267 if (animator == null) throw new NullPointerException("animator");
268
269 if (!isShowing(p) && (!p.isVisible() || p.getParent() == null)) return;
270
271 hidePopup(p);
272 AnimatedPopup ap = new AnimatedPopup(
273 animator,
274 System.currentTimeMillis(),
275 null,
276 false,
277 p.getLocation());
278
279 animator.starting(false, p.getBounds());
280
281 synchronized (_animatingPopups) {
282 _animatingPopups.add(ap);
283 }
284
285 if (_animationThread == null || !_animationThread.isAlive() || _animationThread.willDie) {
286 _animationThread = new AnimationThread();
287 _animationThread.start();
288 }
289 }
290
291
292
293 /**
294 * Displays a popup at a specific location.
295 *
296 * @param p
297 * Must not be null.
298 *
299 * @param invoker
300 * The component responsible for showing the popup. can be null.
301 * Used such that when invoker pressed, the popup will not auto hide.
302 *
303 * @param loc
304 * Must not be null.
305 *
306 * @throws NullPointerException
307 * If p or loc is null
308 *
309 */
310 public void showPopup(Popup p, Point loc, Component invoker) {
311 if (p == null) throw new NullPointerException("p");
312 if (loc == null) throw new NullPointerException("animator");
313
314
315 if (_popups.containsKey(p)) return;
316
317 p.prepareToPaint();
318 p.setLocation(loc);
319 p.setVisible(true);
320
321 Browser._theBrowser.getLayeredPane().add(p, JLayeredPane.POPUP_LAYER, 0);
322
323 _popups.put(p, invoker);
324 if (invoker != null) _invokers.add(invoker);
325
326 p.onShowing();
327 p.onShow();
328
329 // Invalidate the popup border
330 if (p.getBorderThickness() > 0.0f) {
331 p.invalidateAppearance();
332 }
333 }
334
335 /**
336 * Displays a popup at a specific location - with animation.
337 *
338 * @param p
339 * Must not be null.
340 *
341 * @param invoker
342 * The component responsible for showing the popup. can be null.
343 * Used such that when invoker pressed, the popup will not auto hide.
344 *
345 * @param loc
346 * Must not be null.
347 *
348 * @param animator
349 * Must not be null.
350 *
351 * @throws NullPointerException
352 * If p, animator or loc is null
353 */
354 public void showPopup(Popup p, Point loc, Component invoker, PopupAnimator animator) {
355 if (animator == null)
356 throw new NullPointerException("animator");
357 if (p == null)
358 throw new NullPointerException("p");
359 if (loc == null)
360 throw new NullPointerException("loc");
361
362 if (_popups.containsKey(p)) return;
363
364 _popups.put(p, invoker);
365 if (invoker != null) _invokers.add(invoker);
366
367
368 AnimatedPopup ap = new AnimatedPopup(
369 animator,
370 System.currentTimeMillis(),
371 p,
372 true,
373 loc);
374
375
376 animator.starting(true, new Rectangle(loc.x, loc.y, p.getWidth(), p.getHeight()));
377
378 p.onShowing();
379
380 synchronized (_animatingPopups) {
381 _animatingPopups.add(ap);
382 }
383
384 if (_animationThread == null || !_animationThread.isAlive() || _animationThread.willDie) {
385 _animationThread = new AnimationThread();
386 _animationThread.start();
387 }
388
389 }
390
391 /**
392 * Does a pure asynch animation with no popups involved.
393 *
394 * For example you may want to have an effect such that an item is expanding
395 * or moving into a new location on the screen.
396 *
397 * @param animator
398 * Must not be null.
399 *
400 * @param target
401 * Must not be null.
402 *
403 * @throws NullPointerException
404 * If animator or target is null
405 *
406 */
407 public void doPureAnimation(PopupAnimator animator, Rectangle target) {
408
409 if (animator == null)
410 throw new NullPointerException("animator");
411 if (target == null)
412 throw new NullPointerException("target");
413
414 AnimatedPopup ap = new AnimatedPopup(
415 animator,
416 System.currentTimeMillis(),
417 null,
418 false,
419 target.getLocation());
420
421
422 animator.starting(true, target);
423
424 synchronized (_animatingPopups) {
425 _animatingPopups.add(ap);
426 }
427
428 if (_animationThread == null || !_animationThread.isAlive() || _animationThread.willDie) {
429 _animationThread = new AnimationThread();
430 _animationThread.start();
431 }
432
433 }
434
435
436 /**
437 * Paints all popups in the browsers popup pane with the given graphics.
438 *
439 * @param g
440 * Where to paint to.
441 *
442 * @param clip
443 */
444 void paintLayeredPane(Graphics g, Area clip) {
445 if (Browser._theBrowser == null) return;
446
447 Component[] compsOnPopup = Browser._theBrowser.getLayeredPane().getComponentsInLayer(JLayeredPane.POPUP_LAYER);
448
449 for (int i = 0; i < compsOnPopup.length; i++) {
450 Component c = compsOnPopup[i];
451
452 Point p = c.getLocation();
453
454 if (clip == null || clip.intersects(c.getBounds())) {
455 g.translate(p.x, p.y);
456 c.paint(g);
457 g.translate(-p.x, -p.y);
458 }
459
460 }
461 }
462
463 /**
464 * Paints current popup animations to the expeditee browser content pane.
465 */
466 void paintAnimations() {
467
468 if (Browser._theBrowser == null
469 || Browser._theBrowser.g == null) return;
470
471 Graphics g = Browser._theBrowser.g;
472
473 synchronized (_animatingPopups) {
474
475 for (AnimatedPopup ap : _animatingPopups) {
476 ap.animator.paint(g);
477 }
478
479 }
480 }
481
482 /**
483 * Proccesses animation on a dedicated thread.
484 *
485 * @author Brook Novak
486 *
487 */
488 private class AnimationThread extends Thread {
489
490 private boolean willDie = false;
491
492 @Override
493 public void run() {
494
495
496 LinkedList<AnimatedPopup> finishedAnimations;
497
498 while (true) {
499
500 // Perform animation logic
501 finishedAnimations = animate();
502
503 // Check if finished all animations
504 if (finishedAnimations == null) return; // done
505
506 // Check for finalization of animation. That is, adding the popups to the layered pane
507 boolean needsFinalization = false;
508 for (AnimatedPopup ap : finishedAnimations) {
509 if (ap.isShowing) {
510 needsFinalization = true;
511 break;
512 }
513
514 // FInal invalidation
515 FrameGraphics.invalidateArea(ap.animator.getCurrentDrawingArea());
516
517 }
518
519 if (needsFinalization) {
520 SwingUtilities.invokeLater(new AnimationFinalizor(finishedAnimations));
521 // Will repaint when a popup becomes anchored...
522 } else {
523 FrameGraphics.requestRefresh(true);
524 }
525
526 // Limit animation rate
527 try {
528 sleep(ANIMATION_RATE);
529 } catch (InterruptedException e) {
530 e.printStackTrace();
531 }
532
533 }
534
535 }
536
537 /**
538 * Performs animations
539 * @return
540 */
541 private LinkedList<AnimatedPopup> animate() {
542
543 LinkedList<AnimatedPopup> finishedPopups = null;
544
545 synchronized (_animatingPopups) {
546
547 if (_animatingPopups.isEmpty()) {
548 willDie = true;
549 return null;
550 }
551
552 long currentTime = System.currentTimeMillis();
553
554 finishedPopups = new LinkedList<AnimatedPopup>();
555
556 for (AnimatedPopup ap : _animatingPopups) {
557
558 long duration = currentTime - ap.startTime;
559
560 if (duration >= ANIMATION_DURATION) { // check if complete
561
562 finishedPopups.add(ap);
563
564 } else {
565
566 float percent = ((float)duration / (float)ANIMATION_DURATION);
567 assert (percent >= 0.0f);
568 assert (percent < 1.0f);
569
570 Rectangle dirty = ap.animator.update(percent);
571 if (dirty != null)
572 FrameGraphics.invalidateArea(dirty);
573
574 }
575
576 }
577
578 _animatingPopups.removeAll(finishedPopups);
579
580 }
581
582 return finishedPopups;
583
584 }
585
586 /**
587 * Adds popups to layered pane
588 * @author Brook Novak
589 *
590 */
591 private class AnimationFinalizor implements Runnable {
592
593 private LinkedList<AnimatedPopup> finished;
594
595 AnimationFinalizor(LinkedList<AnimatedPopup> finished) {
596 this.finished = finished;
597 }
598
599 public void run() {
600
601 for (AnimatedPopup ap : finished) {
602
603 if (ap.isShowing && _popups.containsKey(ap.popup)) {
604
605 ap.popup.prepareToPaint();
606 ap.popup.setLocation(ap.popupLocation);
607 ap.popup.setVisible(true);
608
609 Browser._theBrowser.getLayeredPane().add(ap.popup, JLayeredPane.POPUP_LAYER, 0);
610
611 ap.popup.onShow();
612
613 // Invalidate the popup border
614 if (ap.popup.getBorderThickness() > 0.0f) {
615 ap.popup.invalidateAppearance();
616 }
617 }
618
619 }
620
621 }
622 }
623 }
624
625 private class AnimatedPopup {
626
627 PopupAnimator animator;
628 long startTime;
629 Popup popup = null;
630 boolean isShowing;
631 Point popupLocation;
632
633 public AnimatedPopup(PopupAnimator animator, long startTime, Popup popup,
634 boolean isShowing, Point popupLocation) {
635
636 assert(animator != null);
637 assert(popupLocation != null);
638
639 // Only have popup if showing
640 assert (!isShowing && popup == null || (isShowing && popup != null));
641
642
643 this.animator = animator;
644 this.startTime = startTime;
645 this.popup = popup;
646 this.isShowing = isShowing;
647 this.popupLocation = popupLocation;
648
649 }
650
651 }
652
653 /**
654 * Provides animations for a popup when hiding or when showing.
655 *
656 * Note that {@link PopupAnimator#paint(Graphics)} and {@link PopupAnimator#update(float)}
657 * will always be invoked at seperate times - so do not have to worry about thread-saefty.
658 *
659 * These should only be used once... one per popup at a time.
660 *
661 * @author Brook Novak
662 *
663 */
664 public interface PopupAnimator {
665
666 /**
667 * Invoked before showing. Any prepaations are done here.
668 *
669 * @param isShowing
670 * True if this animation will be for a popup that is showing.
671 * False if this animation will be for a popup that is hiding.
672 *
673 * @param popupBounds
674 * The location of the popup. I.E. where it is, or where it will be.
675 *
676 */
677 void starting(boolean isShowing, Rectangle popupBounds);
678
679 /**
680 *
681 * Called on an animation thread.
682 *
683 * @param percent
684 * The percent complete of the animations.
685 * Rangles from 0 to 1.
686 *
687 * @return dirty area that needs painting for last update... Null for no invalidation
688 *
689 *
690 */
691 Rectangle update(float percent);
692
693 /**
694 * Paints the animation - on the swing thread.
695 * Note that this is always on the content pane - not the expeditee frame buffer.
696 *
697 * @param g
698 *
699 */
700 void paint(Graphics g);
701
702
703 /**
704 *
705 * @return
706 * The area which the animation is drawn. Used for final invalidation. Null for no final invaliation
707 */
708 Rectangle getCurrentDrawingArea();
709
710 }
711
712
713 public class ExpandShrinkAnimator implements PopupAnimator {
714
715 private boolean isShowing;
716 private Rectangle popupBounds;
717 private Rectangle sourceRectangle;
718 private Rectangle currentRectangle;
719 private Color fillColor;
720
721 private final Stroke stroke = new BasicStroke(2.0f);
722
723 /**
724 *
725 * @param sourceRectangle
726 * @param fillColor
727 * The fill color of the animated rectangle. Null for no fill.
728 */
729 public ExpandShrinkAnimator(Rectangle sourceRectangle, Color fillColor) {
730 if (sourceRectangle == null) throw new NullPointerException("sourceRectangle");
731
732 this.fillColor = fillColor;
733 this.sourceRectangle = (Rectangle)sourceRectangle.clone();
734 this.currentRectangle = (Rectangle)sourceRectangle.clone();
735 }
736
737 public void paint(Graphics g) {
738
739 if (fillColor != null) {
740 g.setColor(fillColor);
741 g.fillRect(currentRectangle.x, currentRectangle.y, currentRectangle.width, currentRectangle.height);
742 }
743
744 g.setColor(Color.BLACK);
745 ((Graphics2D)g).setStroke(stroke);
746 g.drawRect(currentRectangle.x, currentRectangle.y, currentRectangle.width, currentRectangle.height);
747 }
748
749 public void starting(boolean isShowing, Rectangle popupBounds) {
750 this.isShowing = isShowing;
751 this.popupBounds = popupBounds;
752
753 if (isShowing) {
754 this.currentRectangle = (Rectangle)sourceRectangle.clone();
755 } else {
756 this.currentRectangle = (Rectangle)sourceRectangle.clone();
757 }
758
759 }
760
761 public Rectangle update(float percent) {
762
763 Rectangle oldBounds = currentRectangle;
764
765 if (!isShowing) { // if minimizing just reverse percent
766 percent = 1 - percent;
767 }
768
769 // update X
770 currentRectangle.x = sourceRectangle.x +
771 (int)((popupBounds.x - sourceRectangle.x) * percent);
772
773 // update Y
774 currentRectangle.y = sourceRectangle.y +
775 (int)((popupBounds.y - sourceRectangle.y) * percent);
776
777 // update width
778 currentRectangle.width = sourceRectangle.width +
779 (int)((popupBounds.width - sourceRectangle.width) * percent);
780
781 // update height
782 currentRectangle.height = sourceRectangle.height +
783 (int)((popupBounds.height - sourceRectangle.height) * percent);
784
785 int x = Math.min(oldBounds.x, currentRectangle.x);
786 int y = Math.min(oldBounds.y, currentRectangle.y);
787 int width = Math.min(oldBounds.x + oldBounds.width, currentRectangle.x + currentRectangle.width) - x;
788 int height = Math.min(oldBounds.y + oldBounds.height, currentRectangle.y + currentRectangle.height) - y;
789
790 return new Rectangle(x, y, width, height);
791
792 }
793
794 public Rectangle getCurrentDrawingArea() {
795 return currentRectangle;
796 }
797
798
799 }
800
801
802}
Note: See TracBrowser for help on using the repository browser.