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