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