Improved code readability
[sixth-3d.git] / src / main / java / eu / svjatoslav / sixth / e3d / gui / humaninput / HIDEventTracker.java
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/HIDEventTracker.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/HIDEventTracker.java
new file mode 100755 (executable)
index 0000000..19dee50
--- /dev/null
@@ -0,0 +1,271 @@
+/*
+ * 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;
+    }
+
+}