View Javadoc

1   /* OpenLogViewer
2    *
3    * Copyright 2011
4    *
5    * This file is part of the OpenLogViewer project.
6    *
7    * OpenLogViewer software is free software: you can redistribute it and/or modify
8    * it under the terms of the GNU General Public License as published by
9    * the Free Software Foundation, either version 3 of the License, or
10   * (at your option) any later version.
11   *
12   * OpenLogViewer software is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Public License for more details.
16   *
17   * You should have received a copy of the GNU General Public License
18   * along with any OpenLogViewer software.  If not, see http://www.gnu.org/licenses/
19   *
20   * I ask that if you make any changes to this file you fork the code on github.com!
21   *
22   */
23  package org.diyefi.openlogviewer.graphing;
24  
25  import java.awt.BorderLayout;
26  import java.awt.Dimension;
27  import java.awt.event.ActionEvent;
28  import java.awt.event.ActionListener;
29  import java.awt.event.ComponentEvent;
30  import java.awt.event.ComponentListener;
31  import java.awt.event.MouseEvent;
32  import java.awt.event.MouseListener;
33  import java.awt.event.MouseMotionListener;
34  import java.awt.event.MouseWheelEvent;
35  import java.awt.event.MouseWheelListener;
36  import java.util.ResourceBundle;
37  
38  import javax.swing.AbstractAction;
39  import javax.swing.JPanel;
40  import javax.swing.KeyStroke;
41  import javax.swing.Timer;
42  
43  import org.diyefi.openlogviewer.Keys;
44  import org.diyefi.openlogviewer.OpenLogViewer;
45  import org.diyefi.openlogviewer.genericlog.GenericLog;
46  
47  public class EntireGraphingPanel extends JPanel implements ActionListener,
48  		MouseMotionListener,
49  		MouseListener,
50  		MouseWheelListener,
51  		ComponentListener {
52  	public static final int LEFT_OFFSCREEN_POINTS_ZOOMED_IN = 0;
53  	public static final int RIGHT_OFFSCREEN_POINTS_ZOOMED_IN = 3;
54  	public static final int LEFT_OFFSCREEN_POINTS_ZOOMED_OUT = 2;
55  	public static final int RIGHT_OFFSCREEN_POINTS_ZOOMED_OUT = 2;
56  	private static final long serialVersionUID = 1L;
57  	private static final int BASE_PLAY_SPEED = 10;
58  	private static final int BASE_FLING_SPEED = 10;
59  	private static final double COARSE_MOVEMENT_PERCENTAGE = 0.50;
60  	private static final int GRAPH_DISPLAY_WIDTH = 600;
61  	private static final int GRAPH_DISPLAY_HEIGHT = 400;
62  	private static final int BUTTON_DISPLAY_HEIGHT = 20;
63  	private static final int ZOOM_BUFFER = 8; // 4 pixels per side.
64  	private static final int PLAY_SPEED_DIVISOR = 6;
65  	private static final double FIFTY_PERCENT = 0.50;
66  	private static final int SCROLL_DELAY_THRESHOLD = 50; // milliseconds
67  	private static final int FLING_DELAY_THRESHOLD = 50; // milliseconds
68  
69  	private final MultiGraphLayeredPane multiGraph;
70  	private final GraphPositionPanel graphPositionPanel;
71  	private double graphPosition;
72  	private int graphSize;
73  	private boolean playing;
74  	private boolean wasPlaying;
75  	private final Timer playTimer;
76  	private final Timer flingTimer;
77  	private boolean dragging;
78  	private boolean flinging;
79  	private long thePastMouseDragged;
80  	private long thePastLeftArrow;
81  	private long thePastRightArrow;
82  	private int scrollAcceleration;
83  	private int prevDragXCoord;
84  	private int flingInertia;
85  	private int zoom;
86  	private boolean zoomedOutBeyondOneToOne;
87  	private int oldComponentWidth;
88  
89  	public EntireGraphingPanel(final ResourceBundle labels) {
90  		setName(EntireGraphingPanel.class.getSimpleName());
91  		setLayout(new BorderLayout());
92  		multiGraph = new MultiGraphLayeredPane(labels);
93  		graphPositionPanel = new GraphPositionPanel();
94  		playTimer = new Timer(BASE_PLAY_SPEED, this);
95  		flingTimer = new Timer(BASE_FLING_SPEED, this);
96  		initVariables();
97  		addListeners();
98  		initKeyBindings();
99  	}
100 
101 	private void initVariables() {
102 		multiGraph.setPreferredSize(new Dimension(GRAPH_DISPLAY_WIDTH, GRAPH_DISPLAY_HEIGHT));
103 		add(multiGraph, BorderLayout.CENTER);
104 		graphPositionPanel.setPreferredSize(new Dimension(GRAPH_DISPLAY_WIDTH, BUTTON_DISPLAY_HEIGHT));
105 		add(graphPositionPanel, BorderLayout.SOUTH);
106 		zoom = 1;
107 		zoomedOutBeyondOneToOne = false;
108 		oldComponentWidth = this.getWidth();
109 		resetGraphPosition();
110 		setGraphSize(0);
111 		playing = false;
112 		wasPlaying = false;
113 		playTimer.setInitialDelay(0);
114 		flingTimer.setInitialDelay(0);
115 		stopDragging();
116 		stopFlinging();
117 
118 		// It is not important that all of these times are the same, but
119 		// it doesn't hurt anything and prevents unnecessary System calls.
120 		final long now = System.currentTimeMillis();
121 		thePastMouseDragged = now;
122 		thePastLeftArrow = now;
123 		thePastRightArrow = now;
124 		scrollAcceleration = 0;
125 	}
126 
127 	private void addListeners() {
128 		addMouseListener(this);
129 		addMouseMotionListener(this);
130 		addMouseWheelListener(this);
131 		addMouseListener(multiGraph.getInfoPanel());
132 		addMouseMotionListener(multiGraph.getInfoPanel());
133 	}
134 
135 	public final void actionPerformed(final ActionEvent e) {
136 
137 		//Play timer event fires
138 		if (e.getSource().equals(playTimer)) {
139 			doPlayAction();
140 		}
141 
142 		//Fling timer event fires
143 		if (e.getSource().equals(flingTimer)) {
144 			doFlingAction();
145 		}
146 	}
147 
148 	private void doPlayAction() {
149 		if (playing && graphPosition < getGraphPositionMax()) {
150 			if (zoomedOutBeyondOneToOne) {
151 				moveGraphPosition(zoom);
152 			} else {
153 				moveGraphPosition(1);
154 			}
155 		} else {
156 			pause();
157 		}
158 	}
159 
160 	private void doFlingAction() {
161 		if ((flinging && graphPosition < getGraphPositionMax()) && (graphPosition > getGraphPositionMin())) {
162 			if (flingInertia == 0) {
163 				stopFlinging();
164 			} else {
165 				moveEntireGraphingPanel(flingInertia);
166 				if (flingInertia > 0) {
167 					flingInertia--;
168 				} else if (flingInertia < 0) {
169 					flingInertia++;
170 				} else {
171 					stopFlinging();
172 				}
173 			}
174 		} else {
175 			stopFlinging();
176 		}
177 	}
178 
179 	public final MultiGraphLayeredPane getMultiGraphLayeredPane() {
180 		return multiGraph;
181 	}
182 
183 	public final GraphPositionPanel getGraphPositionPanel() {
184 		return graphPositionPanel;
185 	}
186 
187 	public final void setLog(final GenericLog genLog) {
188 		pause();
189 		multiGraph.setLog(genLog);
190 		graphPositionPanel.setLog(genLog);
191 	}
192 
193 
194 	/**
195 	 * The tightest the user should be allowed to zoom in.
196 	 */
197 	private int getTightestZoom() {
198 		return this.getWidth() - 1;
199 	}
200 
201 	/**
202 	 * The widest the user should be allowed to zoom out.
203 	 */
204 	private int getWidestZoom() {
205 		return ((graphSize + 1) / 2);
206 	}
207 
208 	/**
209 	 * Zoom in using steps larger the further away from 1:1 you are.
210 	 * This assumes you are zooming in on the data centered in the screen.
211 	 * If you need to zoom in on a different location then you must use
212 	 * zoomIn() repeatedly coupled with a move each time.
213 	 */
214 	public final void zoomInCoarse() {
215 		final int zoomAmount = (int) Math.sqrt(zoom);
216 		for (int i = 0; i < zoomAmount; i++) {
217 			zoomIn();
218 		}
219 	}
220 
221 	/**
222 	 * Zoom in by one. This control zooms finer than the coarse zoom control.
223 	 * This assumes you are zooming in on the data centered in the screen.
224 	 * If you need to zoom in on a different location then you must move
225 	 * the graph accordingly.
226 	 */
227 	public final void zoomIn() {
228 		final double graphWidth = this.getWidth();
229 		double move = 0;
230 
231 		if (zoomedOutBeyondOneToOne) {
232 			if (zoom == 2) {
233 				zoomedOutBeyondOneToOne = false;
234 			}
235 			zoom--;
236 			move = graphWidth / (double) (zoom * 2);
237 		} else if (zoom < getTightestZoom()) {
238 			move = graphWidth / (double) (zoom * 2);
239 			zoom++;
240 		}
241 
242 		moveEntireGraphingPanel(move);
243 	}
244 
245 	/**
246 	 * Zoom the graph to a 1:1 pixel-to-data-point ratio.
247 	 */
248 	public final void zoomResetRatio() {
249 		if (zoomedOutBeyondOneToOne) {
250 			for (int i = zoom; i > 1; i--) {
251 				zoomIn();
252 			}
253 		} else {
254 			for (int i = zoom; i > 1; i--) {
255 				zoomOut();
256 			}
257 		}
258 	}
259 
260 	/**
261 	 * Zoom the graph so that if it is centered, then the
262 	 * entire graph will fit within the display. Usually
263 	 * this will result in ultimately zooming out, but if the
264 	 * graph is small enough and/or the display is large enough
265 	 * then zooming in will be more appropriate.
266 	 *
267 	 * If the graph will fit perfectly inside the display
268 	 * then it will be sized down one more time so that
269 	 * there is always at least 4 pixels of blank space to
270 	 * the left and right of the graph so the user will
271 	 * know they are seeing the entire graph trace.
272 	 */
273 	public final void zoomGraphToFit(final int dataPointsToFit) {
274 		final int graphWindowWidth = this.getWidth() - ZOOM_BUFFER;
275 		int dataPointsThatFitInDisplay = 0;
276 		if (zoomedOutBeyondOneToOne) {
277 			dataPointsThatFitInDisplay = graphWindowWidth * zoom;
278 		} else {
279 			dataPointsThatFitInDisplay =  graphWindowWidth / zoom;
280 		}
281 
282 		// Zoom in until the data no longer fits in the display.
283 		while (dataPointsToFit < dataPointsThatFitInDisplay && zoom != getTightestZoom()) {
284 			zoomIn();
285 			if (zoomedOutBeyondOneToOne) {
286 				dataPointsThatFitInDisplay = graphWindowWidth * zoom;
287 			} else {
288 				dataPointsThatFitInDisplay =  graphWindowWidth / zoom;
289 			}
290 		}
291 
292 		// Zoom out one or more times until the data just fits in the display.
293 		while (dataPointsToFit > dataPointsThatFitInDisplay && zoom != getWidestZoom()) {
294 			zoomOut();
295 			if (zoomedOutBeyondOneToOne) {
296 				dataPointsThatFitInDisplay = graphWindowWidth * zoom;
297 			} else {
298 				dataPointsThatFitInDisplay =  graphWindowWidth / zoom;
299 			}
300 		}
301 	}
302 
303 	/**
304 	 * Used by external sources that don't know or care about the size of the graph.
305 	 */
306 	public final void zoomGraphToFit() {
307 		zoomGraphToFit(graphSize);
308 	}
309 
310 	/**
311 	 * Zoom out by one. This control zooms finer than the coarse zoom control.
312 	 * This assumes you are zooming out from the data centered in the screen.
313 	 * If you need to zoom out from a different location then you must move
314 	 * the graph accordingly.
315 	 */
316 	public final void zoomOut() {
317 		final double graphWidth = this.getWidth();
318 		double move = 0;
319 
320 		if (!zoomedOutBeyondOneToOne) {
321 			if (zoom == 1) {
322 				zoomedOutBeyondOneToOne = true;
323 				zoom = 2;
324 				move = graphWidth / (double) (zoom * 2);
325 			} else {
326 				move = graphWidth / (double) (zoom * 2);
327 				zoom--;
328 			}
329 		} else if (zoom < getWidestZoom()) {
330 			zoom++;
331 			move = graphWidth / (double) (zoom * 2);
332 		}
333 
334 		moveEntireGraphingPanel(-move);
335 	}
336 
337 	/**
338 	 * Zoom out using steps larger the further away from 1:1 you are.
339 	 * This assumes you are zooming out with the data centered in the screen.
340 	 * If you need to zoom out on a different location then you must use
341 	 * zoomOut() repeatedly coupled with a move each time.
342 	 */
343 	public final void zoomOutCoarse() {
344 		final int zoomAmount = (int) Math.sqrt(zoom);
345 		for (int i = 0; i < zoomAmount; i++) {
346 			zoomOut();
347 		}
348 	}
349 
350 	public final boolean isZoomedOutBeyondOneToOne() {
351 		return zoomedOutBeyondOneToOne;
352 	}
353 
354 	/**
355 	 * Slows the speed of playback (exponentially)
356 	 */
357 	public final void slowDown() {
358 		final int currentDelay = playTimer.getDelay();
359 		long newDelay = currentDelay + (currentDelay / PLAY_SPEED_DIVISOR) + 1;
360 		if (newDelay > Integer.MAX_VALUE) {
361 			newDelay = Integer.MAX_VALUE;
362 		}
363 		playTimer.setDelay((int) newDelay);
364 	}
365 
366 	/**
367 	 * Resets the speed of playback to the original speed
368 	 */
369 	public final void resetPlaySpeed() {
370 		playTimer.setDelay(BASE_PLAY_SPEED);
371 	}
372 
373 	public final void play() {
374 		if (playing) {
375 			pause();
376 		} else {
377 			playing = true;
378 			playTimer.start();
379 		}
380 		OpenLogViewer.getInstance().getNavBarPanel().updatePausePlayButton();
381 	}
382 
383 	public final void pause() {
384 		playing = false;
385 		playTimer.stop();
386 		OpenLogViewer.getInstance().getNavBarPanel().updatePausePlayButton();
387 	}
388 
389 	/**
390 	 * Increases the speed of the graph exponentially until the delay is zero,
391 	 * at which speed cannot be advanced any further and will essentially update
392 	 * as fast as possible.
393 	 */
394 	public final void speedUp() {
395 		final int currentDelay = playTimer.getDelay();
396 		int newDelay = currentDelay - (currentDelay / PLAY_SPEED_DIVISOR) - 1;
397 		if (newDelay < 0) {
398 			newDelay = 0;
399 		}
400 		playTimer.setDelay(newDelay);
401 	}
402 
403 	public final void fling() {
404 		flinging = true;
405 		flingTimer.start();
406 	}
407 
408 	private double getGraphPositionMin() {
409 		double min = 0.0;
410 		if (zoomedOutBeyondOneToOne) {
411 			min = -((this.getWidth() - 1) * zoom);
412 		} else {
413 			min = -(((double) this.getWidth() - 1.0) / (double) zoom);
414 		}
415 		return min;
416 	}
417 
418 	public final double getGraphPosition() {
419 		return graphPosition;
420 	}
421 
422 	private int getGraphPositionMax() {
423 		if (zoom == getWidestZoom()) {
424 			int size = graphSize - (LEFT_OFFSCREEN_POINTS_ZOOMED_OUT * zoom);
425 			if (size < 0) {
426 				size = 0;
427 			}
428 			return size;
429 		}
430 		return graphSize;
431 	}
432 
433 	public final void setGraphPosition(final double newPos) {
434 		graphPosition = newPos;
435 		repaint();
436 	}
437 
438 	/**
439 	 * How many available data records we are dealing with.
440 	 */
441 	public final void setGraphSize(final int newGraphSize) {
442 		graphSize = newGraphSize;
443 		if (graphSize > 0) {
444 			zoomGraphToFit(graphSize);
445 			centerGraphPosition(0, graphSize);
446 		}
447 	}
448 
449 	/**
450 	 * Move the graph to the right so that only one valid
451 	 * data point shows on the right-most part of the display.
452 	 */
453 	private void resetGraphPosition() {
454 		setGraphPosition(getGraphPositionMin());
455 	}
456 
457 	/**
458 	 * Move the graph to the beginning with the first data point centered.
459 	 */
460 	public final void moveToBeginning() {
461 		resetGraphPosition();
462 		moveForwardPercentage(FIFTY_PERCENT);
463 	}
464 
465 	/**
466 	 * Move the graph backward a small amount (with acceleration).
467 	 */
468 	public final void moveBackward() {
469 		int localZoom = zoom;
470 		if (zoomedOutBeyondOneToOne) {
471 			localZoom = 1;
472 		}
473 		final long now = System.currentTimeMillis();
474 		final long delay = now - thePastLeftArrow;
475 		if (delay < SCROLL_DELAY_THRESHOLD) {
476 			scrollAcceleration++;
477 			moveEntireGraphingPanel(-localZoom - (scrollAcceleration * localZoom));
478 		} else {
479 			scrollAcceleration = 0;
480 			moveEntireGraphingPanel(-localZoom);
481 		}
482 		thePastLeftArrow = System.currentTimeMillis();
483 	}
484 
485 	/**
486 	 * Move the graph backward a large amount.
487 	 */
488 	public final void moveBackwardCoarse() {
489 		moveBackwardPercentage(COARSE_MOVEMENT_PERCENTAGE);
490 	}
491 
492 	/**
493 	 * Move the graph backward by a percentage (amount) of the screen width.
494 	 * Percentages are expected in decimal form. For example, 0.50 for 50%.
495 	 */
496 	public final void moveBackwardPercentage(final double amount) {
497 		moveEntireGraphingPanel(-(this.getWidth() * amount));
498 	}
499 
500 	/**
501 	 * Move the graph to the center of the two provided graph positions
502 	 * so that there are equal data points to the left and to the right.
503 	 *
504 	 * Right now the method is expecting to get integer data points as
505 	 * it should be impossible to select fractions of a data point.
506 	 */
507 	private void centerGraphPosition(final int beginPosition, final int endPosition) {
508 		final int halfScreen = this.getWidth() / 2;
509 		double pointsThatFitInHalfScreen = 0;
510 		if (zoomedOutBeyondOneToOne) {
511 			pointsThatFitInHalfScreen = halfScreen * zoom;
512 		} else {
513 			pointsThatFitInHalfScreen = halfScreen / (double) zoom;
514 		}
515 		final int distanceBetweenPositions = endPosition - beginPosition;
516 		final double halfwayBetweenTwoPositions = distanceBetweenPositions / 2d;
517 		final double centerPosition = (beginPosition + halfwayBetweenTwoPositions) - pointsThatFitInHalfScreen;
518 		setGraphPosition(centerPosition);
519 	}
520 
521 	/**
522 	 * Used by external sources that don't know or care about the size of the graph.
523 	 */
524 	public final void centerGraphPosition() {
525 		centerGraphPosition(0, graphSize);
526 	}
527 
528 	/**
529 	 * Move the graph forward a small amount (with acceleration).
530 	 */
531 	public final void moveForward() {
532 		int localZoom = zoom;
533 		if (zoomedOutBeyondOneToOne) {
534 			localZoom = 1;
535 		}
536 		final long now = System.currentTimeMillis();
537 		final long delay = now - thePastRightArrow;
538 		if (delay < SCROLL_DELAY_THRESHOLD) {
539 			scrollAcceleration++;
540 			moveEntireGraphingPanel(localZoom + (scrollAcceleration * localZoom));
541 		} else {
542 			scrollAcceleration = 0;
543 			moveEntireGraphingPanel(localZoom);
544 		}
545 		thePastRightArrow = System.currentTimeMillis();
546 	}
547 
548 	/**
549 	 * Move the graph forward by a percentage (amount) of the screen width.
550 	 * Percentages are expected in decimal form. For example, 0.50 for 50%.
551 	 */
552 	private void moveForwardPercentage(final double amount) {
553 		moveEntireGraphingPanel(this.getWidth() * amount);
554 	}
555 
556 	/**
557 	 * Move the graph forward a large amount.
558 	 */
559 	public final void moveForwardCoarse() {
560 		moveForwardPercentage(COARSE_MOVEMENT_PERCENTAGE);
561 	}
562 
563 	/**
564 	 * Move the graph to the end with the last data point centered.
565 	 */
566 	public final void moveToEnd() {
567 		goToLastGraphPosition();
568 		moveBackwardPercentage(FIFTY_PERCENT);
569 	}
570 
571 	/**
572 	 * Move the graph to the left so that only one valid
573 	 * data point shows on the left-most part of the display.
574 	 */
575 	private void goToLastGraphPosition() {
576 		setGraphPosition(getGraphPositionMax());
577 	}
578 
579 	public final boolean isPlaying() {
580 		return playing;
581 	}
582 
583 	public final int getZoom() {
584 		return zoom;
585 	}
586 
587 	/**
588 	 * When the windows is resized, the graph needs to move to maintain the centering.
589 	 */
590 	public final void moveGraphDueToResize() {
591 		final int newWidth = this.getWidth();
592 		if (newWidth != oldComponentWidth) {
593 			double move = 0.0;
594 			final int amount = newWidth - oldComponentWidth;
595 			if (zoomedOutBeyondOneToOne) {
596 				move = -(amount * zoom);
597 			} else {
598 				move = -((double) amount / (double) zoom);
599 			}
600 			move /= 2.0;
601 			oldComponentWidth = newWidth;
602 			moveGraphPosition(move);
603 		}
604 	}
605 
606 	/**
607 	 * Take the current graph position and move amount positions forward.
608 	 */
609 	private void moveGraphPosition(final double amount) {
610 		final double newPos = graphPosition + amount;
611 		if (newPos > getGraphPositionMax()) {
612 			goToLastGraphPosition();
613 		} else if (newPos < getGraphPositionMin()) {
614 			resetGraphPosition();
615 		} else {
616 			setGraphPosition(newPos);
617 		}
618 	}
619 
620 	/**
621 	 * Move the graph position to newPosition where newPosition is dictated by
622 	 * an x screen coordinate.
623 	 */
624 	private void moveEntireGraphingPanel(final double newPosition) {
625 		double move = -1.0;
626 		if (zoomedOutBeyondOneToOne) {
627 			move = newPosition * zoom;
628 		} else {
629 			move = newPosition / zoom;
630 		}
631 		if (graphPosition + move < getGraphPositionMax()) {
632 			if (graphPosition + move < getGraphPositionMin()) {
633 				resetGraphPosition();
634 			} else {
635 				moveGraphPosition(move);
636 			}
637 		} else {
638 			goToLastGraphPosition();
639 		}
640 	}
641 
642 	private void stopDragging() {
643 		dragging = false;
644 		prevDragXCoord = -1;
645 	}
646 
647 	private void stopFlinging() {
648 		flinging = false;
649 		flingInertia = 0;
650 		flingTimer.stop();
651 	}
652 
653 	// Mouse listener functionality
654 	@Override
655 	public final void mouseClicked(final MouseEvent e) {
656 		if (!dragging) {
657 			final int half = this.getWidth() / 2;
658 			moveEntireGraphingPanel(e.getX() - half);
659 		} else {
660 			stopDragging();
661 			stopFlinging();
662 		}
663 	}
664 
665 	@Override
666 	public final void mouseDragged(final MouseEvent e) {
667 		dragging = true;
668 		final int xMouseCoord = e.getX();
669 		if ((prevDragXCoord > 0) && (prevDragXCoord != xMouseCoord)) {
670 			moveEntireGraphingPanel(prevDragXCoord - xMouseCoord);
671 			flingInertia = ((prevDragXCoord - xMouseCoord) * 2);
672 			thePastMouseDragged = System.currentTimeMillis();
673 		}
674 		prevDragXCoord = xMouseCoord;
675 	}
676 
677 	@Override
678 	public final void mouseMoved(final MouseEvent e) {
679 		// What should be here?
680 		// Ben says eventually there might be stuff here, and it is required implementation for the MouseMovementListener interface.
681 		// Fred says thanks! :-)
682 	}
683 
684 	@Override
685 	public final void mouseEntered(final MouseEvent e) {
686 		// What should be here?
687 		// Ben says eventually there might be stuff here, and it is required implementation for the MouseMovementListener interface.
688 		// Fred says thanks! :-)
689 	}
690 
691 	@Override
692 	public final void mouseExited(final MouseEvent e) {
693 		// What should be here?
694 		// Ben says eventually there might be stuff here, and it is required implementation for the MouseMovementListener interface.
695 		// Fred says thanks! :-)
696 	}
697 
698 	@Override
699 	public final void mousePressed(final MouseEvent e) {
700 		wasPlaying = playing;
701 		if (playing) {
702 			pause();
703 		}
704 
705 		stopDragging();
706 		stopFlinging();
707 	}
708 
709 	@Override
710 	public final void mouseReleased(final MouseEvent e) {
711 		stopDragging();
712 
713 		final long now = System.currentTimeMillis();
714 		if ((now - thePastMouseDragged) > FLING_DELAY_THRESHOLD) {
715 			stopFlinging(); // If over FLING_DELAY_THRESHOLD milliseconds since dragging then don't fling
716 		}
717 
718 		if (flingInertia != 0) {
719 			fling();
720 		}
721 
722 		if (wasPlaying) {
723 			play();
724 		}
725 	}
726 
727 	@Override
728 	public final void mouseWheelMoved(final MouseWheelEvent e) {
729 		final int notches = e.getWheelRotation();
730 		if (notches < 0) {
731 			mouseWheelMovedForward(e.getX());
732 		} else {
733 			mouseWheelMovedBackward(e.getX());
734 		}
735 	}
736 
737 	private void mouseWheelMovedForward(final int xMouseCoord) {
738 		final double center = this.getWidth() / 2.0;
739 		double move = 0;
740 		final int zoomAmount = (int) Math.sqrt(zoom);
741 		for (int i = 0; i < zoomAmount; i++) {
742 			if (zoomedOutBeyondOneToOne) {
743 				move = (xMouseCoord - center) / (zoom - 1.0);
744 			} else {
745 				move = (xMouseCoord - center) / zoom;
746 			}
747 			if (!(!zoomedOutBeyondOneToOne && zoom == getTightestZoom())) {
748 				zoomIn();
749 				moveEntireGraphingPanel(move);
750 			}
751 		}
752 	}
753 
754 	private void mouseWheelMovedBackward(final int xMouseCoord) {
755 		final double center = this.getWidth() / 2.0;
756 		double move = 0;
757 		final int zoomAmount = (int) Math.sqrt(zoom);
758 		for (int i = 0; i < zoomAmount; i++) {
759 			if (zoomedOutBeyondOneToOne || zoom == 1) {
760 				move = -(xMouseCoord - center) / (zoom + 1.0);
761 			} else {
762 				move = -(xMouseCoord - center) / zoom;
763 			}
764 			if (!(zoomedOutBeyondOneToOne && zoom == getWidestZoom())) {
765 				zoomOut();
766 				moveEntireGraphingPanel(move);
767 			}
768 		}
769 	}
770 
771 	@Override
772 	public void componentHidden(final ComponentEvent e) {
773 		// Ben says eventually there might be stuff here, and it is required implementation for the ComponentListener interface.
774 		// Fred says thanks! :-)
775 	}
776 
777 	// Call resize event handler because the Mac sort of treats resizing as a move
778 	@Override
779 	public final void componentMoved(final ComponentEvent e) {
780 		moveGraphDueToResize();
781 	}
782 
783 	@Override
784 	public final void componentResized(final ComponentEvent e) {
785 		moveGraphDueToResize();
786 	}
787 
788 	@Override
789 	public void componentShown(final ComponentEvent e) {
790 		// Ben says eventually there might be stuff here, and it is required implementation for the ComponentListener interface.
791 		// Fred says thanks! :-)
792 	}
793 
794 	private void initKeyBindings() {
795 		// Play key bindings
796 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.SPACE), Keys.PLAY);
797 		getActionMap().put(Keys.PLAY, new AbstractAction() {
798 			private static final long serialVersionUID = 1L;
799 			public void actionPerformed(final ActionEvent e) {
800 				play();
801 			}
802 		});
803 
804 		// Enter full screen key bindings
805 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.ALT_ENTER), Keys.ENTER_FULL_SCREEN);
806 		getActionMap().put(Keys.ENTER_FULL_SCREEN, new AbstractAction() {
807 			private static final long serialVersionUID = 1L;
808 			public void actionPerformed(final ActionEvent e) {
809 				OpenLogViewer.getInstance().enterFullScreen();
810 			}
811 		});
812 
813 		// Exit full screen key bindings
814 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.ESCAPE), Keys.EXIT_FULL_SCREEN);
815 		getActionMap().put(Keys.EXIT_FULL_SCREEN, new AbstractAction() {
816 			private static final long serialVersionUID = 1L;
817 			public void actionPerformed(final ActionEvent e) {
818 				OpenLogViewer.getInstance().exitFullScreen();
819 			}
820 		});
821 
822 		// Toggle full screen key bindings
823 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.F11), Keys.TOGGLE_FULL_SCREEN);
824 		getActionMap().put(Keys.TOGGLE_FULL_SCREEN, new AbstractAction() {
825 			private static final long serialVersionUID = 1L;
826 			public void actionPerformed(final ActionEvent e) {
827 				OpenLogViewer.getInstance().toggleFullScreen();
828 			}
829 		});
830 
831 		// Move to beginning key bindings
832 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.HOME), Keys.MOVE_TO_BEGINNING);
833 		getActionMap().put(Keys.MOVE_TO_BEGINNING, new AbstractAction() {
834 			private static final long serialVersionUID = 1L;
835 			public void actionPerformed(final ActionEvent e) {
836 				moveToBeginning();
837 			}
838 		});
839 
840 		// Move to end key bindings
841 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.END), Keys.MOVE_TO_END);
842 		getActionMap().put(Keys.MOVE_TO_END, new AbstractAction() {
843 			private static final long serialVersionUID = 1L;
844 			public void actionPerformed(final ActionEvent e) {
845 				moveToEnd();
846 			}
847 		});
848 
849 		// Move backward key bindings
850 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.LEFT), Keys.MOVE_BACKWARD);
851 		getActionMap().put(Keys.MOVE_BACKWARD, new AbstractAction() {
852 			private static final long serialVersionUID = 1L;
853 			public void actionPerformed(final ActionEvent e) {
854 				moveBackward();
855 			}
856 		});
857 
858 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.PAGE_UP), Keys.MOVE_BACKWARD_COARSE);
859 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.CONTROL_LEFT), Keys.MOVE_BACKWARD_COARSE);
860 		getActionMap().put(Keys.MOVE_BACKWARD_COARSE, new AbstractAction() {
861 			private static final long serialVersionUID = 1L;
862 			public void actionPerformed(final ActionEvent e) {
863 				moveBackwardCoarse();
864 			}
865 		});
866 
867 		// Move forward key bindings
868 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.RIGHT), Keys.MOVE_FORWARD);
869 		getActionMap().put(Keys.MOVE_FORWARD, new AbstractAction() {
870 			private static final long serialVersionUID = 1L;
871 			public void actionPerformed(final ActionEvent e) {
872 				moveForward();
873 			}
874 		});
875 
876 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.PAGE_DOWN), Keys.MOVE_FORWARD_COARSE);
877 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.CONTROL_RIGHT), Keys.MOVE_FORWARD_COARSE);
878 		getActionMap().put(Keys.MOVE_FORWARD_COARSE, new AbstractAction() {
879 			private static final long serialVersionUID = 1L;
880 			public void actionPerformed(final ActionEvent e) {
881 				moveForwardCoarse();
882 			}
883 		});
884 
885 		// Zoom in key bindings
886 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.UP), Keys.ZOOM_IN_COARSE);
887 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.CONTROL_ADD), Keys.ZOOM_IN_COARSE);
888 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.CONTROL_EQUALS), Keys.ZOOM_IN_COARSE);
889 		getActionMap().put(Keys.ZOOM_IN_COARSE, new AbstractAction() {
890 			private static final long serialVersionUID = 1L;
891 			public void actionPerformed(final ActionEvent e) {
892 				zoomInCoarse();
893 			}
894 		});
895 
896 		// Zoom out key bindings
897 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.DOWN), Keys.ZOOM_OUT_COARSE);
898 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.CONTROL_MINUS), Keys.ZOOM_OUT_COARSE);
899 		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(Keys.CONTROL_SUBTRACT), Keys.ZOOM_OUT_COARSE);
900 		getActionMap().put(Keys.ZOOM_OUT_COARSE, new AbstractAction() {
901 			private static final long serialVersionUID = 1L;
902 			public void actionPerformed(final ActionEvent e) {
903 				zoomOutCoarse();
904 			}
905 		});
906 	}
907 }