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