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.