Optimized frame repainting. Fixed mouse click processing.
[sixth-3d.git] / src / main / java / eu / svjatoslav / sixth / e3d / gui / humaninput / HIDInputTracker.java
1 /*
2  * Sixth 3D engine. Copyright ©2012-2018, 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         MouseEvent mouseEventAndLocationToTrace = getMouseEventAndLocationToTrace(viewPanel);
121         if (mouseEventAndLocationToTrace != null)
122         {
123             viewPanel.getRenderingContext().mouseEvent = mouseEventAndLocationToTrace;
124             return true;
125         }
126         return false;
127     }
128
129     private MouseEvent getMouseEventAndLocationToTrace(ViewPanel viewPanel) {
130         MouseEvent unprocessedMouseEvent = findClickLocationToTrace();
131         if (unprocessedMouseEvent != null) {
132             return unprocessedMouseEvent;
133         } else
134             return getHoverLocationToTrace(viewPanel);
135     }
136
137     private MouseEvent findClickLocationToTrace() {
138         synchronized (detectedMouseEvents) {
139             if (detectedMouseEvents.isEmpty())
140                 return null;
141
142             return detectedMouseEvents.remove(0);
143         }
144     }
145
146     private MouseEvent getHoverLocationToTrace(ViewPanel viewPanel) {
147         if (mouseMoved) {
148             mouseMoved = false;
149             if (currentMouseLocation != null) {
150                 return new MouseEvent(currentMouseLocation, 0);
151                 // mouse click with button 0 amounts to mouse hovering event
152             }
153         }
154         return null;
155     }
156
157     boolean isKeyPressed(final int keyCode) {
158         return pressedKeysToPressedTimeMap.containsKey(keyCode);
159     }
160
161     @Override
162     public void keyPressed(final KeyEvent evt) {
163         synchronized (detectedKeyEvents) {
164             pressedKeysToPressedTimeMap.put(evt.getKeyCode(), System.currentTimeMillis());
165             detectedKeyEvents.add(evt);
166         }
167     }
168
169     @Override
170     public void keyReleased(final KeyEvent evt) {
171         synchronized (detectedKeyEvents) {
172             pressedKeysToPressedTimeMap.remove(evt.getKeyCode());
173             detectedKeyEvents.add(evt);
174         }
175     }
176
177     @Override
178     public void keyTyped(final KeyEvent e) {
179     }
180
181     @Override
182     public void mouseClicked(final java.awt.event.MouseEvent e) {
183         synchronized (detectedMouseEvents) {
184             detectedMouseEvents.add(new MouseEvent(e.getX(), e.getY(), e.getButton()));
185         }
186     }
187
188     @Override
189     public void mouseDragged(final java.awt.event.MouseEvent evt) {
190         final Point2D mouseLocation = new Point2D(evt.getX(), evt.getY());
191
192         if (oldMouseCoordinatesWhenDragging == null) {
193             oldMouseCoordinatesWhenDragging = mouseLocation;
194             return;
195         }
196
197         mouseDraggedDirection.add(mouseLocation.clone().subtract(oldMouseCoordinatesWhenDragging));
198
199         oldMouseCoordinatesWhenDragging = mouseLocation;
200     }
201
202     @Override
203     public void mouseEntered(final java.awt.event.MouseEvent e) {
204         mouseWithinWindow = true;
205     }
206
207     @Override
208     public synchronized void mouseExited(final java.awt.event.MouseEvent e) {
209         mouseWithinWindow = false;
210         currentMouseLocation = null;
211     }
212
213     @Override
214     public synchronized void mouseMoved(final java.awt.event.MouseEvent e) {
215         currentMouseLocation = new Point2D(e.getX(), e.getY());
216         mouseMoved = true;
217     }
218
219     @Override
220     public void mousePressed(final java.awt.event.MouseEvent e) {
221     }
222
223     @Override
224     public void mouseReleased(final java.awt.event.MouseEvent evt) {
225         oldMouseCoordinatesWhenDragging = null;
226     }
227
228     @Override
229     public void mouseWheelMoved(final java.awt.event.MouseWheelEvent evt) {
230         wheelMovedDirection += evt.getWheelRotation();
231     }
232
233     /**
234      * @return <code>true</code> if view needs to be repainted.
235      */
236     private boolean handleMouseVerticalScrolling() {
237         final Avatar avatar = viewPanel.getAvatar();
238         final double actualAcceleration = 50 * avatar.avatarAcceleration * (1 + (avatar.getMovementSpeed() / 10));
239         avatar.getMovementVector().y += (wheelMovedDirection * actualAcceleration);
240         avatar.enforceSpeedLimit();
241         boolean repaintNeeded = wheelMovedDirection != 0;
242         wheelMovedDirection = 0;
243         return repaintNeeded;
244     }
245
246     /**
247      * @return <code>true</code> if view needs to be repainted.
248      */
249     private boolean handleMouseDragging() {
250         // TODO: need to detect whether user moved mouse or touch screen
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 }