--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.humaninput;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.gui.Avatar;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.gui.ViewRenderListener;
+
+import javax.swing.*;
+import java.awt.event.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class is responsible for tracking human input devices (keyboard, mouse, etc.) and
+ * forwarding those inputs to subsequent virtual components.
+ */
+public class HIDEventTracker implements
+ MouseMotionListener, KeyListener, MouseListener, MouseWheelListener, ViewRenderListener {
+
+ /**
+ * <p> Map of pressed keys. </p>
+ * <p> Key is mouse button code. </p>
+ * <p> Value is system milliseconds when button was pressed. </p>
+ * <p> So by reading the map one can determine currently pressed buttons as well as duration. </p>
+ */
+ private final Map<Integer, Long> pressedKeysToPressedTimeMap = new HashMap<>();
+ private final List<MouseEvent> detectedMouseEvents = new ArrayList<>();
+ private final List<KeyEvent> detectedKeyEvents = new ArrayList<>();
+ private final Point2D mouseDraggedDirection = new Point2D();
+ private final ViewPanel viewPanel;
+ private int wheelMovedDirection = 0;
+ private Point2D oldMouseCoordinatesWhenDragging;
+ private Point2D currentMouseLocation;
+ private boolean mouseMoved;
+ private boolean mouseWithinWindow = false;
+
+ /**
+ * Construct new tracker for specified panel.
+ */
+ public HIDEventTracker(final ViewPanel viewPanel) {
+ this.viewPanel = viewPanel;
+ bind(viewPanel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean beforeRender(final ViewPanel viewPanel, final int millisecondsSinceLastFrame) {
+ boolean viewUpdateNeeded = handleKeyboardEvents();
+ viewUpdateNeeded |= handleMouseClicksAndHover(viewPanel);
+ viewUpdateNeeded |= handleMouseDragging();
+ viewUpdateNeeded |= handleMouseVerticalScrolling();
+ return viewUpdateNeeded;
+ }
+
+ /**
+ * Bind this tracker to specified panel.
+ * @param panel panel to bind to.
+ */
+ private void bind(final JPanel panel) {
+ panel.addMouseMotionListener(this);
+
+ panel.addKeyListener(this);
+
+ panel.addMouseListener(this);
+
+ panel.addMouseWheelListener(this);
+ }
+
+ /**
+ * @return <code>true</code> if view needs to be repainted.
+ */
+ private boolean handleKeyboardEvents() {
+ final KeyboardInputHandler currentFocusOwner = viewPanel.getKeyboardFocusStack().getCurrentFocusOwner();
+ ArrayList<KeyEvent> unprocessedKeyboardEvents = getUnprocessedKeyboardEvents();
+
+ return currentFocusOwner != null
+ && forwardKeyboardEventsToFocusOwner(currentFocusOwner, unprocessedKeyboardEvents);
+ }
+
+ private ArrayList<KeyEvent> getUnprocessedKeyboardEvents() {
+ synchronized (detectedKeyEvents) {
+ ArrayList<KeyEvent> result = new ArrayList<>(detectedKeyEvents);
+ detectedKeyEvents.clear();
+ return result;
+ }
+ }
+
+ /**
+ * @return <code>true</code> if view update is needed.
+ */
+ private boolean forwardKeyboardEventsToFocusOwner(
+ KeyboardInputHandler currentFocusOwner, ArrayList<KeyEvent> keyEvents) {
+ boolean viewUpdateNeeded = false;
+
+ for (KeyEvent keyEvent : keyEvents)
+ viewUpdateNeeded |= processKeyEvent(currentFocusOwner, keyEvent);
+
+ return viewUpdateNeeded;
+ }
+
+ private boolean processKeyEvent(KeyboardInputHandler currentFocusOwner, KeyEvent keyEvent) {
+ switch (keyEvent.getID()) {
+ case KeyEvent.KEY_PRESSED:
+ return currentFocusOwner.keyPressed(keyEvent, viewPanel);
+
+ case KeyEvent.KEY_RELEASED:
+ return currentFocusOwner.keyReleased(keyEvent, viewPanel);
+ }
+ return false;
+ }
+
+ /**
+ * @return <code>true</code> if view needs to be repainted.
+ */
+ private synchronized boolean handleMouseClicksAndHover(final ViewPanel viewPanel) {
+ boolean rerenderNeeded = false;
+ MouseEvent event = findClickLocationToTrace();
+ if (event != null) {
+ // process mouse clicks as a first priority
+ rerenderNeeded = true;
+ } else {
+ // when there are no mouse clicks, process mouse hovering
+
+ if (mouseMoved) {
+ mouseMoved = false;
+ // we would like to re-render frame when user moved mouse, to see what objects mouse is hovering over
+ rerenderNeeded = true;
+ }
+
+ if (currentMouseLocation != null) {
+ // mouse click with button 0 amounts to mouse hovering event
+ event = new MouseEvent(currentMouseLocation, 0);
+ }
+ }
+
+ if (viewPanel.getRenderingContext() != null)
+ viewPanel.getRenderingContext().setMouseEvent(event);
+
+ return rerenderNeeded;
+ }
+
+ private MouseEvent findClickLocationToTrace() {
+ synchronized (detectedMouseEvents) {
+ if (detectedMouseEvents.isEmpty())
+ return null;
+
+ return detectedMouseEvents.remove(0);
+ }
+ }
+
+ boolean isKeyPressed(final int keyCode) {
+ return pressedKeysToPressedTimeMap.containsKey(keyCode);
+ }
+
+ @Override
+ public void keyPressed(final KeyEvent evt) {
+ synchronized (detectedKeyEvents) {
+ pressedKeysToPressedTimeMap.put(evt.getKeyCode(), System.currentTimeMillis());
+ detectedKeyEvents.add(evt);
+ }
+ }
+
+ @Override
+ public void keyReleased(final KeyEvent evt) {
+ synchronized (detectedKeyEvents) {
+ pressedKeysToPressedTimeMap.remove(evt.getKeyCode());
+ detectedKeyEvents.add(evt);
+ }
+ }
+
+ @Override
+ public void keyTyped(final KeyEvent e) {
+ }
+
+ @Override
+ public void mouseClicked(final java.awt.event.MouseEvent e) {
+ synchronized (detectedMouseEvents) {
+ detectedMouseEvents.add(new MouseEvent(e.getX(), e.getY(), e.getButton()));
+ }
+ }
+
+ @Override
+ public void mouseDragged(final java.awt.event.MouseEvent evt) {
+ final Point2D mouseLocation = new Point2D(evt.getX(), evt.getY());
+
+ if (oldMouseCoordinatesWhenDragging == null) {
+ oldMouseCoordinatesWhenDragging = mouseLocation;
+ return;
+ }
+
+ mouseDraggedDirection.add(mouseLocation.clone().subtract(oldMouseCoordinatesWhenDragging));
+
+ oldMouseCoordinatesWhenDragging = mouseLocation;
+ }
+
+ @Override
+ public void mouseEntered(final java.awt.event.MouseEvent e) {
+ mouseWithinWindow = true;
+ }
+
+ @Override
+ public synchronized void mouseExited(final java.awt.event.MouseEvent e) {
+ mouseWithinWindow = false;
+ currentMouseLocation = null;
+ }
+
+ @Override
+ public synchronized void mouseMoved(final java.awt.event.MouseEvent e) {
+ currentMouseLocation = new Point2D(e.getX(), e.getY());
+ mouseMoved = true;
+ }
+
+ @Override
+ public void mousePressed(final java.awt.event.MouseEvent e) {
+ }
+
+ @Override
+ public void mouseReleased(final java.awt.event.MouseEvent evt) {
+ oldMouseCoordinatesWhenDragging = null;
+ }
+
+ @Override
+ public void mouseWheelMoved(final java.awt.event.MouseWheelEvent evt) {
+ wheelMovedDirection += evt.getWheelRotation();
+ }
+
+ /**
+ * @return <code>true</code> if view needs to be repainted.
+ */
+ private boolean handleMouseVerticalScrolling() {
+ final Avatar avatar = viewPanel.getAvatar();
+ final double actualAcceleration = 50 * avatar.avatarAcceleration * (1 + (avatar.getMovementSpeed() / 10));
+ avatar.getMovementVector().y += (wheelMovedDirection * actualAcceleration);
+ avatar.enforceSpeedLimit();
+ boolean repaintNeeded = wheelMovedDirection != 0;
+ wheelMovedDirection = 0;
+ return repaintNeeded;
+ }
+
+ /**
+ * @return <code>true</code> if view needs to be repainted.
+ */
+ private boolean handleMouseDragging() {
+ // TODO: It would be nice here to detect somehow whether user moved mouse or touch screen.
+ // in case of touch screen, we would like to reverse movement along X and Y axis.
+
+ final Avatar avatar = viewPanel.getAvatar();
+ // for mouse
+ avatar.setAngleXZ(avatar.getAngleXZ() - ((float) mouseDraggedDirection.x / 50));
+ avatar.setAngleYZ(avatar.getAngleYZ() - ((float) mouseDraggedDirection.y / 50));
+
+ // for touch screen
+ // avatar.setAngleXZ(avatar.getAngleXZ() + ((float)
+ // mouseDraggedDirection.x / 50));
+ // avatar.setAngleYZ(avatar.getAngleYZ() + ((float)
+ // mouseDraggedDirection.y / 50));
+
+ boolean viewUpdateNeeded = !mouseDraggedDirection.isZero();
+ mouseDraggedDirection.zero();
+ return viewUpdateNeeded;
+ }
+
+}