1   /****************************************************************************
2    **
3    ** This file is part of yFiles-2.9. 
4    ** 
5    ** yWorks proprietary/confidential. Use is subject to license terms.
6    **
7    ** Redistribution of this file or of an unauthorized byte-code version
8    ** of this file is strictly forbidden.
9    **
10   ** Copyright (c) 2000-2011 by yWorks GmbH, Vor dem Kreuzberg 28, 
11   ** 72070 Tuebingen, Germany. All rights reserved.
12   **
13   ***************************************************************************/
14  package demo.view.orgchart;
15  
16  import java.awt.Rectangle;
17  import java.awt.event.ActionEvent;
18  import java.awt.event.ActionListener;
19  import java.awt.event.KeyAdapter;
20  import java.awt.event.KeyEvent;
21  import java.awt.event.KeyListener;
22  import java.awt.geom.Point2D;
23  
24  import javax.swing.Timer;
25  
26  import y.anim.AnimationObject;
27  import y.anim.AnimationPlayer;
28  import y.util.DefaultMutableValue2D;
29  import y.view.Graph2DView;
30  import y.view.ViewAnimationFactory;
31  
32  /**
33   * Factory class that provides {@link java.awt.event.KeyListener} implementations suitable for
34   * navigating within Graph2DView.
35   */
36  public class KeyboardNavigation {
37  
38    private Graph2DView view;
39    
40    /**
41     * Creates a KeyboardNavigation for the given view.
42     * @param view
43     */
44    public KeyboardNavigation(Graph2DView view) {
45      this.view = view;
46    }
47    
48    /**
49     * Creates and returns a KeyListener that zooms into the view
50     * while keyCode1 or keyCode2 is being pressed.
51     */
52    public KeyListener createZoomInKeyListener(final int keyCode1, final int keyCode2 ) 
53    {
54      return new ZoomTrigger(true, keyCode1, keyCode2);
55    }
56  
57  
58    /**
59     * Creates and returns a KeyListener that zooms out of the view
60     * while keyCode1 or keyCode2 is being pressed.
61     */
62    public KeyListener createZoomOutKeyListener(final int keyCode1, final int keyCode2 ) 
63    {
64      return new ZoomTrigger(false, keyCode1, keyCode2);
65    }
66  
67    /**
68     * Creates and returns a KeyListener that moves the view port up
69     * while keyCode is being pressed.
70     */
71    public KeyListener createMoveViewportUpKeyListener(final int keyCode) 
72    {
73      return new MoveViewportTrigger(MoveViewportTrigger.UP, keyCode);        
74    }
75  
76    /**
77     * Creates and returns a KeyListener that moves the view port down
78     * while keyCode is being pressed.
79     */
80    public KeyListener createMoveViewportDownKeyListener(final int keyCode) 
81    {
82      return new MoveViewportTrigger(MoveViewportTrigger.DOWN, keyCode);
83    }
84    
85  
86    /**
87     * Creates and returns a KeyListener that moves the view port to the left
88     * while keyCode is being pressed.
89     */
90    public KeyListener createMoveViewportLeftKeyListener(final int keyCode) 
91    {
92      return new MoveViewportTrigger(MoveViewportTrigger.LEFT, keyCode);
93    }
94  
95    /**
96     * Creates and returns a KeyListener that moves the view port to the right
97     * while keyCode is being pressed.
98     */
99    public KeyListener createMoveViewportRightKeyListener(final int keyCode) 
100   {
101     return new MoveViewportTrigger(MoveViewportTrigger.RIGHT, keyCode);
102   }
103   
104   /**
105    * KeyListener base used for all implementation provided by this class.
106    * It uses the yFiles animation framework to perform smooth navigation
107    * effects.
108    */
109   private abstract class AnimationTrigger extends KeyAdapter {
110     private AnimationPlayer player;
111     private DisarmableAnimationWrapper wrapper;
112 
113     private final int keyCode1;
114     private final int keyCode2;
115     private final Timer timer;
116 
117     AnimationTrigger( final int keyCode1, final int keyCode2  ) {
118       this.keyCode1 = keyCode1;
119       this.keyCode2 = keyCode2;
120       this.timer = new Timer(0, new ActionListener() {
121         public void actionPerformed( final ActionEvent e ) {
122           if (player != null && player.isPlaying()) {
123             wrapper.disarm();
124             player.stop();
125           }
126         }
127       });
128       this.timer.setInitialDelay(50);
129       this.timer.setRepeats(false);
130     }
131 
132     public void keyPressed( final KeyEvent e ) {
133       if (player == null) {
134         player = new AnimationPlayer(false);
135         player.addAnimationListener(view);
136       }
137 
138       final int keyCode = e.getKeyCode();
139       if ((keyCode1 == keyCode || keyCode2 == keyCode) &&
140           (0 == e.getModifiersEx())) {
141         timer.stop();
142         if (!player.isPlaying()) {
143           wrapper = new DisarmableAnimationWrapper(createAnimation());
144           if (wrapper.preferredDuration() > 0) {
145             player.animate(wrapper);
146           }
147         }
148       }
149     }
150 
151     public void keyReleased( final KeyEvent e ) {
152       final int keyCode = e.getKeyCode();
153       if ((keyCode1 == keyCode || keyCode2 == keyCode) &&
154           (0 == e.getModifiersEx())) {
155         if (!timer.isRunning()) {
156           timer.restart();
157         }
158       }
159     }
160 
161     abstract AnimationObject createAnimation();
162   }
163 
164   /**
165    * KeyListener implementation that performs zooming
166    */
167   class ZoomTrigger extends AnimationTrigger {
168     private ViewAnimationFactory factory;
169     private final boolean zoomIn;
170 
171     ZoomTrigger( final boolean zoomIn, final int keyCode1, final int keyCode2 ) {
172       super(keyCode1, keyCode2);
173       this.zoomIn = zoomIn;
174     }
175 
176     AnimationObject createAnimation() {
177       final double newZoom = calculateZoom();
178       if ((zoomIn && newZoom > view.getZoom()) || (!zoomIn && newZoom < view.getZoom())) {
179         if (factory == null) {
180           factory = new ViewAnimationFactory(view);
181         }
182         return factory.zoom(newZoom, ViewAnimationFactory.APPLY_EFFECT, 1000);
183       } else {
184         return null;
185       }
186     }
187 
188     private double calculateZoom() {
189       if (zoomIn) {
190         return 4;
191       } else {
192         Point2D oldP = view.getViewPoint2D();
193         double oldZoom = view.getZoom();
194         view.fitContent();
195         double fitContentZoom = view.getZoom();
196         view.setZoom(oldZoom);
197         view.setViewPoint2D(oldP.getX(), oldP.getY());
198         return fitContentZoom;
199       }
200     }
201   }
202 
203 
204   /**
205    * KeyListener implementation that moves the view port of the view
206    */
207   private class MoveViewportTrigger extends AnimationTrigger {
208     static final int LEFT = 0;
209     static final int RIGHT = 1;
210     static final int UP = 2;
211     static final int DOWN = 3;
212 
213     private int direction;
214     private ViewAnimationFactory factory;
215 
216     MoveViewportTrigger( final int direction, final int keyCode ) {
217       super(keyCode, -1);
218       this.direction = direction;
219     }
220 
221     AnimationObject createAnimation() {
222       final Rectangle bx = view.getGraph2D().getBoundingBox();
223       final double dx = Math.max(bx.getWidth(), 10000);
224       final double dy = Math.max(bx.getHeight(), 10000);
225 
226       final Point2D oldCenter = view.getCenter();
227       final DefaultMutableValue2D newCenter =
228               DefaultMutableValue2D.create(oldCenter.getX(), oldCenter.getY());
229 
230       double dist = 0;
231       switch (direction) {
232         case LEFT:
233           newCenter.setX(bx.getX() - dx);
234           dist = oldCenter.getX() - newCenter.getX();
235           break;
236         case RIGHT:
237           newCenter.setX(bx.getX() + bx.getWidth() + dx);
238           dist = newCenter.getX() - oldCenter.getX();
239           break;
240         case UP:
241           newCenter.setY(bx.getY() - dy);
242           dist = oldCenter.getY() - newCenter.getY();
243           break;
244         case DOWN:
245           newCenter.setY(bx.getY() + bx.getHeight() + dy);
246           dist = newCenter.getY() - oldCenter.getY();
247           break;
248       }
249 
250       if (dist > 1e-4) {
251         if (factory == null) {
252           factory = new ViewAnimationFactory(view);
253         }
254         return factory.moveCamera(newCenter, (long) Math.rint(dist * view.getZoom()));
255       } else {
256         return null;
257       }
258     }
259   }
260   
261   /**
262    * Animation object wrapper that can be used to disarm an AnimationObject during execution.
263    */
264   private static final class DisarmableAnimationWrapper implements AnimationObject {
265     private AnimationObject animation;
266 
267     private boolean armed;
268 
269     DisarmableAnimationWrapper( final AnimationObject animation ) {
270       this.animation = animation;
271       this.armed = true;
272     }
273 
274     public void initAnimation() {
275       if (armed && animation != null) {
276         animation.initAnimation();
277       }
278     }
279 
280     public void calcFrame( final double time ) {
281       if (armed && animation != null) {
282         animation.calcFrame(time);
283       }
284     }
285 
286     public void disposeAnimation() {
287       if (armed && animation != null) {
288         animation.disposeAnimation();
289       }
290     }
291 
292     public long preferredDuration() {
293       return animation != null ? animation.preferredDuration() : 0;
294     }
295 
296     void disarm() {
297       armed = false;
298     }
299   }
300 
301 }
302