diff --git a/assets/Controls/Default.controls b/assets/Controls/Default.controls index 0c0d9e75f..db9d111d7 100644 --- a/assets/Controls/Default.controls +++ b/assets/Controls/Default.controls @@ -1,4 +1,4 @@ -; Current control configuration +; Current control configuration ; ============================= ; This file was automatically generated. Please modify only if you know what you're doing. ; Last updated 2026.01.16 @@ -79,6 +79,7 @@ CAMERA_ZOOM_OUT, keyboard, Keypad0, 0 CAMERA_POI_PREVIOUS, keyboard, Keypad1, 0 CAMERA_POI_NEXT, keyboard, Keypad7, 0 CAMERA_RESET, keyboard, Keypad5, 0 +CAMERA_GRAB_TOGGLE, mouse, 2, 0 CAMERA_RESTRICTION, keyboard, R, 2 TIMETABLE_TOGGLE, keyboard, T, 2 TIMETABLE_UP, keyboard, Up, 2 diff --git a/assets/Languages/en-US.xlf b/assets/Languages/en-US.xlf index de51fd26f..66d9202ba 100755 --- a/assets/Languages/en-US.xlf +++ b/assets/Languages/en-US.xlf @@ -900,6 +900,30 @@ positive direction + + Mouse: + + + Button: + + + Mouse + + + Left Click + + + Middle Click + + + Right Click + + + Scroll Up + + + Scroll Down + invalid direction @@ -1222,6 +1246,9 @@ Transition duration (sec): + + Zoom Scroll Speed: + Detail of simulation @@ -1315,6 +1342,7 @@ Prefer custom timetable + Choose... @@ -2238,6 +2266,9 @@ Resets the camera view to default values + + Toggles camera grab (Mouse look) + Activates or deactivates interior view camera restriction diff --git a/assets/Languages/id-ID.xlf b/assets/Languages/id-ID.xlf index c815562d1..15ac67843 100644 --- a/assets/Languages/id-ID.xlf +++ b/assets/Languages/id-ID.xlf @@ -1156,6 +1156,38 @@ positive direction arah positif + + Mouse: + Mouse: + + + Button: + Tombol: + + + Mouse + Mouse + + + Left Click + Klik Kiri + + + Middle Click + Klik Tengah + + + Right Click + Klik Kanan + + + Scroll Up + Scroll Atas + + + Scroll Down + Scroll Bawah + invalid direction arah tidak valid @@ -1489,6 +1521,26 @@ Miscellaneous Lainnya + + Camera options + Pengaturan kamera + + + Smooth interior transition + Transisi interior halus + + + Smooth exterior transition + Transisi eksterior halus + + + Transition duration (sec): + Durasi transisi (detik): + + + Zoom Scroll Speed: + Kecepatan Scroll Zoom: + Detail of simulation Detail simulasi @@ -1611,6 +1663,7 @@ Prefer custom timetable Buat sendiri + Choose... Pilih.... @@ -2808,6 +2861,10 @@ Resets the camera view to default values Reset kamera + + Toggles camera grab (Mouse look) + Aktifkan/nonaktifkan kontrol kamera dengan mouse + Activates or deactivates interior view camera restriction Aktifkan / matikan batasan kamera diff --git a/source/OpenBVE/Game/Menu/Menu.Controls.cs b/source/OpenBVE/Game/Menu/Menu.Controls.cs index 4777281e2..e6d4abbb3 100644 --- a/source/OpenBVE/Game/Menu/Menu.Controls.cs +++ b/source/OpenBVE/Game/Menu/Menu.Controls.cs @@ -1,4 +1,4 @@ -using LibRender2.Primitives; +using LibRender2.Primitives; using OpenBveApi.Colors; using OpenBveApi.Hosts; using OpenBveApi.Interface; @@ -71,6 +71,19 @@ private static string GetControlDescription(int idx) case ControlMethod.Invalid: str = Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "menu", "joystick_notavailable" }); break; + case ControlMethod.Mouse: + str = Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse" }) + " ["; + switch (loadedControl.Element) + { + case MouseElement.Left: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_left" }); break; + case MouseElement.Middle: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_middle" }); break; + case MouseElement.Right: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_right" }); break; + case MouseElement.ScrollUp: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_scrollup" }); break; + case MouseElement.ScrollDown: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_scrolldown" }); break; + default: str += loadedControl.Element; break; + } + str += "]"; + break; } return str; diff --git a/source/OpenBVE/Game/Menu/Menu.cs b/source/OpenBVE/Game/Menu/Menu.cs index 86fe5527f..db0b77e00 100644 --- a/source/OpenBVE/Game/Menu/Menu.cs +++ b/source/OpenBVE/Game/Menu/Menu.cs @@ -222,7 +222,7 @@ public bool IsCustomizingControl() // // SET CONTROL CUSTOM DATA // - internal void SetControlKbdCustomData(Key key, KeyboardModifier keybMod) + internal void SetControlKbdCustomData(Key key, KeyboardModifier keybMod, int modifierOrder) { //Check that we are customising a key, and that our key is NOT the menu back key if (isCustomisingControl && key != MenuBackKey && CustomControlIdx < Interface.CurrentControls.Length) @@ -230,6 +230,7 @@ internal void SetControlKbdCustomData(Key key, KeyboardModifier keybMod) Interface.CurrentControls[CustomControlIdx].Method = ControlMethod.Keyboard; Interface.CurrentControls[CustomControlIdx].Key = key; Interface.CurrentControls[CustomControlIdx].Modifier = keybMod; + Interface.CurrentControls[CustomControlIdx].ModifierOrder = modifierOrder; Interface.SaveControls(null, Interface.CurrentControls); } PopMenu(); diff --git a/source/OpenBVE/System/GameWindow.cs b/source/OpenBVE/System/GameWindow.cs index d139b5595..a96a97188 100644 --- a/source/OpenBVE/System/GameWindow.cs +++ b/source/OpenBVE/System/GameWindow.cs @@ -273,8 +273,8 @@ protected override void OnRenderFrame(FrameEventArgs e) MainLoop.UpdateControlRepeats(RealTimeElapsed); MainLoop.ProcessKeyboard(); - MainLoop.UpdateMouse(RealTimeElapsed); MainLoop.ProcessControls(TimeElapsed); + MainLoop.UpdateMouse(RealTimeElapsed); if (Program.Joysticks.AttachedJoysticks.TryGetTypedValue(AbstractRailDriver.Guid, out AbstractRailDriver railDriver)) { if (Interface.CurrentOptions.RailDriverMPH) diff --git a/source/OpenBVE/System/Input/Controls.cs b/source/OpenBVE/System/Input/Controls.cs index 1945c8a52..4a417321c 100644 --- a/source/OpenBVE/System/Input/Controls.cs +++ b/source/OpenBVE/System/Input/Controls.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -191,7 +191,21 @@ internal static void LoadControls(string FileOrNull, out Control[] Controls) Controls[Length].Key = (OpenBveApi.Input.Key)CurrentKey; Controls[Length].Direction = 0; Controls[Length].Modifier = (KeyboardModifier) Modifiers; - if (Terms.Length >= 5 && int.TryParse(Terms[4], NumberStyles.Integer, Culture, out int Option)) + if (Terms.Length >= 6) + { + // Format including the required modifier press order: key, modifiers, order, option + int Order; + if (!int.TryParse(Terms[4], NumberStyles.Integer, Culture, out Order) || Order < 0) + { + Order = 0; + } + Controls[Length].ModifierOrder = Order; + if (int.TryParse(Terms[5], NumberStyles.Integer, Culture, out int Option)) + { + Controls[Length].Option = Option; + } + } + else if (Terms.Length >= 5 && int.TryParse(Terms[4], NumberStyles.Integer, Culture, out int Option)) { Controls[Length].Option = Option; } @@ -332,6 +346,25 @@ internal static void LoadControls(string FileOrNull, out Control[] Controls) } } + else if (Method == ControlMethod.Mouse & Terms.Length >= 3) + { + if (int.TryParse(Terms[2], out int CurrentButton)) + { + Controls[Length].Method = Method; + Controls[Length].Element = CurrentButton; + Controls[Length].Modifier = KeyboardModifier.None; + Controls[Length].Option = 0; + if (Terms.Length >= 4 && int.TryParse(Terms[3], NumberStyles.Integer, Culture, out int Option)) + { + Controls[Length].Option = Option; + } + if (Terms.Length >= 5 && int.TryParse(Terms[4], NumberStyles.Integer, Culture, out int Modifiers)) + { + Controls[Length].Modifier = (KeyboardModifier) Modifiers; + } + Valid = true; + } + } if (!Valid) { diff --git a/source/OpenBVE/System/Input/Keyboard.cs b/source/OpenBVE/System/Input/Keyboard.cs index 2a0de2933..5c0d56220 100644 --- a/source/OpenBVE/System/Input/Keyboard.cs +++ b/source/OpenBVE/System/Input/Keyboard.cs @@ -11,6 +11,21 @@ internal static partial class MainLoop /// Called when a KeyDown event is generated internal static void KeyDownEvent(object sender, KeyboardKeyEventArgs e) { + switch (e.Key) + { + case Key.ShiftLeft: + case Key.ShiftRight: + PushHeldModifier(KeyboardModifier.Shift); + break; + case Key.ControlLeft: + case Key.ControlRight: + PushHeldModifier(KeyboardModifier.Ctrl); + break; + case Key.AltLeft: + case Key.AltRight: + PushHeldModifier(KeyboardModifier.Alt); + break; + } if (Interface.CurrentOptions.KioskMode && Program.Renderer.CurrentInterface != InterfaceType.GLMainMenu) { //If in kiosk mode, reset the timer and disable AI on keypress @@ -34,9 +49,11 @@ internal static void KeyDownEvent(object sender, KeyboardKeyEventArgs e) if (e.Shift) CurrentKeyboardModifier |= KeyboardModifier.Shift; if (e.Control) CurrentKeyboardModifier |= KeyboardModifier.Ctrl; if (e.Alt) CurrentKeyboardModifier |= KeyboardModifier.Alt; + HeldKeyboardModifiers |= CurrentKeyboardModifier; + int capturedModifierOrder = ModifierOrder.GetRank(HeldModifierSequence, HeldModifierSequenceCount, CurrentKeyboardModifier); if (Program.Renderer.CurrentInterface >= InterfaceType.Menu && Game.Menu.IsCustomizingControl()) { - Game.Menu.SetControlKbdCustomData((OpenBveApi.Input.Key)e.Key, CurrentKeyboardModifier); + Game.Menu.SetControlKbdCustomData((OpenBveApi.Input.Key)e.Key, CurrentKeyboardModifier, capturedModifierOrder); return; } //Traverse the controls array @@ -47,7 +64,7 @@ internal static void KeyDownEvent(object sender, KeyboardKeyEventArgs e) //Compare the current and previous keyboard states //Only process if they are different if (!Enum.IsDefined(typeof(OpenBveApi.Input.Key), Interface.CurrentControls[i].Key)) continue; - if ((OpenBveApi.Input.Key)e.Key == Interface.CurrentControls[i].Key && Interface.CurrentControls[i].Modifier == CurrentKeyboardModifier) + if ((OpenBveApi.Input.Key)e.Key == Interface.CurrentControls[i].Key && Interface.CurrentControls[i].Modifier == CurrentKeyboardModifier && (Interface.CurrentControls[i].ModifierOrder == 0 || Interface.CurrentControls[i].ModifierOrder == capturedModifierOrder)) { Interface.CurrentControls[i].AnalogState = 1.0; @@ -103,8 +120,26 @@ internal static void KeyUpEvent(object sender, KeyboardKeyEventArgs e) { TrainManager.PlayerTrain.Plugin.RawKeyUp((OpenBveApi.Input.Key)e.Key); } - //We don't need to check for modifiers on key up + //Track held modifiers so mouse bindings requiring them can be matched BlockKeyRepeat = true; + switch (e.Key) + { + case Key.ShiftLeft: + case Key.ShiftRight: + HeldKeyboardModifiers &= ~KeyboardModifier.Shift; + RemoveHeldModifier(KeyboardModifier.Shift); + break; + case Key.ControlLeft: + case Key.ControlRight: + HeldKeyboardModifiers &= ~KeyboardModifier.Ctrl; + RemoveHeldModifier(KeyboardModifier.Ctrl); + break; + case Key.AltLeft: + case Key.AltRight: + HeldKeyboardModifiers &= ~KeyboardModifier.Alt; + RemoveHeldModifier(KeyboardModifier.Alt); + break; + } //Traverse the controls array for (int i = 0; i < Interface.CurrentControls.Length; i++) { diff --git a/source/OpenBVE/System/Input/ProcessControls.Analog.cs b/source/OpenBVE/System/Input/ProcessControls.Analog.cs index df0e8d523..4dd63a311 100644 --- a/source/OpenBVE/System/Input/ProcessControls.Analog.cs +++ b/source/OpenBVE/System/Input/ProcessControls.Analog.cs @@ -1,4 +1,4 @@ -using System; +using System; using LibRender2.Cameras; using LibRender2.Overlays; using OpenBveApi.Interface; @@ -9,6 +9,17 @@ namespace OpenBve { internal static partial class MainLoop { + /// The boost applied to camera rotation when driven by the mouse scroll wheel + private const double ScrollRotateBoost = 10.0; + /// The boost applied to timetable scrolling when driven by the mouse scroll wheel + private const double ScrollTimetableBoost = 5.0; + + /// Checks whether the control is bound to the mouse scroll wheel + private static bool IsMouseScroll(Control Control) + { + return Control.Method == ControlMethod.Mouse && (Control.Element == MouseElement.ScrollUp || Control.Element == MouseElement.ScrollDown); + } + private static void ProcessAnalogControl(double TimeElapsed, ref Control Control) { // analog control @@ -217,7 +228,12 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control case Translations.Command.CameraMoveRight: case Translations.Command.CameraMoveUp: case Translations.Command.CameraMoveDown: - Program.Renderer.Camera.Move(Control.Command, Control.AnalogState); + double moveFactor = Control.AnalogState; + if (IsMouseScroll(Control)) + { + moveFactor *= Interface.CurrentOptions.ZoomScrollSpeed; + } + Program.Renderer.Camera.Move(Control.Command, moveFactor); break; case Translations.Command.CameraRotateLeft: case Translations.Command.CameraRotateRight: @@ -225,13 +241,23 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control case Translations.Command.CameraRotateDown: case Translations.Command.CameraRotateCCW: case Translations.Command.CameraRotateCW: - Program.Renderer.Camera.Rotate(Control.Command, Control.AnalogState); + double rotateFactor = Control.AnalogState; + if (IsMouseScroll(Control)) + { + rotateFactor *= ScrollRotateBoost; + } + Program.Renderer.Camera.Rotate(Control.Command, rotateFactor); break; case Translations.Command.CameraZoomIn: // camera zoom in if (TimeElapsed > 0.0) { - Program.Renderer.Camera.AlignmentDirection.Zoom = -CameraProperties.ZoomTopSpeed * Control.AnalogState; + double factor = Control.AnalogState; + if (IsMouseScroll(Control)) + { + factor *= Interface.CurrentOptions.ZoomScrollSpeed; + } + Program.Renderer.Camera.AlignmentDirection.Zoom = -CameraProperties.ZoomTopSpeed * factor; } break; @@ -239,7 +265,12 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control // camera zoom out if (TimeElapsed > 0.0) { - Program.Renderer.Camera.AlignmentDirection.Zoom = CameraProperties.ZoomTopSpeed * Control.AnalogState; + double factor = Control.AnalogState; + if (IsMouseScroll(Control)) + { + factor *= Interface.CurrentOptions.ZoomScrollSpeed; + } + Program.Renderer.Camera.AlignmentDirection.Zoom = CameraProperties.ZoomTopSpeed * factor; } break; @@ -248,15 +279,20 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control if (TimeElapsed > 0.0) { const double scrollSpeed = 250.0; + double timetableFactor = Control.AnalogState; + if (IsMouseScroll(Control)) + { + timetableFactor *= ScrollTimetableBoost; + } switch (Program.Renderer.CurrentTimetable) { case DisplayedTimetable.Default: - Timetable.DefaultTimetablePosition += scrollSpeed * Control.AnalogState * TimeElapsed; + Timetable.DefaultTimetablePosition += scrollSpeed * timetableFactor * TimeElapsed; if (Timetable.DefaultTimetablePosition > 0.0) Timetable.DefaultTimetablePosition = 0.0; break; case DisplayedTimetable.Custom: - Timetable.CustomTimetablePosition += scrollSpeed * Control.AnalogState * TimeElapsed; + Timetable.CustomTimetablePosition += scrollSpeed * timetableFactor * TimeElapsed; if (Timetable.CustomTimetablePosition > 0.0) Timetable.CustomTimetablePosition = 0.0; break; @@ -269,10 +305,15 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control if (TimeElapsed > 0.0) { const double scrollSpeed = 250.0; + double timetableFactor = Control.AnalogState; + if (IsMouseScroll(Control)) + { + timetableFactor *= ScrollTimetableBoost; + } switch (Program.Renderer.CurrentTimetable) { case DisplayedTimetable.Default: - Timetable.DefaultTimetablePosition -= scrollSpeed * Control.AnalogState * TimeElapsed; + Timetable.DefaultTimetablePosition -= scrollSpeed * timetableFactor * TimeElapsed; double max; if (Timetable.DefaultTimetableTexture != null) { @@ -291,7 +332,7 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control break; case DisplayedTimetable.Custom: - Timetable.CustomTimetablePosition -= scrollSpeed * Control.AnalogState * TimeElapsed; + Timetable.CustomTimetablePosition -= scrollSpeed * timetableFactor * TimeElapsed; Texture texture = Timetable.CurrentCustomTimetableDaytimeTexture ?? Timetable.CurrentCustomTimetableNighttimeTexture; if (texture != null) { diff --git a/source/OpenBVE/System/Input/ProcessControls.Digital.cs b/source/OpenBVE/System/Input/ProcessControls.Digital.cs index 83e2eca03..b9644a2bd 100644 --- a/source/OpenBVE/System/Input/ProcessControls.Digital.cs +++ b/source/OpenBVE/System/Input/ProcessControls.Digital.cs @@ -12,6 +12,7 @@ using OpenBveApi.Motor; using OpenBveApi.Routes; using OpenBveApi.Runtime; +using OpenTK.Input; using RouteManager2.MessageManager; using RouteManager2.SignalManager; using RouteManager2.Stations; @@ -573,6 +574,19 @@ private static void ProcessDigitalControl(double TimeElapsed, ref Control Contro break; } + break; + case Translations.Command.CameraGrabToggle: + if (Program.Renderer.CurrentInterface == InterfaceType.Normal) + { + MainLoop.MouseGrabEnabled = !MainLoop.MouseGrabEnabled; + if (MainLoop.MouseGrabEnabled) + { + Program.Renderer.GameWindow.CursorVisible = false; + System.Drawing.Point center = Program.Renderer.GameWindow.PointToScreen(new System.Drawing.Point(Program.Renderer.GameWindow.ClientRectangle.Width / 2, Program.Renderer.GameWindow.ClientRectangle.Height / 2)); + Mouse.SetPosition(center.X, center.Y); + } + MainLoop.MouseGrabIgnoreOnce = true; + } break; case Translations.Command.DeviceConstSpeed: // const speed @@ -942,6 +956,8 @@ private static void ProcessDigitalControl(double TimeElapsed, ref Control Contro break; case Translations.Command.MenuActivate: // menu + MainLoop.MouseGrabEnabled = false; + Program.Renderer.GameWindow.CursorVisible = true; Game.Menu.PushMenu(MenuType.Top); break; case Translations.Command.MiscPause: diff --git a/source/OpenBVE/System/MainLoop.cs b/source/OpenBVE/System/MainLoop.cs index 3abbdf1ca..10d2197bf 100644 --- a/source/OpenBVE/System/MainLoop.cs +++ b/source/OpenBVE/System/MainLoop.cs @@ -122,11 +122,20 @@ PROCESS EVENTS // MOUSE EVENTS // - /// The current mouse state - internal static MouseState currentMouseState, previousMouseState; + /// The sensitivity of mouse-look in interior views + private const double InteriorLookSensitivity = 1.0; + /// The sensitivity of mouse-look in exterior views + private const double ExteriorLookSensitivity = 3.0; + /// The base scale applied to raw mouse movement deltas + private const double LookSensitivityScale = 0.001; + /// The distance in pixels from the window center at which the cursor is recentered whilst grabbing + private const double RecenterThresholdPixels = 400.0; internal static bool MouseGrabEnabled = false; + private static bool scrollUpPressed = false; + private static bool scrollDownPressed = false; internal static bool MouseGrabIgnoreOnce = false; + private static int lastMouseX, lastMouseY; internal static OpenBveApi.Math.Vector2 MouseGrabTarget = new OpenBveApi.Math.Vector2(0.0, 0.0); /// Called when a mouse button is pressed @@ -140,10 +149,13 @@ internal static void mouseDownEvent(object sender, MouseButtonEventArgs e) return; } timeSinceLastMouseEvent = 0; - if (e.Button == MouseButton.Right) + switch (e.Button) { - MouseGrabEnabled = !MouseGrabEnabled; - MouseGrabIgnoreOnce = true; + case MouseButton.Left: + case MouseButton.Middle: + case MouseButton.Right: + ProcessMouseControl((int)e.Button, true); + break; } if (e.Button == MouseButton.Left) { @@ -172,6 +184,14 @@ internal static void mouseUpEvent(object sender, MouseButtonEventArgs e) return; } timeSinceLastMouseEvent = 0; + switch (e.Button) + { + case MouseButton.Left: + case MouseButton.Middle: + case MouseButton.Right: + ProcessMouseControl((int)e.Button, false); + break; + } if (e.Button == MouseButton.Left) { if (Program.Renderer.CurrentInterface == InterfaceType.Normal) @@ -210,6 +230,26 @@ internal static void mouseWheelEvent(object sender, MouseWheelEventArgs e) { Game.Menu.ProcessMouseScroll(e.Delta); } + if (e.Delta != 0) + { + int element = e.Delta > 0 ? MouseElement.ScrollUp : MouseElement.ScrollDown; + // Accumulate scroll delta in AnalogState for smoother multi-scroll frames + for (int i = 0; i < Interface.CurrentControls.Length; i++) + { + if (Interface.CurrentControls[i].Method != ControlMethod.Mouse || Interface.CurrentControls[i].Element != element || Interface.CurrentControls[i].Modifier != HeldKeyboardModifiers) + { + continue; + } + if (Interface.CurrentControls[i].ModifierOrder != 0 && Interface.CurrentControls[i].ModifierOrder != ModifierOrder.GetRank(HeldModifierSequence, HeldModifierSequenceCount, HeldKeyboardModifiers)) + { + continue; + } + Interface.CurrentControls[i].AnalogState += 1.0; + Interface.CurrentControls[i].DigitalState = DigitalControlState.Pressed; + } + if (element == MouseElement.ScrollUp) scrollUpPressed = true; + if (element == MouseElement.ScrollDown) scrollDownPressed = true; + } } internal static void UpdateMouse(double TimeElapsed) @@ -221,39 +261,107 @@ internal static void UpdateMouse(double TimeElapsed) else { timeSinceLastMouseEvent = 0; //Always show the mouse in the menu + Program.Renderer.GameWindow.CursorVisible = true; + MainLoop.MouseGrabEnabled = false; } - if (Interface.CurrentOptions.CursorHideDelay > 0 && timeSinceLastMouseEvent > Interface.CurrentOptions.CursorHideDelay) + if (scrollUpPressed) { - Program.Renderer.GameWindow.CursorVisible = false; + ProcessMouseControl(MouseElement.ScrollUp, false); + scrollUpPressed = false; } - else + if (scrollDownPressed) { - Program.Renderer.GameWindow.CursorVisible = true; + ProcessMouseControl(MouseElement.ScrollDown, false); + scrollDownPressed = false; } if (MainLoop.MouseGrabEnabled) { - double factor; - if (Program.Renderer.Camera.CurrentMode == CameraViewMode.Interior | Program.Renderer.Camera.CurrentMode == CameraViewMode.InteriorLookAhead) + Program.Renderer.GameWindow.CursorVisible = false; + if (Program.Renderer.GameWindow.Focused) { - factor = 1.0; + ApplyMouseGrab(); } else { - factor = 3.0; + // Suspend mouse-look whilst the window is not focused, + // as otherwise we would yank the global cursor around other applications + MouseGrabIgnoreOnce = true; + MouseGrabTarget = OpenBveApi.Math.Vector2.Null; } + } + else if (Interface.CurrentOptions.CursorHideDelay > 0 && timeSinceLastMouseEvent > Interface.CurrentOptions.CursorHideDelay) + { + Program.Renderer.GameWindow.CursorVisible = false; + } + } - Program.Renderer.Camera.AlignmentDirection.Yaw += factor * MouseGrabTarget.X; - Program.Renderer.Camera.AlignmentDirection.Pitch -= factor * MouseGrabTarget.Y; - MouseGrabTarget = OpenBveApi.Math.Vector2.Null; + /// Applies the accumulated mouse movement to the camera whilst the mouse is grabbed + private static void ApplyMouseGrab() + { + if (MouseGrabTarget.IsNullVector()) + { + return; + } + double factor; + if (Program.Renderer.Camera.CurrentMode == CameraViewMode.Interior | Program.Renderer.Camera.CurrentMode == CameraViewMode.InteriorLookAhead) + { + factor = InteriorLookSensitivity; + } + else + { + factor = ExteriorLookSensitivity; } + + double zoomFactor = System.Math.Exp(Program.Renderer.Camera.Alignment.Zoom); + Program.Renderer.Camera.Alignment.Yaw += factor * MouseGrabTarget.X * LookSensitivityScale * zoomFactor; + Program.Renderer.Camera.Alignment.Pitch -= factor * MouseGrabTarget.Y * LookSensitivityScale * zoomFactor; + // The viewing distances depend on the camera direction, so update them now that it has changed + Program.Renderer.UpdateViewingDistances(Program.CurrentRoute.CurrentBackground.BackgroundImageDistance); + MouseGrabTarget = OpenBveApi.Math.Vector2.Null; } // // KEYBOARD EVENTS // private static KeyboardModifier CurrentKeyboardModifier = KeyboardModifier.None; + /// The keyboard modifiers currently held down, tracked across KeyDown/KeyUp events so that mouse bindings can require modifiers + internal static KeyboardModifier HeldKeyboardModifiers = KeyboardModifier.None; + /// The keyboard modifiers currently held down, in the order they were pressed (for sequence-sensitive bindings) + internal static readonly KeyboardModifier[] HeldModifierSequence = new KeyboardModifier[3]; + internal static int HeldModifierSequenceCount; + + internal static void PushHeldModifier(KeyboardModifier modifier) + { + for (int i = 0; i < HeldModifierSequenceCount; i++) + { + if (HeldModifierSequence[i] == modifier) + { + return; + } + } + if (HeldModifierSequenceCount < HeldModifierSequence.Length) + { + HeldModifierSequence[HeldModifierSequenceCount++] = modifier; + } + } + + internal static void RemoveHeldModifier(KeyboardModifier modifier) + { + for (int i = 0; i < HeldModifierSequenceCount; i++) + { + if (HeldModifierSequence[i] == modifier) + { + for (int j = i; j < HeldModifierSequenceCount - 1; j++) + { + HeldModifierSequence[j] = HeldModifierSequence[j + 1]; + } + HeldModifierSequenceCount--; + return; + } + } + } internal static void ProcessKeyboard() { @@ -310,19 +418,32 @@ internal static void ProcessKeyboard() } return; } - if (MouseGrabEnabled) + if (MouseGrabEnabled && Program.Renderer.GameWindow.Focused) { - previousMouseState = currentMouseState; - currentMouseState = Mouse.GetState(); - if (previousMouseState != currentMouseState) + int centerX = Program.Renderer.GameWindow.ClientRectangle.Width / 2; + int centerY = Program.Renderer.GameWindow.ClientRectangle.Height / 2; + System.Drawing.Point screenCenter = Program.Renderer.GameWindow.PointToScreen(new System.Drawing.Point(centerX, centerY)); + + if (MouseGrabIgnoreOnce) { - if (MouseGrabIgnoreOnce) - { - MouseGrabIgnoreOnce = false; - } - else if (MouseGrabEnabled) + MouseGrabIgnoreOnce = false; + MouseState state = Mouse.GetCursorState(); + lastMouseX = state.X; + lastMouseY = state.Y; + MouseGrabTarget = OpenBveApi.Math.Vector2.Null; + } + else + { + MouseState state = Mouse.GetCursorState(); + int curX = state.X; + int curY = state.Y; + MouseGrabTarget = new OpenBveApi.Math.Vector2(curX - lastMouseX, curY - lastMouseY); + lastMouseX = curX; + lastMouseY = curY; + if (Math.Abs(curX - screenCenter.X) > RecenterThresholdPixels || Math.Abs(curY - screenCenter.Y) > RecenterThresholdPixels) { - MouseGrabTarget = new OpenBveApi.Math.Vector2(currentMouseState.X - previousMouseState.X, currentMouseState.Y - previousMouseState.Y); + Mouse.SetPosition(screenCenter.X, screenCenter.Y); + MouseGrabIgnoreOnce = true; } } } @@ -603,5 +724,26 @@ internal static void CheckForOpenGlError(string Location) { } } #endif + private static void ProcessMouseControl(int element, bool pressed) + { + for (int i = 0; i < Interface.CurrentControls.Length; i++) + { + if (Interface.CurrentControls[i].Method == ControlMethod.Mouse && Interface.CurrentControls[i].Element == element) + { + // On press require the modifiers to match (including press order when the binding demands it); + // on release always release so controls cannot get stuck + bool matches = !pressed || Interface.CurrentControls[i].Modifier == HeldKeyboardModifiers; + if (matches && pressed && Interface.CurrentControls[i].ModifierOrder != 0) + { + matches = Interface.CurrentControls[i].ModifierOrder == ModifierOrder.GetRank(HeldModifierSequence, HeldModifierSequenceCount, HeldKeyboardModifiers); + } + if (matches) + { + Interface.CurrentControls[i].AnalogState = pressed ? 1.0 : 0.0; + Interface.CurrentControls[i].DigitalState = pressed ? DigitalControlState.Pressed : DigitalControlState.Released; + } + } + } + } } } diff --git a/source/OpenBVE/System/Options.cs b/source/OpenBVE/System/Options.cs index ad0479351..8e8f10ec8 100644 --- a/source/OpenBVE/System/Options.cs +++ b/source/OpenBVE/System/Options.cs @@ -22,6 +22,8 @@ internal class Options : BaseOptions { /// The on disk folder in which user interface components are stored internal string UserInterfaceFolder; + /// The speed at which the mouse scroll zooms the camera + internal double ZoomScrollSpeed; /// The accelerated time factor (1x to 5x) internal int TimeAccelerationFactor; ///// The current type of motion blur @@ -129,6 +131,7 @@ internal Options() TransparencyMode = TransparencyMode.Quality; AnisotropicFilteringLevel = 0; AnisotropicFilteringMaximum = 0; + ZoomScrollSpeed = 30.0; AntiAliasingLevel = 0; ViewingDistance = 600; QuadTreeLeafSize = 60; @@ -354,6 +357,7 @@ public override void Save(string fileName) Builder.AppendLine("keyRepeatDelay = " + (1000.0 * KeyRepeatDelay).ToString("0", Culture)); Builder.AppendLine("keyRepeatInterval = " + (1000.0 * KeyRepeatInterval).ToString("0", Culture)); Builder.AppendLine("raildrivermph = " + (RailDriverMPH ? "true" : "false")); + Builder.AppendLine("zoomScrollSpeed = " + ZoomScrollSpeed.ToString(Culture)); Builder.AppendLine(); Builder.AppendLine("[sound]"); Builder.AppendLine("model = " + SoundModel); @@ -580,6 +584,11 @@ internal static void LoadOptions() CurrentOptions.KeyRepeatInterval = interval * 0.001; block.GetValue(OptionsKey.RailDriverMPH, out CurrentOptions.RailDriverMPH); block.GetValue(OptionsKey.CursorHideDelay, out CurrentOptions.CursorHideDelay); + block.TryGetValue(OptionsKey.ZoomScrollSpeed, ref CurrentOptions.ZoomScrollSpeed); + if (CurrentOptions.ZoomScrollSpeed <= 0.0 || CurrentOptions.ZoomScrollSpeed > 100.0) + { + CurrentOptions.ZoomScrollSpeed = 30.0; + } break; case OptionsSection.Sound: block.GetEnumValue(OptionsKey.Model, out CurrentOptions.SoundModel); diff --git a/source/OpenBVE/UserInterface/formMain.Controls.cs b/source/OpenBVE/UserInterface/formMain.Controls.cs index dc97128f6..ee7263d70 100644 --- a/source/OpenBVE/UserInterface/formMain.Controls.cs +++ b/source/OpenBVE/UserInterface/formMain.Controls.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Drawing; using System.Linq; using System.Windows.Forms; @@ -18,7 +18,10 @@ internal partial class formMain { private bool blockComboBoxIndexEvent = false; - + + /// Maps the indices of the mouse button combobox onto the mouse elements they represent + private static readonly int[] MouseButtonElements = { MouseElement.Left, MouseElement.Middle, MouseElement.Right, MouseElement.ScrollUp, MouseElement.ScrollDown }; + // ======== // controls // ======== @@ -54,13 +57,18 @@ private void listviewControls_SelectedIndexChanged(object sender, EventArgs e) { case ControlMethod.RailDriver: radiobuttonJoystick.Checked = true; break; + case ControlMethod.Mouse: + radiobuttonMouse.Checked = true; + break; default: radiobuttonKeyboard.Checked = false; radiobuttonJoystick.Checked = false; + radiobuttonMouse.Checked = false; textboxJoystickGrab.Enabled = false; break; } panelKeyboard.Enabled = radiobuttonKeyboard.Checked; + panelKeyboard.Visible = radiobuttonKeyboard.Checked; if (radiobuttonKeyboard.Checked) { if (Translations.TranslatedKeys.ContainsKey(Interface.CurrentControls[i].Key)) @@ -70,15 +78,32 @@ private void listviewControls_SelectedIndexChanged(object sender, EventArgs e) { checkboxKeyboardShift.Checked = (Interface.CurrentControls[i].Modifier & KeyboardModifier.Shift) != 0; checkboxKeyboardCtrl.Checked = (Interface.CurrentControls[i].Modifier & KeyboardModifier.Ctrl) != 0; checkboxKeyboardAlt.Checked = (Interface.CurrentControls[i].Modifier & KeyboardModifier.Alt) != 0; + LoadKeyboardCheckboxOrder(i); } else if (radiobuttonJoystick.Checked) { labelJoystickAssignmentValue.Text = GetControlDetails(i); + } else if (radiobuttonMouse.Checked) { + comboboxMouseButton.SelectedIndex = Array.IndexOf(MouseButtonElements, Interface.CurrentControls[i].Element); + checkboxMouseShift.Checked = (Interface.CurrentControls[i].Modifier & KeyboardModifier.Shift) != 0; + checkboxMouseCtrl.Checked = (Interface.CurrentControls[i].Modifier & KeyboardModifier.Ctrl) != 0; + checkboxMouseAlt.Checked = (Interface.CurrentControls[i].Modifier & KeyboardModifier.Alt) != 0; + LoadMouseCheckboxOrder(i); } else { comboboxKeyboardKey.SelectedIndex = -1; checkboxKeyboardShift.Checked = false; checkboxKeyboardCtrl.Checked = false; checkboxKeyboardAlt.Checked = false; + comboboxMouseButton.SelectedIndex = -1; + checkboxMouseShift.Checked = false; + checkboxMouseCtrl.Checked = false; + checkboxMouseAlt.Checked = false; + KeyboardCheckboxOrderCount = 0; + MouseCheckboxOrderCount = 0; } panelJoystick.Enabled = radiobuttonJoystick.Checked; + panelJoystick.Visible = radiobuttonJoystick.Checked; + panelMouse.Enabled = radiobuttonMouse.Checked; + panelMouse.Visible = radiobuttonMouse.Checked; + textboxJoystickGrab.Visible = radiobuttonJoystick.Checked || radiobuttonKeyboard.Checked; // finalize Tag = null; } @@ -91,11 +116,18 @@ private void listviewControls_SelectedIndexChanged(object sender, EventArgs e) { comboboxCommand.SelectedIndex = -1; radiobuttonKeyboard.Checked = false; radiobuttonJoystick.Checked = false; + radiobuttonMouse.Checked = false; groupboxControl.Enabled = false; comboboxKeyboardKey.SelectedIndex = -1; checkboxKeyboardShift.Checked = false; checkboxKeyboardCtrl.Checked = false; checkboxKeyboardAlt.Checked = false; + comboboxMouseButton.SelectedIndex = -1; + checkboxMouseShift.Checked = false; + checkboxMouseCtrl.Checked = false; + checkboxMouseAlt.Checked = false; + KeyboardCheckboxOrderCount = 0; + MouseCheckboxOrderCount = 0; labelJoystickAssignmentValue.Text = ""; Tag = null; buttonControlRemove.Enabled = false; @@ -122,6 +154,9 @@ private void UpdateControlListElement(ListViewItem Item, int Index, bool ResizeC case ControlMethod.Joystick: Item.ImageKey = Info.Type == Translations.CommandType.AnalogHalf || Info.Type == Translations.CommandType.AnalogFull ? @"joystick" : @"gamepad"; break; + case ControlMethod.Mouse: + Item.ImageKey = @"mouse"; + break; default: Item.ImageKey = null; break; @@ -138,10 +173,7 @@ private string GetControlDetails(int Index) { System.Globalization.CultureInfo Culture = System.Globalization.CultureInfo.InvariantCulture; string Separator = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_separator"}); if (Interface.CurrentControls[Index].Method == ControlMethod.Keyboard) { - string t = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_keyboard"}) + Separator; - if ((Interface.CurrentControls[Index].Modifier & KeyboardModifier.Shift) != 0) t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_keyboard_shift"}); - if ((Interface.CurrentControls[Index].Modifier & KeyboardModifier.Ctrl) != 0) t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_keyboard_ctrl"}); - if ((Interface.CurrentControls[Index].Modifier & KeyboardModifier.Alt) != 0) t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_keyboard_alt"}); + string t = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_keyboard"}) + Separator + BuildModifierDetails(Index); if (Interface.CurrentControls[Index].Key != Key.Unknown) { @@ -266,10 +298,43 @@ private string GetControlDetails(int Index) { } return t; } + if (Interface.CurrentControls[Index].Method == ControlMethod.Mouse) { + string t = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse"}) + Separator + BuildModifierDetails(Index); + switch (Interface.CurrentControls[Index].Element) { + case MouseElement.Left: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_left"}); break; + case MouseElement.Middle: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_middle"}); break; + case MouseElement.Right: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_right"}); break; + case MouseElement.ScrollUp: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_scrollup"}); break; + case MouseElement.ScrollDown: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_scrolldown"}); break; + default: t += "{" + Interface.CurrentControls[Index].Element.ToString(Culture) + "}"; break; + } + return t; + } return Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_invalid"}); } + // modifiers + private string BuildModifierDetails(int Index) { + string t = string.Empty; + KeyboardModifier modifier = Interface.CurrentControls[Index].Modifier; + bool ordered = Interface.CurrentControls[Index].ModifierOrder != 0 && CountModifiers((int)modifier) >= 2; + if (ordered) { + foreach (KeyboardModifier m in ModifierOrder.FromRank((int)modifier, Interface.CurrentControls[Index].ModifierOrder)) { + switch (m) { + case KeyboardModifier.Shift: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_keyboard_shift"}); break; + case KeyboardModifier.Ctrl: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_keyboard_ctrl"}); break; + case KeyboardModifier.Alt: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_keyboard_alt"}); break; + } + } + } else { + if ((modifier & KeyboardModifier.Shift) != 0) t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_keyboard_shift"}); + if ((modifier & KeyboardModifier.Ctrl) != 0) t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_keyboard_ctrl"}); + if ((modifier & KeyboardModifier.Alt) != 0) t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_keyboard_alt"}); + } + return t; + } + // control add private void buttonControlAdd_Click(object sender, EventArgs e) { for (int i = 0; i < Interface.CurrentControls.Length; i++) { @@ -359,12 +424,56 @@ private void updownCommandOption_ValueChanged(object sender, EventArgs e) { // keyboard private void radiobuttonKeyboard_CheckedChanged(object sender, EventArgs e) { textboxJoystickGrab.Enabled = radiobuttonJoystick.Checked || radiobuttonKeyboard.Checked; + textboxJoystickGrab.Visible = radiobuttonJoystick.Checked || radiobuttonKeyboard.Checked; if (Tag == null & listviewControls.SelectedIndices.Count == 1) { int i = listviewControls.SelectedIndices[0]; Interface.CurrentControls[i].Method = ControlMethod.Keyboard; UpdateControlListElement(listviewControls.Items[i], i, true); } panelKeyboard.Enabled = radiobuttonKeyboard.Checked; + panelKeyboard.Visible = radiobuttonKeyboard.Checked; + } + + // mouse + private void radiobuttonMouse_CheckedChanged(object sender, EventArgs e) { + if (Tag == null & listviewControls.SelectedIndices.Count == 1) { + int i = listviewControls.SelectedIndices[0]; + Interface.CurrentControls[i].Method = ControlMethod.Mouse; + UpdateControlListElement(listviewControls.Items[i], i, true); + } + panelMouse.Enabled = radiobuttonMouse.Checked; + panelMouse.Visible = radiobuttonMouse.Checked; + textboxJoystickGrab.Visible = radiobuttonJoystick.Checked || radiobuttonKeyboard.Checked; + } + + private void comboboxMouseButton_SelectedIndexChanged(object sender, EventArgs e) { + if (Tag == null & listviewControls.SelectedIndices.Count == 1 && comboboxMouseButton.SelectedIndex >= 0) { + int i = listviewControls.SelectedIndices[0]; + Interface.CurrentControls[i].Element = MouseButtonElements[comboboxMouseButton.SelectedIndex]; + UpdateControlListElement(listviewControls.Items[i], i, true); + } + } + + // mouse modifiers + private void checkboxMouseShift_CheckedChanged(object sender, EventArgs e) { + UpdateMouseModifiers(KeyboardModifier.Shift, checkboxMouseShift.Checked); + } + private void checkboxMouseCtrl_CheckedChanged(object sender, EventArgs e) { + UpdateMouseModifiers(KeyboardModifier.Ctrl, checkboxMouseCtrl.Checked); + } + private void checkboxMouseAlt_CheckedChanged(object sender, EventArgs e) { + UpdateMouseModifiers(KeyboardModifier.Alt, checkboxMouseAlt.Checked); + } + private void UpdateMouseModifiers(KeyboardModifier toggled, bool pressed) { + if (Tag == null & listviewControls.SelectedIndices.Count == 1) { + int i = listviewControls.SelectedIndices[0]; + ToggleCheckboxModifier(MouseCheckboxOrder, ref MouseCheckboxOrderCount, toggled, pressed); + Interface.CurrentControls[i].Modifier = (checkboxMouseShift.Checked ? KeyboardModifier.Shift : KeyboardModifier.None) | + (checkboxMouseCtrl.Checked ? KeyboardModifier.Ctrl : KeyboardModifier.None) | + (checkboxMouseAlt.Checked ? KeyboardModifier.Alt : KeyboardModifier.None); + Interface.CurrentControls[i].ModifierOrder = CountModifiers((int)Interface.CurrentControls[i].Modifier) >= 2 ? ModifierOrder.GetRank(MouseCheckboxOrder, MouseCheckboxOrderCount, Interface.CurrentControls[i].Modifier) : 0; + UpdateControlListElement(listviewControls.Items[i], i, true); + } } // key @@ -385,29 +494,22 @@ private void comboboxKeyboardKey_SelectedIndexChanged(object sender, EventArgs e // modifiers private void checkboxKeyboardShift_CheckedChanged(object sender, EventArgs e) { - if (Tag == null & listviewControls.SelectedIndices.Count == 1) { - int i = listviewControls.SelectedIndices[0]; - Interface.CurrentControls[i].Modifier = (checkboxKeyboardShift.Checked ? KeyboardModifier.Shift : KeyboardModifier.None) | - (checkboxKeyboardCtrl.Checked ? KeyboardModifier.Ctrl : KeyboardModifier.None) | - (checkboxKeyboardAlt.Checked ? KeyboardModifier.Alt : KeyboardModifier.None); - UpdateControlListElement(listviewControls.Items[i], i, true); - } + UpdateKeyboardModifiers(KeyboardModifier.Shift, checkboxKeyboardShift.Checked); } private void checkboxKeyboardCtrl_CheckedChanged(object sender, EventArgs e) { - if (Tag == null & listviewControls.SelectedIndices.Count == 1) { - int i = listviewControls.SelectedIndices[0]; - Interface.CurrentControls[i].Modifier = (checkboxKeyboardShift.Checked ? KeyboardModifier.Shift : KeyboardModifier.None) | - (checkboxKeyboardCtrl.Checked ? KeyboardModifier.Ctrl : KeyboardModifier.None) | - (checkboxKeyboardAlt.Checked ? KeyboardModifier.Alt : KeyboardModifier.None); - UpdateControlListElement(listviewControls.Items[i], i, true); - } + UpdateKeyboardModifiers(KeyboardModifier.Ctrl, checkboxKeyboardCtrl.Checked); } private void checkboxKeyboardAlt_CheckedChanged(object sender, EventArgs e) { + UpdateKeyboardModifiers(KeyboardModifier.Alt, checkboxKeyboardAlt.Checked); + } + private void UpdateKeyboardModifiers(KeyboardModifier toggled, bool pressed) { if (Tag == null & listviewControls.SelectedIndices.Count == 1) { int i = listviewControls.SelectedIndices[0]; + ToggleCheckboxModifier(KeyboardCheckboxOrder, ref KeyboardCheckboxOrderCount, toggled, pressed); Interface.CurrentControls[i].Modifier = (checkboxKeyboardShift.Checked ? KeyboardModifier.Shift : KeyboardModifier.None) | (checkboxKeyboardCtrl.Checked ? KeyboardModifier.Ctrl : KeyboardModifier.None) | (checkboxKeyboardAlt.Checked ? KeyboardModifier.Alt : KeyboardModifier.None); + Interface.CurrentControls[i].ModifierOrder = CountModifiers((int)Interface.CurrentControls[i].Modifier) >= 2 ? ModifierOrder.GetRank(KeyboardCheckboxOrder, KeyboardCheckboxOrderCount, Interface.CurrentControls[i].Modifier) : 0; UpdateControlListElement(listviewControls.Items[i], i, true); } } @@ -426,6 +528,7 @@ private void radiobuttonJoystick_CheckedChanged(object sender, EventArgs e) { UpdateControlListElement(listviewControls.Items[i], i, true); } panelJoystick.Enabled = radiobuttonJoystick.Checked; + panelJoystick.Visible = radiobuttonJoystick.Checked; if (radiobuttonJoystick.Checked || radiobuttonKeyboard.Checked) { textboxJoystickGrab.Enabled = true; @@ -434,6 +537,7 @@ private void radiobuttonJoystick_CheckedChanged(object sender, EventArgs e) { { textboxJoystickGrab.Enabled = false; } + textboxJoystickGrab.Visible = radiobuttonJoystick.Checked || radiobuttonKeyboard.Checked; textboxJoystickGrab.Text = Translations.GetInterfaceString(HostApplication.OpenBve, radiobuttonJoystick.Checked ? new[] {"controls","selection_joystick_assignment_grab"} : new[] {"controls","selection_keyboard_assignment_grab"}); } @@ -522,6 +626,85 @@ private void buttonControlsExport_Click(object sender, EventArgs e) { } private bool KeyGrab = false; + private readonly KeyboardModifier[] GrabSequence = new KeyboardModifier[3]; + private int GrabSequenceCount; + private readonly KeyboardModifier[] KeyboardCheckboxOrder = new KeyboardModifier[3]; + private int KeyboardCheckboxOrderCount; + private readonly KeyboardModifier[] MouseCheckboxOrder = new KeyboardModifier[3]; + private int MouseCheckboxOrderCount; + + private void PushGrabModifier(KeyboardModifier modifier) + { + for (int i = 0; i < GrabSequenceCount; i++) + { + if (GrabSequence[i] == modifier) + { + return; + } + } + if (GrabSequenceCount < GrabSequence.Length) + { + GrabSequence[GrabSequenceCount++] = modifier; + } + } + + private static void ToggleCheckboxModifier(KeyboardModifier[] order, ref int count, KeyboardModifier modifier, bool pressed) + { + if (pressed) + { + for (int i = 0; i < count; i++) + { + if (order[i] == modifier) + { + return; + } + } + if (count < order.Length) + { + order[count++] = modifier; + } + return; + } + for (int i = 0; i < count; i++) + { + if (order[i] == modifier) + { + for (int j = i; j < count - 1; j++) + { + order[j] = order[j + 1]; + } + count--; + return; + } + } + } + + private void LoadKeyboardCheckboxOrder(int index) + { + LoadCheckboxOrder(index, KeyboardCheckboxOrder, ref KeyboardCheckboxOrderCount); + } + + private void LoadMouseCheckboxOrder(int index) + { + LoadCheckboxOrder(index, MouseCheckboxOrder, ref MouseCheckboxOrderCount); + } + + private void LoadCheckboxOrder(int index, KeyboardModifier[] order, ref int count) + { + count = 0; + if (Interface.CurrentControls[index].ModifierOrder != 0) + { + foreach (KeyboardModifier m in ModifierOrder.FromRank((int)Interface.CurrentControls[index].Modifier, Interface.CurrentControls[index].ModifierOrder)) + { + order[count++] = m; + } + } + } + + private static int CountModifiers(int modifierBits) + { + return ((modifierBits & 1) != 0 ? 1 : 0) + ((modifierBits & 2) != 0 ? 1 : 0) + ((modifierBits & 4) != 0 ? 1 : 0); + } // joystick grab private void textboxJoystickGrab_Enter(object sender, EventArgs e) { @@ -530,6 +713,7 @@ private void textboxJoystickGrab_Enter(object sender, EventArgs e) { textboxJoystickGrab.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_keyboard_assignment_grabbing"}); textboxJoystickGrab.BackColor = Color.LightSkyBlue; textboxJoystickGrab.ForeColor = Color.Black; + GrabSequenceCount = 0; KeyGrab = true; return; @@ -557,17 +741,55 @@ private void textboxJoystickGrab_KeyDown(object sender, KeyEventArgs e) { return; } + //Capture held modifiers, but never treat the modifier keys themselves as the assigned key + switch (e.KeyCode) + { + case Keys.LShiftKey: + case Keys.RShiftKey: + PushGrabModifier(KeyboardModifier.Shift); + break; + case Keys.LControlKey: + case Keys.RControlKey: + PushGrabModifier(KeyboardModifier.Ctrl); + break; + case Keys.LMenu: + case Keys.RMenu: + PushGrabModifier(KeyboardModifier.Alt); + break; + } + KeyboardModifier modifier = + (kbState.IsKeyDown(OpenTK.Input.Key.ShiftLeft) | kbState.IsKeyDown(OpenTK.Input.Key.ShiftRight) ? KeyboardModifier.Shift : KeyboardModifier.None) | + (kbState.IsKeyDown(OpenTK.Input.Key.ControlLeft) | kbState.IsKeyDown(OpenTK.Input.Key.ControlRight) ? KeyboardModifier.Ctrl : KeyboardModifier.None) | + (kbState.IsKeyDown(OpenTK.Input.Key.AltLeft) | kbState.IsKeyDown(OpenTK.Input.Key.AltRight) ? KeyboardModifier.Alt : KeyboardModifier.None); + int selected = -1; for (int j = 0; j < Translations.TranslatedKeys.Count; j++) { - Key k = Translations.TranslatedKeys.ElementAt(j).Key; - if (kbState.IsKeyDown((OpenTK.Input.Key)k)) + OpenTK.Input.Key k = (OpenTK.Input.Key)Translations.TranslatedKeys.ElementAt(j).Key; + if (k == OpenTK.Input.Key.ShiftLeft | k == OpenTK.Input.Key.ShiftRight | k == OpenTK.Input.Key.ControlLeft | k == OpenTK.Input.Key.ControlRight | k == OpenTK.Input.Key.AltLeft | k == OpenTK.Input.Key.AltRight) { - int i = listviewControls.SelectedIndices[0]; - Interface.CurrentControls[i].Key = k; - UpdateControlListElement(listviewControls.Items[i], i, true); - comboboxKeyboardKey.SelectedIndex = j; + continue; + } + if (kbState.IsKeyDown(k)) + { + selected = j; + break; } } + if (selected >= 0 && listviewControls.SelectedIndices.Count == 1) + { + int i = listviewControls.SelectedIndices[0]; + Interface.CurrentControls[i].Key = Translations.TranslatedKeys.ElementAt(selected).Key; + Interface.CurrentControls[i].Modifier = modifier; + Interface.CurrentControls[i].ModifierOrder = modifier != KeyboardModifier.None ? ModifierOrder.GetRank(GrabSequence, GrabSequenceCount, modifier) : 0; + UpdateControlListElement(listviewControls.Items[i], i, true); + Tag = new object(); + checkboxKeyboardShift.Checked = (modifier & KeyboardModifier.Shift) != 0; + checkboxKeyboardCtrl.Checked = (modifier & KeyboardModifier.Ctrl) != 0; + checkboxKeyboardAlt.Checked = (modifier & KeyboardModifier.Alt) != 0; + Tag = null; + LoadKeyboardCheckboxOrder(i); + comboboxKeyboardKey.SelectedIndex = selected; + } textboxJoystickGrab.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_keyboard_assignment_grab"}); textboxJoystickGrab.BackColor = Color.White; textboxJoystickGrab.ForeColor = Color.Black; diff --git a/source/OpenBVE/UserInterface/formMain.Designer.cs b/source/OpenBVE/UserInterface/formMain.Designer.cs index 5eda0d59b..bc7cde65e 100644 --- a/source/OpenBVE/UserInterface/formMain.Designer.cs +++ b/source/OpenBVE/UserInterface/formMain.Designer.cs @@ -139,6 +139,8 @@ private void InitializeComponent() { this.trackbarTransparency = new System.Windows.Forms.TrackBar(); this.panelOptionsRight = new System.Windows.Forms.Panel(); this.groupBoxOther = new System.Windows.Forms.GroupBox(); + this.labelZoomScrollSpeed = new System.Windows.Forms.Label(); + this.updownZoomScrollSpeed = new System.Windows.Forms.NumericUpDown(); this.comboBoxTimeTableDisplayMode = new System.Windows.Forms.ComboBox(); this.labelTimeTableDisplayMode = new System.Windows.Forms.Label(); this.groupBoxRailDriver = new System.Windows.Forms.GroupBox(); @@ -338,6 +340,14 @@ private void InitializeComponent() { this.labelJoystickAssignmentValue = new System.Windows.Forms.Label(); this.radiobuttonJoystick = new System.Windows.Forms.RadioButton(); this.radiobuttonKeyboard = new System.Windows.Forms.RadioButton(); + this.radiobuttonMouse = new System.Windows.Forms.RadioButton(); + this.panelMouse = new System.Windows.Forms.Panel(); + this.labelMouseButton = new System.Windows.Forms.Label(); + this.comboboxMouseButton = new System.Windows.Forms.ComboBox(); + this.labelMouseModifier = new System.Windows.Forms.Label(); + this.checkboxMouseShift = new System.Windows.Forms.CheckBox(); + this.checkboxMouseCtrl = new System.Windows.Forms.CheckBox(); + this.checkboxMouseAlt = new System.Windows.Forms.CheckBox(); this.panelInfo = new System.Windows.Forms.Panel(); this.linkLabelReportBug = new System.Windows.Forms.LinkLabel(); this.linkLabelCheckUpdates = new System.Windows.Forms.LinkLabel(); @@ -2121,11 +2131,44 @@ private void InitializeComponent() { this.groupBoxOther.ForeColor = System.Drawing.Color.Black; this.groupBoxOther.Location = new System.Drawing.Point(0, 365); this.groupBoxOther.Name = "groupBoxOther"; - this.groupBoxOther.Size = new System.Drawing.Size(316, 48); + this.groupBoxOther.Size = new System.Drawing.Size(316, 50); this.groupBoxOther.TabIndex = 19; this.groupBoxOther.TabStop = false; this.groupBoxOther.Text = "Other"; // + // labelZoomScrollSpeed + // + this.labelZoomScrollSpeed.AutoSize = true; + this.labelZoomScrollSpeed.Location = new System.Drawing.Point(8, 98); + this.labelZoomScrollSpeed.Name = "labelZoomScrollSpeed"; + this.labelZoomScrollSpeed.Size = new System.Drawing.Size(130, 18); + this.labelZoomScrollSpeed.TabIndex = 4; + this.labelZoomScrollSpeed.Text = "Zoom Scroll Speed:"; + this.labelZoomScrollSpeed.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // updownZoomScrollSpeed + // + this.updownZoomScrollSpeed.Location = new System.Drawing.Point(200, 96); + this.updownZoomScrollSpeed.Maximum = new decimal(new int[] { + 100, + 0, + 0, + 0}); + this.updownZoomScrollSpeed.Minimum = new decimal(new int[] { + 1, + 0, + 0, + 0}); + this.updownZoomScrollSpeed.Name = "updownZoomScrollSpeed"; + this.updownZoomScrollSpeed.Size = new System.Drawing.Size(52, 20); + this.updownZoomScrollSpeed.TabIndex = 3; + this.updownZoomScrollSpeed.Value = new decimal(new int[] { + 30, + 0, + 0, + 0}); + this.updownZoomScrollSpeed.ValueChanged += new System.EventHandler(this.updownZoomScrollSpeed_ValueChanged); + // // comboBoxTimeTableDisplayMode // this.comboBoxTimeTableDisplayMode.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); @@ -2691,6 +2734,8 @@ private void InitializeComponent() { this.groupboxCamera.Controls.Add(this.checkboxCameraExteriorTransition); this.groupboxCamera.Controls.Add(this.labelCameraTransitionSpeed); this.groupboxCamera.Controls.Add(this.updownCameraTransitionSpeed); + this.groupboxCamera.Controls.Add(this.labelZoomScrollSpeed); + this.groupboxCamera.Controls.Add(this.updownZoomScrollSpeed); this.groupboxCamera.ForeColor = System.Drawing.Color.Black; this.groupboxCamera.Location = new System.Drawing.Point(330, 245); this.groupboxCamera.Name = "groupboxCamera"; @@ -4346,6 +4391,8 @@ private void InitializeComponent() { this.groupboxControl.Controls.Add(this.panelJoystick); this.groupboxControl.Controls.Add(this.radiobuttonJoystick); this.groupboxControl.Controls.Add(this.radiobuttonKeyboard); + this.groupboxControl.Controls.Add(this.radiobuttonMouse); + this.groupboxControl.Controls.Add(this.panelMouse); this.groupboxControl.Enabled = false; this.groupboxControl.ForeColor = System.Drawing.Color.Black; this.groupboxControl.Location = new System.Drawing.Point(8, 349); @@ -4442,22 +4489,22 @@ private void InitializeComponent() { this.comboboxCommand.FormattingEnabled = true; this.comboboxCommand.Location = new System.Drawing.Point(88, 21); this.comboboxCommand.Name = "comboboxCommand"; - this.comboboxCommand.Size = new System.Drawing.Size(587, 21); + this.comboboxCommand.Size = new System.Drawing.Size(400, 21); this.comboboxCommand.TabIndex = 1; this.comboboxCommand.SelectedIndexChanged += new System.EventHandler(this.comboboxCommand_SelectedIndexChanged); // // updownCommandOption // - this.updownCommandOption.Location = new System.Drawing.Point(583, 48); + this.updownCommandOption.Location = new System.Drawing.Point(615, 21); this.updownCommandOption.Name = "updownCommandOption"; - this.updownCommandOption.Size = new System.Drawing.Size(52, 20); + this.updownCommandOption.Size = new System.Drawing.Size(60, 20); this.updownCommandOption.TabIndex = 6; this.updownCommandOption.ValueChanged += new System.EventHandler(this.updownCommandOption_ValueChanged); // // labelCommandOption // this.labelCommandOption.AutoEllipsis = true; - this.labelCommandOption.Location = new System.Drawing.Point(463, 51); + this.labelCommandOption.Location = new System.Drawing.Point(490, 24); this.labelCommandOption.Name = "labelCommandOption"; this.labelCommandOption.Size = new System.Drawing.Size(120, 18); this.labelCommandOption.TabIndex = 7; @@ -4499,7 +4546,7 @@ private void InitializeComponent() { this.panelJoystick.Controls.Add(this.labelJoystickAssignmentCaption); this.panelJoystick.Controls.Add(this.labelJoystickAssignmentValue); this.panelJoystick.Enabled = false; - this.panelJoystick.Location = new System.Drawing.Point(264, 72); + this.panelJoystick.Location = new System.Drawing.Point(232, 72); this.panelJoystick.Name = "panelJoystick"; this.panelJoystick.Size = new System.Drawing.Size(235, 48); this.panelJoystick.TabIndex = 4; @@ -4525,7 +4572,7 @@ private void InitializeComponent() { // radiobuttonJoystick // this.radiobuttonJoystick.AutoSize = true; - this.radiobuttonJoystick.Location = new System.Drawing.Point(272, 48); + this.radiobuttonJoystick.Location = new System.Drawing.Point(232, 48); this.radiobuttonJoystick.Name = "radiobuttonJoystick"; this.radiobuttonJoystick.Size = new System.Drawing.Size(66, 17); this.radiobuttonJoystick.TabIndex = 3; @@ -4546,6 +4593,98 @@ private void InitializeComponent() { this.radiobuttonKeyboard.UseVisualStyleBackColor = true; this.radiobuttonKeyboard.CheckedChanged += new System.EventHandler(this.radiobuttonKeyboard_CheckedChanged); // + // radiobuttonMouse + // + this.radiobuttonMouse.AutoSize = true; + this.radiobuttonMouse.Location = new System.Drawing.Point(456, 48); + this.radiobuttonMouse.Name = "radiobuttonMouse"; + this.radiobuttonMouse.Size = new System.Drawing.Size(60, 17); + this.radiobuttonMouse.TabIndex = 11; + this.radiobuttonMouse.TabStop = true; + this.radiobuttonMouse.Text = "Mouse:"; + this.radiobuttonMouse.UseVisualStyleBackColor = true; + this.radiobuttonMouse.CheckedChanged += new System.EventHandler(this.radiobuttonMouse_CheckedChanged); + // + // panelMouse + // + this.panelMouse.Controls.Add(this.comboboxMouseButton); + this.panelMouse.Controls.Add(this.labelMouseButton); + this.panelMouse.Controls.Add(this.checkboxMouseAlt); + this.panelMouse.Controls.Add(this.checkboxMouseCtrl); + this.panelMouse.Controls.Add(this.checkboxMouseShift); + this.panelMouse.Controls.Add(this.labelMouseModifier); + this.panelMouse.Enabled = false; + this.panelMouse.Location = new System.Drawing.Point(8, 72); + this.panelMouse.Name = "panelMouse"; + this.panelMouse.Size = new System.Drawing.Size(248, 48); + this.panelMouse.TabIndex = 12; + this.panelMouse.Visible = false; + // + // labelMouseButton + // + this.labelMouseButton.AutoEllipsis = true; + this.labelMouseButton.Location = new System.Drawing.Point(0, 3); + this.labelMouseButton.Name = "labelMouseButton"; + this.labelMouseButton.Size = new System.Drawing.Size(60, 18); + this.labelMouseButton.TabIndex = 0; + this.labelMouseButton.Text = "Button:"; + this.labelMouseButton.TextAlign = System.Drawing.ContentAlignment.TopRight; + // + // comboboxMouseButton + // + this.comboboxMouseButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.comboboxMouseButton.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.comboboxMouseButton.FormattingEnabled = true; + this.comboboxMouseButton.Location = new System.Drawing.Point(64, 0); + this.comboboxMouseButton.Name = "comboboxMouseButton"; + this.comboboxMouseButton.Size = new System.Drawing.Size(104, 21); + this.comboboxMouseButton.TabIndex = 1; + this.comboboxMouseButton.SelectedIndexChanged += new System.EventHandler(this.comboboxMouseButton_SelectedIndexChanged); + // + // checkboxMouseAlt + // + this.checkboxMouseAlt.AutoSize = true; + this.checkboxMouseAlt.Location = new System.Drawing.Point(192, 26); + this.checkboxMouseAlt.Name = "checkboxMouseAlt"; + this.checkboxMouseAlt.Size = new System.Drawing.Size(38, 17); + this.checkboxMouseAlt.TabIndex = 5; + this.checkboxMouseAlt.Text = "Alt"; + this.checkboxMouseAlt.UseVisualStyleBackColor = true; + this.checkboxMouseAlt.CheckedChanged += new System.EventHandler(this.checkboxMouseAlt_CheckedChanged); + // + // checkboxMouseCtrl + // + this.checkboxMouseCtrl.AutoSize = true; + this.checkboxMouseCtrl.Location = new System.Drawing.Point(136, 26); + this.checkboxMouseCtrl.Name = "checkboxMouseCtrl"; + this.checkboxMouseCtrl.Size = new System.Drawing.Size(41, 17); + this.checkboxMouseCtrl.TabIndex = 4; + this.checkboxMouseCtrl.Text = "Ctrl"; + this.checkboxMouseCtrl.UseVisualStyleBackColor = true; + this.checkboxMouseCtrl.CheckedChanged += new System.EventHandler(this.checkboxMouseCtrl_CheckedChanged); + // + // checkboxMouseShift + // + this.checkboxMouseShift.AutoSize = true; + this.checkboxMouseShift.Location = new System.Drawing.Point(80, 26); + this.checkboxMouseShift.Name = "checkboxMouseShift"; + this.checkboxMouseShift.Size = new System.Drawing.Size(47, 17); + this.checkboxMouseShift.TabIndex = 3; + this.checkboxMouseShift.Text = "Shift"; + this.checkboxMouseShift.UseVisualStyleBackColor = true; + this.checkboxMouseShift.CheckedChanged += new System.EventHandler(this.checkboxMouseShift_CheckedChanged); + // + // labelMouseModifier + // + this.labelMouseModifier.AutoEllipsis = true; + this.labelMouseModifier.Location = new System.Drawing.Point(0, 29); + this.labelMouseModifier.Name = "labelMouseModifier"; + this.labelMouseModifier.Size = new System.Drawing.Size(76, 18); + this.labelMouseModifier.TabIndex = 2; + this.labelMouseModifier.Text = "Modifiers:"; + this.labelMouseModifier.TextAlign = System.Drawing.ContentAlignment.TopRight; + // // panelInfo // this.panelInfo.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); @@ -6732,6 +6871,14 @@ private void InitializeComponent() { private System.Windows.Forms.GroupBox groupboxControl; private System.Windows.Forms.RadioButton radiobuttonJoystick; private System.Windows.Forms.RadioButton radiobuttonKeyboard; + private System.Windows.Forms.RadioButton radiobuttonMouse; + private System.Windows.Forms.Panel panelMouse; + private System.Windows.Forms.Label labelMouseButton; + private System.Windows.Forms.ComboBox comboboxMouseButton; + private System.Windows.Forms.Label labelMouseModifier; + private System.Windows.Forms.CheckBox checkboxMouseShift; + private System.Windows.Forms.CheckBox checkboxMouseCtrl; + private System.Windows.Forms.CheckBox checkboxMouseAlt; private System.Windows.Forms.Panel panelKeyboard; private System.Windows.Forms.ComboBox comboboxKeyboardKey; private System.Windows.Forms.Label labelKeyboardKey; @@ -6926,6 +7073,8 @@ private void InitializeComponent() { private System.Windows.Forms.Button SaveFileNameButton; private System.Windows.Forms.TextBox textBoxPackageFileName; private System.Windows.Forms.GroupBox groupBoxOther; + private System.Windows.Forms.Label labelZoomScrollSpeed; + private System.Windows.Forms.NumericUpDown updownZoomScrollSpeed; private System.Windows.Forms.ComboBox comboBoxTimeTableDisplayMode; private System.Windows.Forms.Label labelTimeTableDisplayMode; private System.Windows.Forms.Label labelSaveAs; diff --git a/source/OpenBVE/UserInterface/formMain.Options.cs b/source/OpenBVE/UserInterface/formMain.Options.cs index 43c536808..15a6b24d1 100644 --- a/source/OpenBVE/UserInterface/formMain.Options.cs +++ b/source/OpenBVE/UserInterface/formMain.Options.cs @@ -208,5 +208,10 @@ private void comboboxCursor_SelectedIndexChanged(object sender, EventArgs e) if (Tag != null) return; Cursors.SelectedCursor(comboboxCursor, pictureboxCursor); } + + private void updownZoomScrollSpeed_ValueChanged(object sender, EventArgs e) + { + Interface.CurrentOptions.ZoomScrollSpeed = (double)updownZoomScrollSpeed.Value; + } } } diff --git a/source/OpenBVE/UserInterface/formMain.cs b/source/OpenBVE/UserInterface/formMain.cs index 729452d8b..f1a2f33fa 100644 --- a/source/OpenBVE/UserInterface/formMain.cs +++ b/source/OpenBVE/UserInterface/formMain.cs @@ -538,6 +538,7 @@ private void formMain_Load(object sender, EventArgs e) checkboxCameraInteriorTransition.Checked = Interface.CurrentOptions.CameraInteriorTransition; checkboxCameraExteriorTransition.Checked = Interface.CurrentOptions.CameraExteriorTransition; updownCameraTransitionSpeed.Value = (decimal)Interface.CurrentOptions.CameraTransitionSpeed; + updownZoomScrollSpeed.Value = (decimal)Interface.CurrentOptions.ZoomScrollSpeed; ListInputDevicePlugins(); if (Program.CurrentHost.MonoRuntime) { @@ -794,6 +795,7 @@ private void ApplyLanguage() checkboxCameraInteriorTransition.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","camera_interior_transition"}); checkboxCameraExteriorTransition.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","camera_exterior_transition"}); labelCameraTransitionSpeed.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","camera_transition_duration"}); + labelZoomScrollSpeed.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","camera_zoom_scroll_speed"}); checkboxToppling.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","misc_simulation_toppling"}); checkboxCollisions.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","misc_simulation_collisions"}); checkboxDerailments.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","misc_simulation_derailments"}); @@ -1008,6 +1010,20 @@ private void ApplyLanguage() checkboxKeyboardAlt.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_keyboard_modifiers_alt"}); radiobuttonJoystick.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_joystick"}); + radiobuttonMouse.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_mouse"}); + labelMouseButton.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_mouse_button"}); + labelMouseModifier.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_keyboard_modifiers"}); + checkboxMouseShift.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_keyboard_modifiers_shift"}); + checkboxMouseCtrl.Location = new Point(checkboxMouseShift.Location.X + (checkboxMouseShift.Text.Length + 5) * 5, checkboxMouseCtrl.Location.Y); + checkboxMouseCtrl.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_keyboard_modifiers_ctrl"}); + checkboxMouseAlt.Location = new Point(checkboxMouseCtrl.Location.X + (checkboxMouseCtrl.Text.Length + 5) * 5, checkboxMouseAlt.Location.Y); + checkboxMouseAlt.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_keyboard_modifiers_alt"}); + comboboxMouseButton.Items.Clear(); + comboboxMouseButton.Items.Add(Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_left"})); + comboboxMouseButton.Items.Add(Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_middle"})); + comboboxMouseButton.Items.Add(Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_right"})); + comboboxMouseButton.Items.Add(Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_scrollup"})); + comboboxMouseButton.Items.Add(Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_scrolldown"})); labelJoystickAssignmentCaption.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_joystick_assignment"}); textboxJoystickGrab.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","selection_keyboard_assignment_grab"}); groupboxJoysticks.Text = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","attached"}); diff --git a/source/OpenBveApi/Interface/Input/Commands.CommandInfo.cs b/source/OpenBveApi/Interface/Input/Commands.CommandInfo.cs index 5e0e55d4f..d23ff4155 100644 --- a/source/OpenBveApi/Interface/Input/Commands.CommandInfo.cs +++ b/source/OpenBveApi/Interface/Input/Commands.CommandInfo.cs @@ -240,6 +240,7 @@ public static CommandInfo TryGetInfo(this Dictionary comma { Command.CameraPOIPrevious, new CommandInfo(Command.CameraPOIPrevious, CommandType.Digital, "CAMERA_POI_PREVIOUS") }, { Command.CameraPOINext, new CommandInfo(Command.CameraPOINext, CommandType.Digital, "CAMERA_POI_NEXT") }, { Command.CameraReset, new CommandInfo(Command.CameraReset, CommandType.Digital, "CAMERA_RESET") }, + { Command.CameraGrabToggle, new CommandInfo(Command.CameraGrabToggle, CommandType.Digital, "CAMERA_GRAB_TOGGLE") }, { Command.CameraRestriction, new CommandInfo(Command.CameraRestriction, CommandType.Digital, "CAMERA_RESTRICTION") }, { Command.TimetableToggle, new CommandInfo(Command.TimetableToggle, CommandType.Digital, "TIMETABLE_TOGGLE") }, { Command.TimetableUp, new CommandInfo(Command.TimetableUp, CommandType.AnalogHalf, "TIMETABLE_UP") }, diff --git a/source/OpenBveApi/Interface/Input/Commands.cs b/source/OpenBveApi/Interface/Input/Commands.cs index 6268fc426..cbea237dd 100644 --- a/source/OpenBveApi/Interface/Input/Commands.cs +++ b/source/OpenBveApi/Interface/Input/Commands.cs @@ -120,6 +120,8 @@ public enum Command CameraPOINext, /// Reset the camera to pointing immediately forwards at track-level CameraReset, + /// Toggles camera grab (Mouse looking) + CameraGrabToggle, /// Toggle camera restriction mode CameraRestriction, /// Shows or hides the in-game timetable diff --git a/source/OpenBveApi/Interface/Input/Control.cs b/source/OpenBveApi/Interface/Input/Control.cs index bc045e331..c980aee3e 100644 --- a/source/OpenBveApi/Interface/Input/Control.cs +++ b/source/OpenBveApi/Interface/Input/Control.cs @@ -1,4 +1,4 @@ -using System; +using System; using OpenBveApi.Input; namespace OpenBveApi.Interface @@ -14,6 +14,8 @@ public struct Control public ControlMethod Method; /// Any keyboard modifiers used public KeyboardModifier Modifier; + /// The required press order of the modifiers (0 = any order, otherwise a one-based lexicographic rank) + public int ModifierOrder; /// The GUID of the device which activates this control public Guid Device; /// The joystick component which activates this control (if joystick) @@ -40,7 +42,12 @@ public override string ToString() switch (Method) { case ControlMethod.Keyboard: - s += Key + ", " + (int)Modifier + ", " + Option; + s += Key + ", " + (int)Modifier; + if (ModifierOrder != 0) + { + s += ", " + ModifierOrder; + } + s += ", " + Option; break; case ControlMethod.Joystick: s += Device + ", " + Component + ", " + Element; @@ -58,6 +65,13 @@ public override string ToString() } s += ", " + Option; break; + case ControlMethod.Mouse: + s += Element + ", " + Option; + if (Modifier != KeyboardModifier.None) + { + s += ", " + (int)Modifier; + } + break; } return s; } diff --git a/source/OpenBveApi/Interface/Input/ControlMethod.cs b/source/OpenBveApi/Interface/Input/ControlMethod.cs index 79b2fe4a5..511cc2b5f 100644 --- a/source/OpenBveApi/Interface/Input/ControlMethod.cs +++ b/source/OpenBveApi/Interface/Input/ControlMethod.cs @@ -1,4 +1,4 @@ -namespace OpenBveApi.Interface +namespace OpenBveApi.Interface { /// The method by which a control is activated public enum ControlMethod @@ -14,6 +14,8 @@ public enum ControlMethod /// This control is activated using the Input Device Plugin InputDevicePlugin = 4, /// This control is activated using a touch element - Touch = 5 + Touch = 5, + /// This control is activated using a mouse button or wheel + Mouse = 6 } } diff --git a/source/OpenBveApi/Interface/Input/ModifierOrder.cs b/source/OpenBveApi/Interface/Input/ModifierOrder.cs new file mode 100644 index 000000000..1e2f42eb3 --- /dev/null +++ b/source/OpenBveApi/Interface/Input/ModifierOrder.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; + +namespace OpenBveApi.Interface +{ + /// Provides encoding and decoding of the press order of keyboard modifiers + public static class ModifierOrder + { + private static readonly KeyboardModifier[] All = { KeyboardModifier.Shift, KeyboardModifier.Ctrl, KeyboardModifier.Alt }; + + private static int Factorial(int n) + { + int result = 1; + for (int i = 2; i <= n; i++) + { + result *= i; + } + return result; + } + + /// Computes the one-based lexicographic rank of the press sequence restricted to the currently held modifier set + /// The modifiers in the order they were pressed + /// The number of valid entries in the sequence + /// The bitmask of modifiers currently held down + public static int GetRank(IList sequence, int count, KeyboardModifier heldSet) + { + if (heldSet == KeyboardModifier.None) + { + return 0; + } + List ordered = new List(); + for (int i = 0; i < count; i++) + { + KeyboardModifier m = sequence[i]; + if ((heldSet & m) == m && !ordered.Contains(m)) + { + ordered.Add(m); + } + } + foreach (KeyboardModifier m in All) + { + if ((heldSet & m) == m && !ordered.Contains(m)) + { + // Modifiers held but missing from the recorded sequence (e.g. pressed before tracking started) are appended in canonical order + ordered.Add(m); + } + } + int[] positions = new int[ordered.Count]; + for (int i = 0; i < ordered.Count; i++) + { + positions[i] = IndexOf(All, ordered[i]); + } + int rank = 0; + for (int i = 0; i < positions.Length; i++) + { + int smaller = 0; + for (int j = i + 1; j < positions.Length; j++) + { + if (positions[j] < positions[i]) + { + smaller++; + } + } + rank += smaller * Factorial(positions.Length - 1 - i); + } + return rank + 1; + } + + /// Returns the members of the modifier set in their required press order for the given one-based rank + public static List FromRank(int set, int rank) + { + List remaining = new List(); + foreach (KeyboardModifier m in All) + { + if ((set & (int)m) != 0) + { + remaining.Add(m); + } + } + List result = new List(); + int r = rank - 1; + while (remaining.Count > 0) + { + int f = Factorial(remaining.Count - 1); + result.Add(remaining[r / f]); + remaining.RemoveAt(r / f); + r %= f; + } + return result; + } + + private static int IndexOf(KeyboardModifier[] array, KeyboardModifier value) + { + for (int i = 0; i < array.Length; i++) + { + if (array[i] == value) + { + return i; + } + } + return -1; + } + } +} diff --git a/source/OpenBveApi/Interface/Input/MouseElement.cs b/source/OpenBveApi/Interface/Input/MouseElement.cs new file mode 100644 index 000000000..b183c3143 --- /dev/null +++ b/source/OpenBveApi/Interface/Input/MouseElement.cs @@ -0,0 +1,18 @@ +namespace OpenBveApi.Interface +{ + /// Represents the mouse element (button or wheel movement) a control is bound to. + /// Values below 13 map directly onto the OpenTK MouseButton enum; wheel movements are placed above the button range so that they can never collide with additional mouse buttons. + public static class MouseElement + { + /// The left mouse button + public const int Left = 0; + /// The middle mouse button + public const int Middle = 1; + /// The right mouse button + public const int Right = 2; + /// The scroll wheel rotated up + public const int ScrollUp = 13; + /// The scroll wheel rotated down + public const int ScrollDown = 14; + } +} diff --git a/source/OpenBveApi/Math/Vectors/Vector3.cs b/source/OpenBveApi/Math/Vectors/Vector3.cs index 09c77609f..a37d5453e 100644 --- a/source/OpenBveApi/Math/Vectors/Vector3.cs +++ b/source/OpenBveApi/Math/Vectors/Vector3.cs @@ -321,6 +321,10 @@ public static implicit operator Vector3f(Vector3 v) /// The multiplied vector public static Vector3 operator *(Vector3 v, Transformation t) { + if (t == null) + { + return v; + } v = t.X * v.X + t.Y * v.Y + t.Z * v.Z; return v; } @@ -497,6 +501,10 @@ public void Rotate(Orientation3 orientation) { /// The transformation public void Rotate(Transformation transformation) { + if (transformation == null) + { + return; + } double x = transformation.X.X * X + transformation.Y.X * Y + transformation.Z.X * Z; double y = transformation.X.Y * X + transformation.Y.Y * Y + transformation.Z.Y * Z; double z = transformation.X.Z * X + transformation.Y.Z * Y + transformation.Z.Z * Z; diff --git a/source/OpenBveApi/OpenBveApi.csproj b/source/OpenBveApi/OpenBveApi.csproj index 691f0b7f1..6a79ce714 100644 --- a/source/OpenBveApi/OpenBveApi.csproj +++ b/source/OpenBveApi/OpenBveApi.csproj @@ -117,10 +117,12 @@ + + diff --git a/source/OpenBveApi/System/BaseOptions.OptionsKey.cs b/source/OpenBveApi/System/BaseOptions.OptionsKey.cs index 0fd2a7bee..92a6ec4e9 100644 --- a/source/OpenBveApi/System/BaseOptions.OptionsKey.cs +++ b/source/OpenBveApi/System/BaseOptions.OptionsKey.cs @@ -82,6 +82,7 @@ public enum OptionsKey KeyRepeatInterval, RailDriverMPH, CursorHideDelay, + ZoomScrollSpeed, // Sound Model, Range, diff --git a/source/OpenBveApi/World/Transformations.cs b/source/OpenBveApi/World/Transformations.cs index 0f3a786d0..d8bbc0a76 100644 --- a/source/OpenBveApi/World/Transformations.cs +++ b/source/OpenBveApi/World/Transformations.cs @@ -1,4 +1,4 @@ -using OpenBveApi.Math; +using OpenBveApi.Math; namespace OpenBveApi.World { @@ -81,6 +81,10 @@ public Transformation(double Yaw, double Pitch, double Roll) /// public Transformation(Transformation Transformation, double Yaw, double Pitch, double Roll) { + if (Transformation == null) + { + Transformation = NullTransformation; + } X = new Vector3(Transformation.X); Y = new Vector3(Transformation.Y); Z = new Vector3(Transformation.Z); @@ -99,6 +103,14 @@ public Transformation(Transformation Transformation, double Yaw, double Pitch, d /// The transformation to apply second public Transformation(Transformation firstTransformation, Transformation secondTransformation) { + if (firstTransformation == null) + { + firstTransformation = NullTransformation; + } + if (secondTransformation == null) + { + secondTransformation = NullTransformation; + } X = new Vector3(firstTransformation.X); Y = new Vector3(firstTransformation.Y); Z = new Vector3(firstTransformation.Z); @@ -133,6 +145,10 @@ public Transformation(Vector3f direction, Vector3f up, Vector3f side) /// The transformation to convert public static explicit operator Matrix4D(Transformation t) { + if (t == null) + { + return Matrix4D.NoTransformation; + } // X, Y and Z represent the basis vector. // Arrange them in row-major to create a change-of-basis matrix. // And converting from the left-handed coordinate system to the right-handed coordinate system by reversing the Z axis.