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

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

Cleaned up Display IO and the setCurrentFrame method - now anything can hook into frame changed events if they want to

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