/* * Util.InputHandler.cs * Authors: Adam Nabinger * Copyright (c) 2007-2008 Cornell University This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; namespace Util { /// /// Which action on the given key triggers the event. /// public enum Trigger { /// /// The trigger was the press of a key. /// Activated = 0, /// /// The trigger was that the key was released. /// Deactivated = 1, } /// /// Which button on the mouse. /// public enum MouseButton { /// /// The Left Mouse Button. /// Left = 0, /// /// The Right Mouse Button. /// Right = 1 } /// /// Util.InputHandler processes keyboard and mouse input, generating InputEvents. /// Multiple InputHandlers may run simultaneously with different binding without conflicting. /// public sealed class InputHandler : IDisposable { #region Constants // Constant for the number of keys traditionally used on keyboards private const Int32 keynum = 256; // IntPtrs can not be constant. private readonly IntPtr DoNotPropagate = new IntPtr(1); // Character used for separating file names internal const char FileSeparator = '\t'; // String constant for representing the mouse internal const string MouseKey = "mouse"; // String constant for representing a released key internal const string ReleasedKey = "released"; #endregion #region Fields // Reference to the game the created this private readonly Game myGame; // Array of keys that have been pressed private readonly Int32[] keyPressedBindings = new Int32[keynum]; // Array of keys that have been released private readonly Int32[] keyReleasedBindings = new Int32[keynum]; // Array of keys that are currently being held down private readonly bool[] keyDown = new bool[keynum]; // Array of mouse inputs private readonly Int32[] mouseBindings = new Int32[6]; // Array for left and right mouse buttons and whether they are being held down or not private readonly bool[] mouseDown = new bool[2]; // Queue of stored input events to be processed private readonly Queue inputEvents = new Queue(); // The last Input Event that was passed into the game logic. This will be written to the log file automatically in the event that the game logic does not. private InputEvent LastEvent; // The Logger the Input Handler should use for logging Input Events. private readonly Logger logger; // Whether or not this has been initialized private bool initialized; // Whether or not this has been disposed private bool disposed; // Whether or not auto disabling has been enable private bool auto_disable; // Whether or not this has been disabled private bool disabled; // The current location of the mouse cursor on screen private Point mouselocation; // The window in which the mouse can move within Rectangle window; // Whether or not all received input events should be processed private bool reportAll; // Whether or not to log the case of any key event, i.e. lower or upper case private bool reportCase; // Whether or not parallel port has been enabled private readonly bool parallelPortEnabled; // Whether or not events should be written out to the parallel port private bool writeToParallel; // Should certain keys be eaten. #if DEBUG private bool shouldEatKeys = false; #else private bool shouldEatKeys = true; #endif // A list of keys to be eaten. 9 = Tab, 91 = Windows Key // This is /Not/ Recommended by Microsoft, and should only be used as a workaround until the game supports alt-tabbing, etc. private readonly Int32[] eatKeys = {9, 91}; #endregion #region Properties /// /// When Enabled, all key presses will generate InputEvents. /// Unbound keys will report EventCode.Null /// Default = false /// public bool ReportAllEvents { get { return reportAll; } set { reportAll = value; } } /// /// The mouse's present location. /// public Point MouseLocation { get { return mouselocation; } } /// /// Report the case of the input in the KeyInputEvents. /// At the moment, events are case-insensative, so this only matters if gathering keypresses for text. /// If false, all events will be recorded as lowercase. /// Default = false /// public bool ReportCase { get { return reportCase; } set { reportCase = value; } } /// /// Will event codes be written to the parallel port, if possible. /// public bool ParallelPortEnabled { get { return parallelPortEnabled; } } /// /// If true; All the events registered by this Util.InputHandler will be written to the Parallel Port, if their event code falls in the range 1-255. /// Setting this to true represents a request to open the Parallel Port, if an error occurs it will remain false. /// The inputHandler must have been constructed with parallel enable set to true; /// public bool WriteToParallel { get { return writeToParallel; } set { if (parallelPortEnabled) { writeToParallel = value; } } } #endregion #region Enabling / Disabling /// /// Enable this Util.InputHandler to resume processing input. /// public void Enable() { disabled = false; } /// /// When Disabled, this Util.InputHandler will not process or generate InputEvents. /// public void Disable() { disabled = true; for (Int32 i = 0; i < keynum; ++i) { keyDown[i] = false; } for (Int32 i = 0; i < 2; ++i) { mouseDown[i] = false; } } /// /// Automatically Disable this inputHandler when the given game loses focus. /// Also Automatically re-enables. /// public void AutoDisable() { if (auto_disable) { return; } auto_disable = true; myGame.Activated += game_Activated; myGame.Deactivated += game_Deactivated; } void game_Activated(object sender, EventArgs e) { Enable(); } void game_Deactivated(object sender, EventArgs e) { Disable(); } #endregion #region Creation /// /// Create a new inputhandler. /// /// The Game reference, for getting the window location for mouse events. /// internal InputHandler(Game Game, Logger TheLogger) { myGame = Game; logger = TheLogger; parallelPortEnabled = Settings.ParallelPortEnabled; myGame.Window.ClientSizeChanged += Window_ClientSizeChanged; window = myGame.Window.ClientBounds; } #endregion #region Destruction /// /// An Inputhandler should be disposed when it's finished with. /// This finalizer will attempt cleanup, and complain if appropriate. /// [DebuggerHidden] ~InputHandler() { #if DEBUG if (disposed) { return; } #endif Dispose(); #if DEBUG Console.WriteLine("Inputhandler was not disposed!"); MainGame.LogError("Inputhandler was not disposed!"); throw new InvalidOperationException("Inputhandler was not disposed!"); #endif } /// /// Release the hooks held by Util.InputHandler, no further input events will be registered. /// This will be called automatically by XNA. /// Calling it twice will have no effect. /// [DebuggerHidden] public void Dispose() { if (initialized && !disposed) { disposed = true; NativeMethods.UnhookWindowsHookEx(_keyboardHookID); _keyboardHookID = IntPtr.Zero; NativeMethods.UnhookWindowsHookEx(_mouseHookID); _mouseHookID = IntPtr.Zero; if (auto_disable) { myGame.Activated -= game_Activated; myGame.Deactivated -= game_Deactivated; } CheckLastEvent(); GC.SuppressFinalize(this); } } #endregion #region Initialization /// /// Set hooks Util.InputHandler uses to gather input events. /// This will be called automatically by XNA. /// Calling it twice is invalid. /// public void Initialize() { if (!initialized && !disposed) { _keyboardProc = KeyCallback; _mouseProc = MouseCallback; using (Process curProcess = Process.GetCurrentProcess()) using (ProcessModule curModule = curProcess.MainModule) { if (curModule == null) { throw new NotSupportedException("Unable to access process module. InputHandler will not be able to listen for events."); } IntPtr handle = NativeMethods.GetModuleHandle(curModule.ModuleName); _keyboardHookID = NativeMethods.SetWindowsHookEx(WH_KEYBOARD_LL, _keyboardProc, handle, 0); _mouseHookID = NativeMethods.SetWindowsHookEx(WH_MOUSE_LL, _mouseProc, handle, 0); } if (parallelPortEnabled) { writeToParallel = true; } initialized = true; } } #endregion #region Window Update /// /// /// /// /// void Window_ClientSizeChanged(object sender, EventArgs e) { window = ((GameWindow)sender).ClientBounds; } /// /// Force a window update, for the purposes of mouse location. /// /// public void ForceUpdateWindow(Rectangle windowLocation) { window = windowLocation; } #endregion #region Binding Management /// /// Bind an event to the specified keyboard key. /// /// The Key on the keyboard. /// If the event is triggered when the key is pressed or released. /// The event to trigger. public void BindButton(Keys key, Trigger trigger, Int32 eventCode) { switch (trigger) { case Trigger.Activated: keyPressedBindings[(Int32)key] = eventCode; break; case Trigger.Deactivated: keyReleasedBindings[(Int32)key] = eventCode; break; } } /// /// Bind an event to the specified mouse key. /// /// The Key of the Util.Mouse. /// If the event is triggered when the key is pressed or released. /// The event to trigger. public void BindButton(MouseButton button, Trigger trigger, Int32 eventCode) { mouseBindings[((Int32)button * 2) + (Int32)trigger] = eventCode; } /// /// Bind an event to movement of the input device. /// public void BindMovement(Trigger trigger, Int32 eventCode) { mouseBindings[Enum.GetValues(typeof(MouseButton)).GetLength(0) * 2 + (Int32)trigger] = eventCode; } /// /// Load bindings of the given type, from the given file. /// /// The event code enum to use /// Path name for the file to load the current bindings from public void LoadBindings(Type eventCodeType, string fileName) { using (FileStream stream = new FileStream(fileName,FileMode.Open, FileAccess.Read, FileShare.Read)) using (TextReader reader = new StreamReader(stream)) { string line; while ((line = reader.ReadLine()) != null) { string[] data = line.Split(new char[] { FileSeparator }, StringSplitOptions.RemoveEmptyEntries); if (line.Contains(MouseKey)) { if (line.Contains(ReleasedKey)) { mouseBindings[((Int32)Enum.Parse(typeof(MouseButton), data[1], true) * 2) + 1] = (Int32)Enum.Parse(eventCodeType, data[0], true); } else { mouseBindings[((Int32)Enum.Parse(typeof(MouseButton), data[1], true) * 2)] = (Int32)Enum.Parse(eventCodeType, data[0], true); } } else { if (line.Contains(ReleasedKey)) { keyReleasedBindings[(Int32)Enum.Parse(typeof(Keys), data[1], true)] = (Int32)Enum.Parse(eventCodeType, data[0], true); } else { keyPressedBindings[(Int32)Enum.Parse(typeof(Keys), data[1], true)] = (Int32)Enum.Parse(eventCodeType, data[0], true); } } } } } /// /// Save bindings of the given type, to the given file. /// /// The event code enum to use /// Path name for the file to save the current bindings to public void SaveBindings(Type eventCodeType, string fileName) { using (FileStream stream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None)) using (TextWriter writer = new StreamWriter(stream)) { for (Int32 m = 0; m < 2; ++m) { if (mouseBindings[m * 2] != 0) { writer.Write(Enum.ToObject(eventCodeType, mouseBindings[m * 2]).ToString()); writer.Write(FileSeparator); writer.Write(((MouseButton)m).ToString()); writer.Write(FileSeparator); writer.WriteLine(MouseKey); } if (mouseBindings[(m * 2) + 1] != 0) { writer.Write(Enum.ToObject(eventCodeType, mouseBindings[(m * 2) + 1]).ToString()); writer.Write(FileSeparator); writer.Write(((MouseButton)m).ToString()); writer.Write(FileSeparator); writer.Write(MouseKey); writer.Write(FileSeparator); writer.WriteLine(ReleasedKey); } } for (Int32 i = 0; i < keynum; ++i) { if (keyPressedBindings[i] != 0) { writer.Write(Enum.ToObject(eventCodeType, keyPressedBindings[i]).ToString()); writer.Write(FileSeparator); writer.WriteLine(((Keys)i).ToString()); } if (keyReleasedBindings[i] != 0) { writer.Write(Enum.ToObject(eventCodeType, keyReleasedBindings[i]).ToString()); writer.Write(FileSeparator); writer.Write(((Keys)i).ToString()); writer.Write(FileSeparator); writer.WriteLine(ReleasedKey); } } } } #endregion #region Getters /// /// Search through the list of bindings, and return all keys bound to given event. /// /// The numeric value of the event get the key with /// The collection of keys associated to the given event public ICollection GetBoundKeys(Int32 eventCode) { ICollection ret = new List(); for (Int32 i = 0; i < keynum; ++i) { if (keyPressedBindings[i] == eventCode) { ret.Add((Keys)i); } } return ret; } /// /// Search through the list of bindings, and return the first key bound to the given event, or Keys.None. /// /// The numeric value of the event get the key with /// The key associated to the given event public Keys GetBoundKey(Int32 eventCode) { for (Int32 i = 0; i < keynum; ++i) { if (keyPressedBindings[i] == eventCode) { return (Keys)i; } } return Keys.None; } /// /// Gets the dictionary of key bindings /// TODO: temp /// /// The dictionary of event code to key bindings internal Dictionary GetBindings() { Dictionary dict = new Dictionary(); for (Int32 i = 0; i < keynum; ++i) { if (keyPressedBindings[i] != 0) { dict.Add(keyPressedBindings[i], (Keys)i); } } return dict; } /// /// Get the next input event that occured. /// Will throw InvalidOperationException if HasEvent would return false. /// This method should never return null. /// /// An Util.InputEvent. [DebuggerHidden] public InputEvent GetNextEvent() { CheckLastEvent(); LastEvent = inputEvents.Dequeue(); return LastEvent; } [DebuggerHidden] private void CheckLastEvent() { // If we have an event, and it would have been written out to the parallel port, try to write it to the logger. if (LastEvent != null && !LastEvent.Logged && writeToParallel && LastEvent.EventCode > 0 && (LastEvent.EventCode & 4294967040) == 0) { #if DEBUG throw new Exception("GetNextEvent() called without logging the last event given. (Unlogged EventCode: " + LastEvent.EventCode + ", Pressed " + ((Stopwatch.GetTimestamp() - LastEvent.Timestamp) / (double)Stopwatch.Frequency).ToString("##0.000") + " seconds ago) This will only throw an Exception in Debug Mode."); #else LastEvent.WriteToLog(logger, "Unlogged_Event"); LastEvent = null; #endif } } #endregion #region Clearers /// /// Remove all pending events. /// public void ClearEvents() { inputEvents.Clear(); } /// /// Removes all the current key bindings /// public void ClearKeyBindings() { for (int i = keynum - 1; i >= 0; --i) { keyPressedBindings[i] = 0; keyReleasedBindings[i] = 0; } } /// /// Removes all the current mouse bindings /// public void ClearMouseBindings() { for (int i = 3; i >= 0; --i) { mouseBindings[i] = 0; } } #endregion #region Checkers /// /// Check if there is at least one event waiting to be processed. /// /// True, if an event is available. public bool HasEvent() { return inputEvents.Count > 0; } /// /// For ease of converting older code, this method is provided. /// Use of the InputEvents is recommended. /// /// The key that is being checked [Obsolete("Event based processing is strongly recommended; however; this method is functional.")] public bool IsKeyDown(Keys key) { if (disabled) { return false; } return keyDown[(Int32)key]; } #endregion #region Callbacks /// /// Callback for key input events /// /// Numeric value representation of the associated event code /// /// /// private IntPtr KeyCallback(Int32 nCode, IntPtr wParam, IntPtr lParam) { long timestamp = Stopwatch.GetTimestamp(); if (disabled || nCode < 0) { return NativeMethods.CallNextHookEx(_keyboardHookID, nCode, wParam, lParam); } KeyStruct input = (KeyStruct)Marshal.PtrToStructure(lParam, typeof(KeyStruct)); if (shouldEatKeys) { foreach (int key in eatKeys) { if (input.vkCode == key) { return DoNotPropagate; } } } switch ((KeyboardMessages)wParam) { case KeyboardMessages.WM_KEYDOWN: if (!keyDown[input.vkCode]) { Int32 code = keyPressedBindings[input.vkCode]; if (writeToParallel && code > 0 && (code & 4294967040) == 0) { logger.parallelPortWriter.Write((byte)code); } if (reportAll || code != 0) { if (reportCase) { inputEvents.Enqueue(new KeyInputEvent(code, timestamp, input.time, Trigger.Activated, (Keys)input.vkCode, (NativeMethods.GetKeyState((int)VirtualKeys.VK_SHIFT) < 0 ^ NativeMethods.GetKeyState((int)VirtualKeys.VK_CAPITAL) > 0))); } else { inputEvents.Enqueue(new KeyInputEvent(code, timestamp, input.time, Trigger.Activated, (Keys)input.vkCode)); } } keyDown[input.vkCode] = true; } break; case KeyboardMessages.WM_KEYUP: if (keyDown[input.vkCode]) { Int32 code = keyReleasedBindings[input.vkCode]; if (writeToParallel && code > 0 && (code & 4294967040) == 0) { logger.parallelPortWriter.Write((byte)code); } if (reportAll || code != 0) { if (reportCase) { inputEvents.Enqueue(new KeyInputEvent(code, timestamp, input.time, Trigger.Deactivated, (Keys)input.vkCode, (NativeMethods.GetKeyState((int)VirtualKeys.VK_SHIFT) < 0 ^ NativeMethods.GetKeyState((int)VirtualKeys.VK_CAPITAL) > 0))); } else { inputEvents.Enqueue(new KeyInputEvent(code, timestamp, input.time, Trigger.Deactivated, (Keys)input.vkCode)); } } keyDown[input.vkCode] = false; } break; } return NativeMethods.CallNextHookEx(_keyboardHookID, nCode, wParam, lParam); } /// /// Callback for mouse input events /// /// Numeric value representation of the associated event code /// /// /// private IntPtr MouseCallback(Int32 nCode, IntPtr wParam, IntPtr lParam) { long timestamp = Stopwatch.GetTimestamp(); if (disabled || nCode < 0) { return NativeMethods.CallNextHookEx(_mouseHookID, nCode, wParam, lParam); } MouseStruct input = (MouseStruct)Marshal.PtrToStructure(lParam, typeof(MouseStruct)); Int32 oldX = mouselocation.X; Int32 oldY = mouselocation.Y; Int32 x = input.x - window.X; Int32 y = input.y - window.Y; mouselocation.X = x; mouselocation.Y = y; switch ((MouseMessages)wParam) { case MouseMessages.WM_LBUTTONPRESSED: if (mouseDown[(Int32)MouseButton.Left]) { break; } Int32 lDownCode = mouseBindings[0]; if (lDownCode != 0) { if (writeToParallel && (lDownCode & 4294967040) == 0) { logger.parallelPortWriter.Write((byte)lDownCode); } inputEvents.Enqueue(new MouseInputEvent(lDownCode, timestamp, input.time, Trigger.Activated, x, y)); } else if (reportAll) { inputEvents.Enqueue(new MouseInputEvent(lDownCode, timestamp, input.time, Trigger.Activated, x, y)); } mouseDown[(Int32)MouseButton.Left] = true; break; case MouseMessages.WM_LBUTTONRELEASED: if (!mouseDown[(Int32)MouseButton.Left]) { break; } Int32 lUpCode = mouseBindings[1]; if (lUpCode != 0) { if (writeToParallel && (lUpCode & 4294967040) == 0) { logger.parallelPortWriter.Write((byte)lUpCode); } inputEvents.Enqueue(new MouseInputEvent(lUpCode, timestamp, input.time, Trigger.Deactivated, x, y)); } else if (reportAll) { inputEvents.Enqueue(new MouseInputEvent(lUpCode, timestamp, input.time, Trigger.Deactivated, x, y)); } mouseDown[(Int32)MouseButton.Left] = false; break; case MouseMessages.WM_RBUTTONPRESSED: if (mouseDown[(Int32)MouseButton.Right]) { break; } Int32 rDownCode = mouseBindings[2]; if (rDownCode != 0) { if (writeToParallel && (rDownCode & 4294967040) == 0) { logger.parallelPortWriter.Write((byte)rDownCode); } inputEvents.Enqueue(new MouseInputEvent(rDownCode, timestamp, input.time, Trigger.Activated, x, y)); } else if (reportAll) { inputEvents.Enqueue(new MouseInputEvent(rDownCode, timestamp, input.time, Trigger.Activated, x, y)); } mouseDown[(Int32)MouseButton.Right] = true; break; case MouseMessages.WM_RBUTTONRELEASED: if (!mouseDown[(Int32)MouseButton.Right]) { break; } Int32 rUpCode = mouseBindings[3]; if (rUpCode != 0) { if (writeToParallel && (rUpCode & 4294967040) == 0) { logger.parallelPortWriter.Write((byte)rUpCode); } inputEvents.Enqueue(new MouseInputEvent(rUpCode, timestamp, input.time, Trigger.Deactivated, x, y)); } else if (reportAll) { inputEvents.Enqueue(new MouseInputEvent(rUpCode, timestamp, input.time, Trigger.Deactivated, x, y)); } mouseDown[(Int32)MouseButton.Right] = false; break; case MouseMessages.WM_MOVEMENT: // Send mouse_out to last location and mouse_over to new location inputEvents.Enqueue(new MouseInputEvent(mouseBindings[5], timestamp, input.time, Trigger.Deactivated, oldX, oldY)); inputEvents.Enqueue(new MouseInputEvent(mouseBindings[4], timestamp, input.time, Trigger.Activated, x, y)); break; } return NativeMethods.CallNextHookEx(_mouseHookID, nCode, wParam, lParam); } #endregion #region Low-level Magic: private NativeMethods.LowLevelProc _keyboardProc; private NativeMethods.LowLevelProc _mouseProc; private IntPtr _keyboardHookID = IntPtr.Zero; private IntPtr _mouseHookID = IntPtr.Zero; private const Int32 WH_KEYBOARD_LL = 13; private const Int32 WH_MOUSE_LL = 14; private enum KeyboardMessages { WM_KEYDOWN = 0x0100, WM_KEYUP = 0x0101, //WM_SYSKEYDOWN = 0x0104 } private enum MouseMessages { WM_MOVEMENT = 0x0200, WM_LBUTTONPRESSED = 0x0201, WM_LBUTTONRELEASED = 0x0202, WM_RBUTTONPRESSED = 0x0204, WM_RBUTTONRELEASED = 0x0205, //WM_WHEELPRESSED = 0x207, //WM_WHEELRELEASED = 0x208, //WM_WHEELMOVED = 0x020A } private enum VirtualKeys { VK_SHIFT = 0x10, VK_CAPITAL = 0x14 } [StructLayout(LayoutKind.Sequential)] private struct KeyStruct { internal UInt32 vkCode; internal UInt32 scanCode; internal UInt32 flags; internal UInt32 time; internal UIntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct MouseStruct { internal Int32 x; internal Int32 y; internal UInt32 mouseData; internal UInt32 flags; internal UInt32 time; internal IntPtr dwExtraInfo; } #endregion } } #if DEBUG /// /// An overlay for direct mouse access, use is discouraged within this program. /// [Obsolete("Adam says: Use Util.InputHandler instead!")] public static class Mouse { /// /// Gets the current state of the mouse, including mouse position and buttons pressed. /// /// Current state of the mouse. public static MouseState GetState() { return Microsoft.Xna.Framework.Input.Mouse.GetState(); } } /// /// An overlay for direct keyboard access, use is discouraged within this program. /// [Obsolete("Adam says: Use Util.InputHandler instead!")] public static class Keyboard { /// /// Returns the current keyboard state. /// /// Current keyboard state. public static KeyboardState GetState() { return Microsoft.Xna.Framework.Input.Keyboard.GetState(); } } #endif