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

Last change on this file since 919 was 919, checked in by jts21, 10 years ago

Added license headers to all files, added full GPL3 license file, moved license header generator script to dev/bin/scripts

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