Fixed git clone URL
[sixth-3d.git] / src / main / java / eu / svjatoslav / sixth / e3d / gui / humaninput / HIDEventTracker.java
1 /*
2  * Sixth 3D engine. Author: Svjatoslav Agejenko.
3  * This project is released under Creative Commons Zero (CC0) license.
4  */
5 package eu.svjatoslav.sixth.e3d.gui.humaninput;
6
7 import eu.svjatoslav.sixth.e3d.geometry.Point2D;
8 import eu.svjatoslav.sixth.e3d.gui.Avatar;
9 import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
10 import eu.svjatoslav.sixth.e3d.gui.ViewRenderListener;
11
12 import javax.swing.*;
13 import java.awt.event.*;
14 import java.util.ArrayList;
15 import java.util.HashMap;
16 import java.util.List;
17 import java.util.Map;
18
19 /**
20  * This class is responsible for tracking human input devices (keyboard, mouse, etc.) and
21  * forwarding those inputs to subsequent virtual components.
22  */
23 public class HIDEventTracker implements
24         MouseMotionListener, KeyListener, MouseListener, MouseWheelListener, ViewRenderListener {
25
26     /**
27      * <p>  Map of pressed keys. </p>
28      * <p>  Key is mouse button code. </p>
29      * <p>  Value is system milliseconds when button was pressed. </p>
30      * <p>  So by reading the map one can determine currently pressed buttons as well as duration. </p>
31      */
32     private final Map<Integer, Long> pressedKeysToPressedTimeMap = new HashMap<>();
33     private final List<MouseEvent> detectedMouseEvents = new ArrayList<>();
34     private final List<KeyEvent> detectedKeyEvents = new ArrayList<>();
35     private final Point2D mouseDraggedDirection = new Point2D();
36     private final ViewPanel viewPanel;
37     private int wheelMovedDirection = 0;
38     private Point2D oldMouseCoordinatesWhenDragging;
39     private Point2D currentMouseLocation;
40     private boolean mouseMoved;
41     private boolean mouseWithinWindow = false;
42
43     /**
44      * Construct new tracker for specified panel.
45      */
46     public HIDEventTracker(final ViewPanel viewPanel) {
47         this.viewPanel = viewPanel;
48         bind(viewPanel);
49     }
50
51     /**
52      * {@inheritDoc}
53      */
54     @Override
55     public boolean beforeRender(final ViewPanel viewPanel, final int millisecondsSinceLastFrame) {
56         boolean viewUpdateNeeded = handleKeyboardEvents();
57         viewUpdateNeeded |= handleMouseClicksAndHover(viewPanel);
58         viewUpdateNeeded |= handleMouseDragging();
59         viewUpdateNeeded |= handleMouseVerticalScrolling();
60         return viewUpdateNeeded;
61     }
62
63     /**
64      * Bind this tracker to specified panel.
65      * @param panel panel to bind to.
66      */
67     private void bind(final JPanel panel) {
68         panel.addMouseMotionListener(this);
69
70         panel.addKeyListener(this);
71
72         panel.addMouseListener(this);
73
74         panel.addMouseWheelListener(this);
75     }
76
77     /**
78      * @return <code>true</code> if view needs to be repainted.
79      */
80     private boolean handleKeyboardEvents() {
81         final KeyboardInputHandler currentFocusOwner = viewPanel.getKeyboardFocusStack().getCurrentFocusOwner();
82         ArrayList<KeyEvent> unprocessedKeyboardEvents = getUnprocessedKeyboardEvents();
83
84         return currentFocusOwner != null
85                 && forwardKeyboardEventsToFocusOwner(currentFocusOwner, unprocessedKeyboardEvents);
86     }
87
88     private ArrayList<KeyEvent> getUnprocessedKeyboardEvents() {
89         synchronized (detectedKeyEvents) {
90             ArrayList<KeyEvent> result = new ArrayList<>(detectedKeyEvents);
91             detectedKeyEvents.clear();
92             return result;
93         }
94     }
95
96     /**
97      * @return <code>true</code> if view update is needed.
98      */
99     private boolean forwardKeyboardEventsToFocusOwner(
100             KeyboardInputHandler currentFocusOwner, ArrayList<KeyEvent> keyEvents) {
101         boolean viewUpdateNeeded = false;
102
103         for (KeyEvent keyEvent : keyEvents)
104             viewUpdateNeeded |= processKeyEvent(currentFocusOwner, keyEvent);
105
106         return viewUpdateNeeded;
107     }
108
109     private boolean processKeyEvent(KeyboardInputHandler currentFocusOwner, KeyEvent keyEvent) {
110         switch (keyEvent.getID()) {
111             case KeyEvent.KEY_PRESSED:
112                 return currentFocusOwner.keyPressed(keyEvent, viewPanel);
113
114             case KeyEvent.KEY_RELEASED:
115                 return currentFocusOwner.keyReleased(keyEvent, viewPanel);
116         }
117         return false;
118     }
119
120     /**
121      * @return <code>true</code> if view needs to be repainted.
122      */
123     private synchronized boolean handleMouseClicksAndHover(final ViewPanel viewPanel) {
124         boolean rerenderNeeded = false;
125         MouseEvent event = findClickLocationToTrace();
126         if (event != null) {
127             // process mouse clicks as a first priority
128             rerenderNeeded = true;
129         } else {
130             // when there are no mouse clicks, process mouse hovering
131
132             if (mouseMoved) {
133                 mouseMoved = false;
134                 // we would like to re-render frame when user moved mouse, to see what objects mouse is hovering over
135                 rerenderNeeded = true;
136             }
137
138             if (currentMouseLocation != null) {
139                 // mouse click with button 0 amounts to mouse hovering event
140                 event = new MouseEvent(currentMouseLocation, 0);
141             }
142         }
143
144         if (viewPanel.getRenderingContext() != null)
145             viewPanel.getRenderingContext().setMouseEvent(event);
146
147         return rerenderNeeded;
148     }
149
150     private MouseEvent findClickLocationToTrace() {
151         synchronized (detectedMouseEvents) {
152             if (detectedMouseEvents.isEmpty())
153                 return null;
154
155             return detectedMouseEvents.remove(0);
156         }
157     }
158
159     boolean isKeyPressed(final int keyCode) {
160         return pressedKeysToPressedTimeMap.containsKey(keyCode);
161     }
162
163     @Override
164     public void keyPressed(final KeyEvent evt) {
165         synchronized (detectedKeyEvents) {
166             pressedKeysToPressedTimeMap.put(evt.getKeyCode(), System.currentTimeMillis());
167             detectedKeyEvents.add(evt);
168         }
169     }
170
171     @Override
172     public void keyReleased(final KeyEvent evt) {
173         synchronized (detectedKeyEvents) {
174             pressedKeysToPressedTimeMap.remove(evt.getKeyCode());
175             detectedKeyEvents.add(evt);
176         }
177     }
178
179     @Override
180     public void keyTyped(final KeyEvent e) {
181     }
182
183     @Override
184     public void mouseClicked(final java.awt.event.MouseEvent e) {
185         synchronized (detectedMouseEvents) {
186             detectedMouseEvents.add(new MouseEvent(e.getX(), e.getY(), e.getButton()));
187         }
188     }
189
190     @Override
191     public void mouseDragged(final java.awt.event.MouseEvent evt) {
192         final Point2D mouseLocation = new Point2D(evt.getX(), evt.getY());
193
194         if (oldMouseCoordinatesWhenDragging == null) {
195             oldMouseCoordinatesWhenDragging = mouseLocation;
196             return;
197         }
198
199         mouseDraggedDirection.add(mouseLocation.clone().subtract(oldMouseCoordinatesWhenDragging));
200
201         oldMouseCoordinatesWhenDragging = mouseLocation;
202     }
203
204     @Override
205     public void mouseEntered(final java.awt.event.MouseEvent e) {
206         mouseWithinWindow = true;
207     }
208
209     @Override
210     public synchronized void mouseExited(final java.awt.event.MouseEvent e) {
211         mouseWithinWindow = false;
212         currentMouseLocation = null;
213     }
214
215     @Override
216     public synchronized void mouseMoved(final java.awt.event.MouseEvent e) {
217         currentMouseLocation = new Point2D(e.getX(), e.getY());
218         mouseMoved = true;
219     }
220
221     @Override
222     public void mousePressed(final java.awt.event.MouseEvent e) {
223     }
224
225     @Override
226     public void mouseReleased(final java.awt.event.MouseEvent evt) {
227         oldMouseCoordinatesWhenDragging = null;
228     }
229
230     @Override
231     public void mouseWheelMoved(final java.awt.event.MouseWheelEvent evt) {
232         wheelMovedDirection += evt.getWheelRotation();
233     }
234
235     /**
236      * @return <code>true</code> if view needs to be repainted.
237      */
238     private boolean handleMouseVerticalScrolling() {
239         final Avatar avatar = viewPanel.getAvatar();
240         final double actualAcceleration = 50 * avatar.avatarAcceleration * (1 + (avatar.getMovementSpeed() / 10));
241         avatar.getMovementVector().y += (wheelMovedDirection * actualAcceleration);
242         avatar.enforceSpeedLimit();
243         boolean repaintNeeded = wheelMovedDirection != 0;
244         wheelMovedDirection = 0;
245         return repaintNeeded;
246     }
247
248     /**
249      * @return <code>true</code> if view needs to be repainted.
250      */
251     private boolean handleMouseDragging() {
252         // TODO: It would be nice here to detect somehow whether user moved mouse or touch screen.
253         // in case of touch screen, we would like to reverse movement along X and Y axis.
254
255         final Avatar avatar = viewPanel.getAvatar();
256         // for mouse
257         avatar.setAngleXZ(avatar.getAngleXZ() - ((float) mouseDraggedDirection.x / 50));
258         avatar.setAngleYZ(avatar.getAngleYZ() - ((float) mouseDraggedDirection.y / 50));
259
260         // for touch screen
261         // avatar.setAngleXZ(avatar.getAngleXZ() + ((float)
262         // mouseDraggedDirection.x / 50));
263         // avatar.setAngleYZ(avatar.getAngleYZ() + ((float)
264         // mouseDraggedDirection.y / 50));
265
266         boolean viewUpdateNeeded = !mouseDraggedDirection.isZero();
267         mouseDraggedDirection.zero();
268         return viewUpdateNeeded;
269     }
270
271 }