From ea65eb14da00c7b1a12ef858e74dfcc49bb77c1b Mon Sep 17 00:00:00 2001
From: adfriz <76892624+adfriz@users.noreply.github.com>
Date: Sun, 26 Apr 2026 11:23:49 +0700
Subject: [PATCH 1/6] Overhaul mouse input system and look-around camera
- Implement smooth mouse look-around using System.Windows.Forms.Cursor.
- Fix camera spinning and choppiness by bypassing OpenTK event lag.
- Add dynamic sensitivity scaling based on zoom level (FOV).
- Add support for mouse scroll and extra buttons in input processing.
- Update Indonesian and English translations for new mouse controls.
- Add default control mappings for mouse input.
---
assets/Controls/Default.controls | 3 +-
assets/Languages/en-US.xlf | 27 +++++
assets/Languages/id-ID.xlf | 38 ++++++-
source/OpenBVE/Game/Menu/Menu.Controls.cs | 15 ++-
source/OpenBVE/System/GameWindow.cs | 2 +-
source/OpenBVE/System/Input/Controls.cs | 16 ++-
.../System/Input/ProcessControls.Analog.cs | 48 +++++++--
.../System/Input/ProcessControls.Digital.cs | 16 +++
source/OpenBVE/System/MainLoop.cs | 100 ++++++++++++++----
.../UserInterface/formMain.Controls.cs | 55 +++++++++-
.../UserInterface/formMain.Designer.cs | 67 ++++++++++--
source/OpenBVE/UserInterface/formMain.cs | 8 ++
.../Interface/Input/Commands.CommandInfo.cs | 1 +
source/OpenBveApi/Interface/Input/Commands.cs | 2 +
source/OpenBveApi/Interface/Input/Control.cs | 5 +-
.../Interface/Input/ControlMethod.cs | 6 +-
source/OpenBveApi/Math/Vectors/Vector3.cs | 8 ++
source/OpenBveApi/World/Transformations.cs | 18 +++-
18 files changed, 392 insertions(+), 43 deletions(-)
diff --git a/assets/Controls/Default.controls b/assets/Controls/Default.controls
index 0c0d9e75fe..db9d111d73 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 d678c5016b..20f0ffe9ce 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
@@ -2209,6 +2233,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 92a6e9bda9..6d57164b6a 100644
--- a/assets/Languages/id-ID.xlf
+++ b/assets/Languages/id-ID.xlf
@@ -1,4 +1,4 @@
-
+
@@ -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
@@ -2772,6 +2804,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 4777281e2c..53b1e85dd7 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 0: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_left" }); break;
+ case 1: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_middle" }); break;
+ case 2: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_right" }); break;
+ case 3: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_scrollup" }); break;
+ case 4: 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/System/GameWindow.cs b/source/OpenBVE/System/GameWindow.cs
index 8d89bd0f76..ca9dc96306 100644
--- a/source/OpenBVE/System/GameWindow.cs
+++ b/source/OpenBVE/System/GameWindow.cs
@@ -267,8 +267,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 1945c8a52a..4f667140c5 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;
@@ -332,6 +332,20 @@ 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].Option = 0;
+ if (Terms.Length >= 4 && int.TryParse(Terms[3], NumberStyles.Integer, Culture, out int Option))
+ {
+ Controls[Length].Option = Option;
+ }
+ Valid = true;
+ }
+ }
if (!Valid)
{
diff --git a/source/OpenBVE/System/Input/ProcessControls.Analog.cs b/source/OpenBVE/System/Input/ProcessControls.Analog.cs
index df0e8d5233..5ba060f899 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;
@@ -217,7 +217,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 (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ {
+ moveFactor *= 30.0;
+ }
+ Program.Renderer.Camera.Move(Control.Command, moveFactor);
break;
case Translations.Command.CameraRotateLeft:
case Translations.Command.CameraRotateRight:
@@ -225,13 +230,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 (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ {
+ rotateFactor *= 10.0; // Boost scroll rotation
+ }
+ 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 (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ {
+ factor *= 30.0;
+ }
+ Program.Renderer.Camera.AlignmentDirection.Zoom = -CameraProperties.ZoomTopSpeed * factor;
}
break;
@@ -239,7 +254,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 (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ {
+ factor *= 30.0;
+ }
+ Program.Renderer.Camera.AlignmentDirection.Zoom = CameraProperties.ZoomTopSpeed * factor;
}
break;
@@ -248,15 +268,20 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control
if (TimeElapsed > 0.0)
{
const double scrollSpeed = 250.0;
+ double timetableFactor = Control.AnalogState;
+ if (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ {
+ timetableFactor *= 5.0;
+ }
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 +294,15 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control
if (TimeElapsed > 0.0)
{
const double scrollSpeed = 250.0;
+ double timetableFactor = Control.AnalogState;
+ if (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ {
+ timetableFactor *= 5.0;
+ }
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 +321,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 5a5b818555..50813140ac 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
@@ -941,6 +955,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 5bc384c7b4..7bc0db9337 100644
--- a/source/OpenBVE/System/MainLoop.cs
+++ b/source/OpenBVE/System/MainLoop.cs
@@ -126,7 +126,10 @@ PROCESS EVENTS
internal static MouseState currentMouseState, previousMouseState;
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,11 +143,7 @@ internal static void mouseDownEvent(object sender, MouseButtonEventArgs e)
return;
}
timeSinceLastMouseEvent = 0;
- if (e.Button == MouseButton.Right)
- {
- MouseGrabEnabled = !MouseGrabEnabled;
- MouseGrabIgnoreOnce = true;
- }
+ ProcessMouseControl((int)e.Button, true);
if (e.Button == MouseButton.Left)
{
switch (Program.Renderer.CurrentInterface)
@@ -172,6 +171,7 @@ internal static void mouseUpEvent(object sender, MouseButtonEventArgs e)
return;
}
timeSinceLastMouseEvent = 0;
+ ProcessMouseControl((int)e.Button, false);
if (e.Button == MouseButton.Left)
{
if (Program.Renderer.CurrentInterface == InterfaceType.Normal)
@@ -210,6 +210,21 @@ internal static void mouseWheelEvent(object sender, MouseWheelEventArgs e)
{
Game.Menu.ProcessMouseScroll(e.Delta);
}
+ if (e.Delta != 0)
+ {
+ int element = e.Delta > 0 ? 3 : 4;
+ // 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].AnalogState += 1.0;
+ Interface.CurrentControls[i].DigitalState = DigitalControlState.Pressed;
+ }
+ }
+ if (element == 3) scrollUpPressed = true;
+ if (element == 4) scrollDownPressed = true;
+ }
}
internal static void UpdateMouse(double TimeElapsed)
@@ -221,15 +236,33 @@ 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)
{
Program.Renderer.GameWindow.CursorVisible = false;
}
- else
+
+ if (scrollUpPressed)
{
- Program.Renderer.GameWindow.CursorVisible = true;
+ ProcessMouseControl(3, false);
+ scrollUpPressed = false;
+ }
+ if (scrollDownPressed)
+ {
+ ProcessMouseControl(4, false);
+ scrollDownPressed = false;
+ }
+
+ if (MainLoop.MouseGrabEnabled)
+ {
+ Program.Renderer.GameWindow.CursorVisible = false;
+ }
+ else if (Interface.CurrentOptions.CursorHideDelay > 0 && timeSinceLastMouseEvent > Interface.CurrentOptions.CursorHideDelay)
+ {
+ Program.Renderer.GameWindow.CursorVisible = false;
}
if (MainLoop.MouseGrabEnabled)
@@ -244,8 +277,9 @@ internal static void UpdateMouse(double TimeElapsed)
factor = 3.0;
}
- Program.Renderer.Camera.AlignmentDirection.Yaw += factor * MouseGrabTarget.X;
- Program.Renderer.Camera.AlignmentDirection.Pitch -= factor * MouseGrabTarget.Y;
+ double zoomFactor = Math.Exp(Program.Renderer.Camera.Alignment.Zoom);
+ Program.Renderer.Camera.Alignment.Yaw += factor * MouseGrabTarget.X * 0.001 * zoomFactor;
+ Program.Renderer.Camera.Alignment.Pitch -= factor * MouseGrabTarget.Y * 0.001 * zoomFactor;
MouseGrabTarget = OpenBveApi.Math.Vector2.Null;
}
}
@@ -312,17 +346,28 @@ internal static void ProcessKeyboard()
}
if (MouseGrabEnabled)
{
- 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;
+ lastMouseX = System.Windows.Forms.Cursor.Position.X;
+ lastMouseY = System.Windows.Forms.Cursor.Position.Y;
+ MouseGrabTarget = OpenBveApi.Math.Vector2.Null;
+ }
+ else
+ {
+ int curX = System.Windows.Forms.Cursor.Position.X;
+ int curY = System.Windows.Forms.Cursor.Position.Y;
+ MouseGrabTarget = new OpenBveApi.Math.Vector2(curX - lastMouseX, curY - lastMouseY);
+ lastMouseX = curX;
+ lastMouseY = curY;
+ if (Math.Abs(curX - screenCenter.X) > 400 || Math.Abs(curY - screenCenter.Y) > 400)
{
- MouseGrabTarget = new OpenBveApi.Math.Vector2(currentMouseState.X - previousMouseState.X, currentMouseState.Y - previousMouseState.Y);
+ System.Windows.Forms.Cursor.Position = screenCenter;
+ MouseGrabIgnoreOnce = true;
}
}
}
@@ -603,5 +648,24 @@ 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)
+ {
+ if (pressed)
+ {
+ Interface.CurrentControls[i].AnalogState = 1.0;
+ Interface.CurrentControls[i].DigitalState = DigitalControlState.Pressed;
+ }
+ else
+ {
+ Interface.CurrentControls[i].AnalogState = 0.0;
+ Interface.CurrentControls[i].DigitalState = DigitalControlState.Released;
+ }
+ }
+ }
+ }
}
}
diff --git a/source/OpenBVE/UserInterface/formMain.Controls.cs b/source/OpenBVE/UserInterface/formMain.Controls.cs
index dc97128f65..26d9bd1035 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;
@@ -54,13 +54,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))
@@ -72,13 +77,20 @@ private void listviewControls_SelectedIndexChanged(object sender, EventArgs e) {
checkboxKeyboardAlt.Checked = (Interface.CurrentControls[i].Modifier & KeyboardModifier.Alt) != 0;
} else if (radiobuttonJoystick.Checked) {
labelJoystickAssignmentValue.Text = GetControlDetails(i);
+ } else if (radiobuttonMouse.Checked) {
+ comboboxMouseButton.SelectedIndex = Interface.CurrentControls[i].Element;
} else {
comboboxKeyboardKey.SelectedIndex = -1;
checkboxKeyboardShift.Checked = false;
checkboxKeyboardCtrl.Checked = false;
checkboxKeyboardAlt.Checked = false;
+ comboboxMouseButton.SelectedIndex = -1;
}
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 +103,13 @@ 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;
labelJoystickAssignmentValue.Text = "";
Tag = null;
buttonControlRemove.Enabled = false;
@@ -122,6 +136,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;
@@ -266,6 +283,18 @@ 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;
+ switch (Interface.CurrentControls[Index].Element) {
+ case 0: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_left"}); break;
+ case 1: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_middle"}); break;
+ case 2: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_right"}); break;
+ case 3: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_scrollup"}); break;
+ case 4: 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"});
}
@@ -359,12 +388,34 @@ 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) {
+ int i = listviewControls.SelectedIndices[0];
+ Interface.CurrentControls[i].Element = comboboxMouseButton.SelectedIndex;
+ UpdateControlListElement(listviewControls.Items[i], i, true);
+ }
}
// key
@@ -426,6 +477,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 +486,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"});
}
diff --git a/source/OpenBVE/UserInterface/formMain.Designer.cs b/source/OpenBVE/UserInterface/formMain.Designer.cs
index cf15c3aaa7..c3ee7a5902 100644
--- a/source/OpenBVE/UserInterface/formMain.Designer.cs
+++ b/source/OpenBVE/UserInterface/formMain.Designer.cs
@@ -336,6 +336,10 @@ 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.panelInfo = new System.Windows.Forms.Panel();
this.linkLabelReportBug = new System.Windows.Forms.LinkLabel();
this.linkLabelCheckUpdates = new System.Windows.Forms.LinkLabel();
@@ -4312,6 +4316,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);
@@ -4408,22 +4414,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;
@@ -4465,7 +4471,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;
@@ -4491,7 +4497,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;
@@ -4512,6 +4518,51 @@ 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.Enabled = false;
+ this.panelMouse.Location = new System.Drawing.Point(8, 72);
+ this.panelMouse.Name = "panelMouse";
+ this.panelMouse.Size = new System.Drawing.Size(192, 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);
+ //
// panelInfo
//
this.panelInfo.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
@@ -6695,6 +6746,10 @@ 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.Panel panelKeyboard;
private System.Windows.Forms.ComboBox comboboxKeyboardKey;
private System.Windows.Forms.Label labelKeyboardKey;
diff --git a/source/OpenBVE/UserInterface/formMain.cs b/source/OpenBVE/UserInterface/formMain.cs
index e9c132ee2f..67de32a316 100644
--- a/source/OpenBVE/UserInterface/formMain.cs
+++ b/source/OpenBVE/UserInterface/formMain.cs
@@ -994,6 +994,14 @@ 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"});
+ 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 1a997a56dd..776b72ba6e 100644
--- a/source/OpenBveApi/Interface/Input/Commands.CommandInfo.cs
+++ b/source/OpenBveApi/Interface/Input/Commands.CommandInfo.cs
@@ -238,6 +238,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 18c450ead3..e738bf49b1 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 bc045e331b..bbd8a1d5a4 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
@@ -58,6 +58,9 @@ public override string ToString()
}
s += ", " + Option;
break;
+ case ControlMethod.Mouse:
+ s += Element + ", " + Option;
+ break;
}
return s;
}
diff --git a/source/OpenBveApi/Interface/Input/ControlMethod.cs b/source/OpenBveApi/Interface/Input/ControlMethod.cs
index 79b2fe4a5e..511cc2b5f2 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/Math/Vectors/Vector3.cs b/source/OpenBveApi/Math/Vectors/Vector3.cs
index 09c77609f8..a37d5453ea 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/World/Transformations.cs b/source/OpenBveApi/World/Transformations.cs
index 0f3a786d0e..d8bbc0a768 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.
From 9a78ac5bf9331eb7f63d8297fac5c81d2fb98584 Mon Sep 17 00:00:00 2001
From: adfriz <76892624+adfriz@users.noreply.github.com>
Date: Wed, 6 May 2026 22:43:08 +0700
Subject: [PATCH 2/6] Decouple mouse-look from WinForms, add configurable Zoom
Scroll Speed setting with max limit, and update localization.
---
assets/Languages/en-US.xlf | 4 ++
assets/Languages/id-ID.xlf | 21 ++++++++++
.../System/Input/ProcessControls.Analog.cs | 6 +--
source/OpenBVE/System/MainLoop.cs | 13 +++---
source/OpenBVE/System/Options.cs | 9 ++++
.../UserInterface/formMain.Designer.cs | 42 ++++++++++++++++++-
.../OpenBVE/UserInterface/formMain.Options.cs | 5 +++
source/OpenBVE/UserInterface/formMain.cs | 6 +++
.../System/BaseOptions.OptionsKey.cs | 1 +
9 files changed, 97 insertions(+), 10 deletions(-)
diff --git a/assets/Languages/en-US.xlf b/assets/Languages/en-US.xlf
index 20f0ffe9ce..fb67028e84 100755
--- a/assets/Languages/en-US.xlf
+++ b/assets/Languages/en-US.xlf
@@ -1240,6 +1240,9 @@
Transition duration (sec):
+
+ Zoom Scroll Speed:
+
Detail of simulation
@@ -1336,6 +1339,7 @@
Prefer custom timetable
+
Choose...
diff --git a/assets/Languages/id-ID.xlf b/assets/Languages/id-ID.xlf
index 6d57164b6a..3fe8b554b6 100644
--- a/assets/Languages/id-ID.xlf
+++ b/assets/Languages/id-ID.xlf
@@ -1515,6 +1515,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
@@ -1641,6 +1661,7 @@
Prefer custom timetable
Buat sendiri
+
Choose...
Pilih....
diff --git a/source/OpenBVE/System/Input/ProcessControls.Analog.cs b/source/OpenBVE/System/Input/ProcessControls.Analog.cs
index 5ba060f899..c9b0b43dfe 100644
--- a/source/OpenBVE/System/Input/ProcessControls.Analog.cs
+++ b/source/OpenBVE/System/Input/ProcessControls.Analog.cs
@@ -220,7 +220,7 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control
double moveFactor = Control.AnalogState;
if (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
{
- moveFactor *= 30.0;
+ moveFactor *= Interface.CurrentOptions.ZoomScrollSpeed;
}
Program.Renderer.Camera.Move(Control.Command, moveFactor);
break;
@@ -244,7 +244,7 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control
double factor = Control.AnalogState;
if (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
{
- factor *= 30.0;
+ factor *= Interface.CurrentOptions.ZoomScrollSpeed;
}
Program.Renderer.Camera.AlignmentDirection.Zoom = -CameraProperties.ZoomTopSpeed * factor;
}
@@ -257,7 +257,7 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control
double factor = Control.AnalogState;
if (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
{
- factor *= 30.0;
+ factor *= Interface.CurrentOptions.ZoomScrollSpeed;
}
Program.Renderer.Camera.AlignmentDirection.Zoom = CameraProperties.ZoomTopSpeed * factor;
}
diff --git a/source/OpenBVE/System/MainLoop.cs b/source/OpenBVE/System/MainLoop.cs
index 7bc0db9337..ba7ad88787 100644
--- a/source/OpenBVE/System/MainLoop.cs
+++ b/source/OpenBVE/System/MainLoop.cs
@@ -280,6 +280,7 @@ internal static void UpdateMouse(double TimeElapsed)
double zoomFactor = Math.Exp(Program.Renderer.Camera.Alignment.Zoom);
Program.Renderer.Camera.Alignment.Yaw += factor * MouseGrabTarget.X * 0.001 * zoomFactor;
Program.Renderer.Camera.Alignment.Pitch -= factor * MouseGrabTarget.Y * 0.001 * zoomFactor;
+ Program.Renderer.UpdateViewingDistances(Program.CurrentRoute.CurrentBackground.BackgroundImageDistance);
MouseGrabTarget = OpenBveApi.Math.Vector2.Null;
}
}
@@ -353,20 +354,22 @@ internal static void ProcessKeyboard()
if (MouseGrabIgnoreOnce)
{
MouseGrabIgnoreOnce = false;
- lastMouseX = System.Windows.Forms.Cursor.Position.X;
- lastMouseY = System.Windows.Forms.Cursor.Position.Y;
+ MouseState state = Mouse.GetCursorState();
+ lastMouseX = state.X;
+ lastMouseY = state.Y;
MouseGrabTarget = OpenBveApi.Math.Vector2.Null;
}
else
{
- int curX = System.Windows.Forms.Cursor.Position.X;
- int curY = System.Windows.Forms.Cursor.Position.Y;
+ 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) > 400 || Math.Abs(curY - screenCenter.Y) > 400)
{
- System.Windows.Forms.Cursor.Position = screenCenter;
+ Mouse.SetPosition(screenCenter.X, screenCenter.Y);
MouseGrabIgnoreOnce = true;
}
}
diff --git a/source/OpenBVE/System/Options.cs b/source/OpenBVE/System/Options.cs
index 68dc8b0a9a..4c78522bf2 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;
@@ -355,6 +358,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);
@@ -565,6 +569,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.Designer.cs b/source/OpenBVE/UserInterface/formMain.Designer.cs
index c3ee7a5902..c8d62d7899 100644
--- a/source/OpenBVE/UserInterface/formMain.Designer.cs
+++ b/source/OpenBVE/UserInterface/formMain.Designer.cs
@@ -137,6 +137,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();
@@ -2093,11 +2095,43 @@ private void InitializeComponent() {
this.groupBoxOther.ForeColor = System.Drawing.Color.Black;
this.groupBoxOther.Location = new System.Drawing.Point(0, 347);
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.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(152, 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)));
@@ -2658,10 +2692,12 @@ 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, 0);
this.groupboxCamera.Name = "groupboxCamera";
- this.groupboxCamera.Size = new System.Drawing.Size(321, 120);
+ this.groupboxCamera.Size = new System.Drawing.Size(321, 150);
this.groupboxCamera.TabIndex = 22;
this.groupboxCamera.TabStop = false;
this.groupboxCamera.Text = "Camera options";
@@ -6944,6 +6980,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 068943f4e6..2a39fec875 100644
--- a/source/OpenBVE/UserInterface/formMain.Options.cs
+++ b/source/OpenBVE/UserInterface/formMain.Options.cs
@@ -195,5 +195,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 67de32a316..704aa644f1 100644
--- a/source/OpenBVE/UserInterface/formMain.cs
+++ b/source/OpenBVE/UserInterface/formMain.cs
@@ -525,6 +525,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)
{
@@ -779,6 +780,11 @@ 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" });
+ if (labelZoomScrollSpeed.Text == "camera_zoom_scroll_speed")
+ {
+ labelZoomScrollSpeed.Text = "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"});
diff --git a/source/OpenBveApi/System/BaseOptions.OptionsKey.cs b/source/OpenBveApi/System/BaseOptions.OptionsKey.cs
index d0952ff247..d50a1e9d00 100644
--- a/source/OpenBveApi/System/BaseOptions.OptionsKey.cs
+++ b/source/OpenBveApi/System/BaseOptions.OptionsKey.cs
@@ -80,6 +80,7 @@ public enum OptionsKey
KeyRepeatInterval,
RailDriverMPH,
CursorHideDelay,
+ ZoomScrollSpeed,
// Sound
Model,
Range,
From da02683b0801a5a62e3957560abbf788ffa2cd0f Mon Sep 17 00:00:00 2001
From: adfriz <76892624+adfriz@users.noreply.github.com>
Date: Fri, 21 Aug 2026 23:28:48 +0700
Subject: [PATCH 3/6] Fix: Prevent additional mouse buttons from being
interpreted as scroll wheel movements
---
source/OpenBVE/Game/Menu/Menu.Controls.cs | 10 +++---
.../System/Input/ProcessControls.Analog.cs | 35 ++++++++++++-------
source/OpenBVE/System/MainLoop.cs | 28 +++++++++++----
.../UserInterface/formMain.Controls.cs | 21 ++++++-----
.../Interface/Input/MouseElement.cs | 18 ++++++++++
source/OpenBveApi/OpenBveApi.csproj | 1 +
6 files changed, 80 insertions(+), 33 deletions(-)
create mode 100644 source/OpenBveApi/Interface/Input/MouseElement.cs
diff --git a/source/OpenBVE/Game/Menu/Menu.Controls.cs b/source/OpenBVE/Game/Menu/Menu.Controls.cs
index 53b1e85dd7..e6d4abbb33 100644
--- a/source/OpenBVE/Game/Menu/Menu.Controls.cs
+++ b/source/OpenBVE/Game/Menu/Menu.Controls.cs
@@ -75,11 +75,11 @@ private static string GetControlDescription(int idx)
str = Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse" }) + " [";
switch (loadedControl.Element)
{
- case 0: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_left" }); break;
- case 1: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_middle" }); break;
- case 2: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_right" }); break;
- case 3: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_scrollup" }); break;
- case 4: str += Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "controls", "assignment_mouse_scrolldown" }); break;
+ 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 += "]";
diff --git a/source/OpenBVE/System/Input/ProcessControls.Analog.cs b/source/OpenBVE/System/Input/ProcessControls.Analog.cs
index c9b0b43dfe..4dd63a3112 100644
--- a/source/OpenBVE/System/Input/ProcessControls.Analog.cs
+++ b/source/OpenBVE/System/Input/ProcessControls.Analog.cs
@@ -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
@@ -218,9 +229,9 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control
case Translations.Command.CameraMoveUp:
case Translations.Command.CameraMoveDown:
double moveFactor = Control.AnalogState;
- if (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ if (IsMouseScroll(Control))
{
- moveFactor *= Interface.CurrentOptions.ZoomScrollSpeed;
+ moveFactor *= Interface.CurrentOptions.ZoomScrollSpeed;
}
Program.Renderer.Camera.Move(Control.Command, moveFactor);
break;
@@ -231,9 +242,9 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control
case Translations.Command.CameraRotateCCW:
case Translations.Command.CameraRotateCW:
double rotateFactor = Control.AnalogState;
- if (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ if (IsMouseScroll(Control))
{
- rotateFactor *= 10.0; // Boost scroll rotation
+ rotateFactor *= ScrollRotateBoost;
}
Program.Renderer.Camera.Rotate(Control.Command, rotateFactor);
break;
@@ -242,9 +253,9 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control
if (TimeElapsed > 0.0)
{
double factor = Control.AnalogState;
- if (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ if (IsMouseScroll(Control))
{
- factor *= Interface.CurrentOptions.ZoomScrollSpeed;
+ factor *= Interface.CurrentOptions.ZoomScrollSpeed;
}
Program.Renderer.Camera.AlignmentDirection.Zoom = -CameraProperties.ZoomTopSpeed * factor;
}
@@ -255,9 +266,9 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control
if (TimeElapsed > 0.0)
{
double factor = Control.AnalogState;
- if (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ if (IsMouseScroll(Control))
{
- factor *= Interface.CurrentOptions.ZoomScrollSpeed;
+ factor *= Interface.CurrentOptions.ZoomScrollSpeed;
}
Program.Renderer.Camera.AlignmentDirection.Zoom = CameraProperties.ZoomTopSpeed * factor;
}
@@ -269,9 +280,9 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control
{
const double scrollSpeed = 250.0;
double timetableFactor = Control.AnalogState;
- if (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ if (IsMouseScroll(Control))
{
- timetableFactor *= 5.0;
+ timetableFactor *= ScrollTimetableBoost;
}
switch (Program.Renderer.CurrentTimetable)
{
@@ -295,9 +306,9 @@ private static void ProcessAnalogControl(double TimeElapsed, ref Control Control
{
const double scrollSpeed = 250.0;
double timetableFactor = Control.AnalogState;
- if (Control.Method == ControlMethod.Mouse && Control.Element >= 3)
+ if (IsMouseScroll(Control))
{
- timetableFactor *= 5.0;
+ timetableFactor *= ScrollTimetableBoost;
}
switch (Program.Renderer.CurrentTimetable)
{
diff --git a/source/OpenBVE/System/MainLoop.cs b/source/OpenBVE/System/MainLoop.cs
index 48c47e9cb2..138b89876d 100644
--- a/source/OpenBVE/System/MainLoop.cs
+++ b/source/OpenBVE/System/MainLoop.cs
@@ -143,7 +143,14 @@ internal static void mouseDownEvent(object sender, MouseButtonEventArgs e)
return;
}
timeSinceLastMouseEvent = 0;
- ProcessMouseControl((int)e.Button, true);
+ switch (e.Button)
+ {
+ case MouseButton.Left:
+ case MouseButton.Middle:
+ case MouseButton.Right:
+ ProcessMouseControl((int)e.Button, true);
+ break;
+ }
if (e.Button == MouseButton.Left)
{
switch (Program.Renderer.CurrentInterface)
@@ -171,7 +178,14 @@ internal static void mouseUpEvent(object sender, MouseButtonEventArgs e)
return;
}
timeSinceLastMouseEvent = 0;
- ProcessMouseControl((int)e.Button, false);
+ 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)
@@ -212,7 +226,7 @@ internal static void mouseWheelEvent(object sender, MouseWheelEventArgs e)
}
if (e.Delta != 0)
{
- int element = e.Delta > 0 ? 3 : 4;
+ 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++)
{
@@ -222,8 +236,8 @@ internal static void mouseWheelEvent(object sender, MouseWheelEventArgs e)
Interface.CurrentControls[i].DigitalState = DigitalControlState.Pressed;
}
}
- if (element == 3) scrollUpPressed = true;
- if (element == 4) scrollDownPressed = true;
+ if (element == MouseElement.ScrollUp) scrollUpPressed = true;
+ if (element == MouseElement.ScrollDown) scrollDownPressed = true;
}
}
@@ -247,12 +261,12 @@ internal static void UpdateMouse(double TimeElapsed)
if (scrollUpPressed)
{
- ProcessMouseControl(3, false);
+ ProcessMouseControl(MouseElement.ScrollUp, false);
scrollUpPressed = false;
}
if (scrollDownPressed)
{
- ProcessMouseControl(4, false);
+ ProcessMouseControl(MouseElement.ScrollDown, false);
scrollDownPressed = false;
}
diff --git a/source/OpenBVE/UserInterface/formMain.Controls.cs b/source/OpenBVE/UserInterface/formMain.Controls.cs
index 26d9bd1035..c61d4ccc21 100644
--- a/source/OpenBVE/UserInterface/formMain.Controls.cs
+++ b/source/OpenBVE/UserInterface/formMain.Controls.cs
@@ -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
// ========
@@ -78,7 +81,7 @@ private void listviewControls_SelectedIndexChanged(object sender, EventArgs e) {
} else if (radiobuttonJoystick.Checked) {
labelJoystickAssignmentValue.Text = GetControlDetails(i);
} else if (radiobuttonMouse.Checked) {
- comboboxMouseButton.SelectedIndex = Interface.CurrentControls[i].Element;
+ comboboxMouseButton.SelectedIndex = Array.IndexOf(MouseButtonElements, Interface.CurrentControls[i].Element);
} else {
comboboxKeyboardKey.SelectedIndex = -1;
checkboxKeyboardShift.Checked = false;
@@ -286,11 +289,11 @@ private string GetControlDetails(int Index) {
if (Interface.CurrentControls[Index].Method == ControlMethod.Mouse) {
string t = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse"}) + Separator;
switch (Interface.CurrentControls[Index].Element) {
- case 0: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_left"}); break;
- case 1: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_middle"}); break;
- case 2: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_right"}); break;
- case 3: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_scrollup"}); break;
- case 4: t += Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse_scrolldown"}); break;
+ 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;
@@ -411,9 +414,9 @@ private void radiobuttonMouse_CheckedChanged(object sender, EventArgs e) {
}
private void comboboxMouseButton_SelectedIndexChanged(object sender, EventArgs e) {
- if (Tag == null & listviewControls.SelectedIndices.Count == 1) {
+ if (Tag == null & listviewControls.SelectedIndices.Count == 1 && comboboxMouseButton.SelectedIndex >= 0) {
int i = listviewControls.SelectedIndices[0];
- Interface.CurrentControls[i].Element = comboboxMouseButton.SelectedIndex;
+ Interface.CurrentControls[i].Element = MouseButtonElements[comboboxMouseButton.SelectedIndex];
UpdateControlListElement(listviewControls.Items[i], i, true);
}
}
diff --git a/source/OpenBveApi/Interface/Input/MouseElement.cs b/source/OpenBveApi/Interface/Input/MouseElement.cs
new file mode 100644
index 0000000000..b183c31432
--- /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/OpenBveApi.csproj b/source/OpenBveApi/OpenBveApi.csproj
index 691f0b7f17..db099f1922 100644
--- a/source/OpenBveApi/OpenBveApi.csproj
+++ b/source/OpenBveApi/OpenBveApi.csproj
@@ -121,6 +121,7 @@
+
From e29fa63b7754c5f74ee11a7fea49266ec0d80959 Mon Sep 17 00:00:00 2001
From: adfriz <76892624+adfriz@users.noreply.github.com>
Date: Fri, 21 Aug 2026 23:30:06 +0700
Subject: [PATCH 4/6] Change: Improve mouse-look performance and focus
handling, clean up mouse input code
---
source/OpenBVE/System/MainLoop.cs | 72 +++++++++++++++---------
source/OpenBVE/UserInterface/formMain.cs | 6 +-
2 files changed, 46 insertions(+), 32 deletions(-)
diff --git a/source/OpenBVE/System/MainLoop.cs b/source/OpenBVE/System/MainLoop.cs
index 138b89876d..2bc69c621d 100644
--- a/source/OpenBVE/System/MainLoop.cs
+++ b/source/OpenBVE/System/MainLoop.cs
@@ -122,8 +122,14 @@ 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;
@@ -254,11 +260,6 @@ internal static void UpdateMouse(double TimeElapsed)
MainLoop.MouseGrabEnabled = false;
}
- if (Interface.CurrentOptions.CursorHideDelay > 0 && timeSinceLastMouseEvent > Interface.CurrentOptions.CursorHideDelay)
- {
- Program.Renderer.GameWindow.CursorVisible = false;
- }
-
if (scrollUpPressed)
{
ProcessMouseControl(MouseElement.ScrollUp, false);
@@ -269,34 +270,51 @@ internal static void UpdateMouse(double TimeElapsed)
ProcessMouseControl(MouseElement.ScrollDown, false);
scrollDownPressed = false;
}
-
+
if (MainLoop.MouseGrabEnabled)
{
Program.Renderer.GameWindow.CursorVisible = false;
+ if (Program.Renderer.GameWindow.Focused)
+ {
+ ApplyMouseGrab();
+ }
+ else
+ {
+ // 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;
}
+ }
- if (MainLoop.MouseGrabEnabled)
+ /// Applies the accumulated mouse movement to the camera whilst the mouse is grabbed
+ private static void ApplyMouseGrab()
+ {
+ if (MouseGrabTarget.IsNullVector())
{
- double factor;
- if (Program.Renderer.Camera.CurrentMode == CameraViewMode.Interior | Program.Renderer.Camera.CurrentMode == CameraViewMode.InteriorLookAhead)
- {
- factor = 1.0;
- }
- else
- {
- factor = 3.0;
- }
-
- double zoomFactor = Math.Exp(Program.Renderer.Camera.Alignment.Zoom);
- Program.Renderer.Camera.Alignment.Yaw += factor * MouseGrabTarget.X * 0.001 * zoomFactor;
- Program.Renderer.Camera.Alignment.Pitch -= factor * MouseGrabTarget.Y * 0.001 * zoomFactor;
- Program.Renderer.UpdateViewingDistances(Program.CurrentRoute.CurrentBackground.BackgroundImageDistance);
- MouseGrabTarget = OpenBveApi.Math.Vector2.Null;
+ 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;
}
//
@@ -359,12 +377,12 @@ internal static void ProcessKeyboard()
}
return;
}
- if (MouseGrabEnabled)
+ if (MouseGrabEnabled && Program.Renderer.GameWindow.Focused)
{
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)
{
MouseGrabIgnoreOnce = false;
@@ -381,7 +399,7 @@ internal static void ProcessKeyboard()
MouseGrabTarget = new OpenBveApi.Math.Vector2(curX - lastMouseX, curY - lastMouseY);
lastMouseX = curX;
lastMouseY = curY;
- if (Math.Abs(curX - screenCenter.X) > 400 || Math.Abs(curY - screenCenter.Y) > 400)
+ if (Math.Abs(curX - screenCenter.X) > RecenterThresholdPixels || Math.Abs(curY - screenCenter.Y) > RecenterThresholdPixels)
{
Mouse.SetPosition(screenCenter.X, screenCenter.Y);
MouseGrabIgnoreOnce = true;
diff --git a/source/OpenBVE/UserInterface/formMain.cs b/source/OpenBVE/UserInterface/formMain.cs
index 476e6d7d58..ff4bf858fb 100644
--- a/source/OpenBVE/UserInterface/formMain.cs
+++ b/source/OpenBVE/UserInterface/formMain.cs
@@ -795,11 +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" });
- if (labelZoomScrollSpeed.Text == "camera_zoom_scroll_speed")
- {
- labelZoomScrollSpeed.Text = "Zoom Scroll Speed:";
- }
+ 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"});
From ee2ec3575f72bf7ed653fd74f432add91ccb9022 Mon Sep 17 00:00:00 2001
From: adfriz <76892624+adfriz@users.noreply.github.com>
Date: Sat, 22 Aug 2026 11:43:58 +0700
Subject: [PATCH 5/6] Change: Support keyboard modifiers for mouse bindings and
fix camera options overlap
- Track held modifiers across KeyDown/KeyUp so mouse bindings can require them
- Load/save an optional trailing modifier column for mouse controls (backwards compatible)
- Show Shift/Ctrl/Alt checkboxes for mouse bindings in Options > Controls
- Fix updownZoomScrollSpeed overflowing its groupbox and groupboxCamera overlapping the Object Parser group
---
source/OpenBVE/System/Input/Controls.cs | 5 ++
source/OpenBVE/System/Input/Keyboard.cs | 18 +++++-
source/OpenBVE/System/MainLoop.cs | 16 +++--
.../UserInterface/formMain.Controls.cs | 41 ++++++++++++
.../UserInterface/formMain.Designer.cs | 62 ++++++++++++++++++-
source/OpenBVE/UserInterface/formMain.cs | 6 ++
source/OpenBveApi/Interface/Input/Control.cs | 4 ++
7 files changed, 139 insertions(+), 13 deletions(-)
diff --git a/source/OpenBVE/System/Input/Controls.cs b/source/OpenBVE/System/Input/Controls.cs
index 4f667140c5..cf3602cd18 100644
--- a/source/OpenBVE/System/Input/Controls.cs
+++ b/source/OpenBVE/System/Input/Controls.cs
@@ -338,11 +338,16 @@ internal static void LoadControls(string FileOrNull, out Control[] Controls)
{
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;
}
}
diff --git a/source/OpenBVE/System/Input/Keyboard.cs b/source/OpenBVE/System/Input/Keyboard.cs
index 2a0de29337..5f76ce750c 100644
--- a/source/OpenBVE/System/Input/Keyboard.cs
+++ b/source/OpenBVE/System/Input/Keyboard.cs
@@ -34,6 +34,7 @@ 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;
if (Program.Renderer.CurrentInterface >= InterfaceType.Menu && Game.Menu.IsCustomizingControl())
{
Game.Menu.SetControlKbdCustomData((OpenBveApi.Input.Key)e.Key, CurrentKeyboardModifier);
@@ -103,8 +104,23 @@ 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;
+ break;
+ case Key.ControlLeft:
+ case Key.ControlRight:
+ HeldKeyboardModifiers &= ~KeyboardModifier.Ctrl;
+ break;
+ case Key.AltLeft:
+ case Key.AltRight:
+ HeldKeyboardModifiers &= ~KeyboardModifier.Alt;
+ break;
+ }
//Traverse the controls array
for (int i = 0; i < Interface.CurrentControls.Length; i++)
{
diff --git a/source/OpenBVE/System/MainLoop.cs b/source/OpenBVE/System/MainLoop.cs
index 2bc69c621d..5022fc2daa 100644
--- a/source/OpenBVE/System/MainLoop.cs
+++ b/source/OpenBVE/System/MainLoop.cs
@@ -236,7 +236,7 @@ internal static void mouseWheelEvent(object sender, MouseWheelEventArgs e)
// 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)
+ if (Interface.CurrentControls[i].Method == ControlMethod.Mouse && Interface.CurrentControls[i].Element == element && Interface.CurrentControls[i].Modifier == HeldKeyboardModifiers)
{
Interface.CurrentControls[i].AnalogState += 1.0;
Interface.CurrentControls[i].DigitalState = DigitalControlState.Pressed;
@@ -321,6 +321,8 @@ private static void ApplyMouseGrab()
// 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;
internal static void ProcessKeyboard()
{
@@ -689,15 +691,11 @@ private static void ProcessMouseControl(int element, bool pressed)
{
if (Interface.CurrentControls[i].Method == ControlMethod.Mouse && Interface.CurrentControls[i].Element == element)
{
- if (pressed)
+ // On press require the modifiers to match; on release always release so controls cannot get stuck
+ if (!pressed || Interface.CurrentControls[i].Modifier == HeldKeyboardModifiers)
{
- Interface.CurrentControls[i].AnalogState = 1.0;
- Interface.CurrentControls[i].DigitalState = DigitalControlState.Pressed;
- }
- else
- {
- Interface.CurrentControls[i].AnalogState = 0.0;
- Interface.CurrentControls[i].DigitalState = DigitalControlState.Released;
+ Interface.CurrentControls[i].AnalogState = pressed ? 1.0 : 0.0;
+ Interface.CurrentControls[i].DigitalState = pressed ? DigitalControlState.Pressed : DigitalControlState.Released;
}
}
}
diff --git a/source/OpenBVE/UserInterface/formMain.Controls.cs b/source/OpenBVE/UserInterface/formMain.Controls.cs
index c61d4ccc21..bc0521aad3 100644
--- a/source/OpenBVE/UserInterface/formMain.Controls.cs
+++ b/source/OpenBVE/UserInterface/formMain.Controls.cs
@@ -82,12 +82,18 @@ private void listviewControls_SelectedIndexChanged(object sender, EventArgs e) {
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;
} 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;
}
panelJoystick.Enabled = radiobuttonJoystick.Checked;
panelJoystick.Visible = radiobuttonJoystick.Checked;
@@ -113,6 +119,9 @@ private void listviewControls_SelectedIndexChanged(object sender, EventArgs e) {
checkboxKeyboardCtrl.Checked = false;
checkboxKeyboardAlt.Checked = false;
comboboxMouseButton.SelectedIndex = -1;
+ checkboxMouseShift.Checked = false;
+ checkboxMouseCtrl.Checked = false;
+ checkboxMouseAlt.Checked = false;
labelJoystickAssignmentValue.Text = "";
Tag = null;
buttonControlRemove.Enabled = false;
@@ -288,6 +297,9 @@ private string GetControlDetails(int Index) {
}
if (Interface.CurrentControls[Index].Method == ControlMethod.Mouse) {
string t = Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"controls","assignment_mouse"}) + 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"});
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;
@@ -421,6 +433,35 @@ private void comboboxMouseButton_SelectedIndexChanged(object sender, EventArgs e
}
}
+ // mouse modifiers
+ private void checkboxMouseShift_CheckedChanged(object sender, EventArgs e) {
+ if (Tag == null & listviewControls.SelectedIndices.Count == 1) {
+ int i = listviewControls.SelectedIndices[0];
+ Interface.CurrentControls[i].Modifier = (checkboxMouseShift.Checked ? KeyboardModifier.Shift : KeyboardModifier.None) |
+ (checkboxMouseCtrl.Checked ? KeyboardModifier.Ctrl : KeyboardModifier.None) |
+ (checkboxMouseAlt.Checked ? KeyboardModifier.Alt : KeyboardModifier.None);
+ UpdateControlListElement(listviewControls.Items[i], i, true);
+ }
+ }
+ private void checkboxMouseCtrl_CheckedChanged(object sender, EventArgs e) {
+ if (Tag == null & listviewControls.SelectedIndices.Count == 1) {
+ int i = listviewControls.SelectedIndices[0];
+ Interface.CurrentControls[i].Modifier = (checkboxMouseShift.Checked ? KeyboardModifier.Shift : KeyboardModifier.None) |
+ (checkboxMouseCtrl.Checked ? KeyboardModifier.Ctrl : KeyboardModifier.None) |
+ (checkboxMouseAlt.Checked ? KeyboardModifier.Alt : KeyboardModifier.None);
+ UpdateControlListElement(listviewControls.Items[i], i, true);
+ }
+ }
+ private void checkboxMouseAlt_CheckedChanged(object sender, EventArgs e) {
+ if (Tag == null & listviewControls.SelectedIndices.Count == 1) {
+ int i = listviewControls.SelectedIndices[0];
+ Interface.CurrentControls[i].Modifier = (checkboxMouseShift.Checked ? KeyboardModifier.Shift : KeyboardModifier.None) |
+ (checkboxMouseCtrl.Checked ? KeyboardModifier.Ctrl : KeyboardModifier.None) |
+ (checkboxMouseAlt.Checked ? KeyboardModifier.Alt : KeyboardModifier.None);
+ UpdateControlListElement(listviewControls.Items[i], i, true);
+ }
+ }
+
// key
private void comboboxKeyboardKey_SelectedIndexChanged(object sender, EventArgs e) {
if (blockComboBoxIndexEvent)
diff --git a/source/OpenBVE/UserInterface/formMain.Designer.cs b/source/OpenBVE/UserInterface/formMain.Designer.cs
index cda3905bf8..bc7cde65e0 100644
--- a/source/OpenBVE/UserInterface/formMain.Designer.cs
+++ b/source/OpenBVE/UserInterface/formMain.Designer.cs
@@ -344,6 +344,10 @@ private void InitializeComponent() {
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();
@@ -2134,6 +2138,7 @@ private void InitializeComponent() {
//
// 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);
@@ -2155,7 +2160,7 @@ private void InitializeComponent() {
0,
0});
this.updownZoomScrollSpeed.Name = "updownZoomScrollSpeed";
- this.updownZoomScrollSpeed.Size = new System.Drawing.Size(152, 20);
+ this.updownZoomScrollSpeed.Size = new System.Drawing.Size(52, 20);
this.updownZoomScrollSpeed.TabIndex = 3;
this.updownZoomScrollSpeed.Value = new decimal(new int[] {
30,
@@ -2734,7 +2739,7 @@ private void InitializeComponent() {
this.groupboxCamera.ForeColor = System.Drawing.Color.Black;
this.groupboxCamera.Location = new System.Drawing.Point(330, 245);
this.groupboxCamera.Name = "groupboxCamera";
- this.groupboxCamera.Size = new System.Drawing.Size(321, 150);
+ this.groupboxCamera.Size = new System.Drawing.Size(321, 120);
this.groupboxCamera.TabIndex = 22;
this.groupboxCamera.TabStop = false;
this.groupboxCamera.Text = "Camera options";
@@ -4604,10 +4609,14 @@ private void InitializeComponent() {
//
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(192, 48);
+ this.panelMouse.Size = new System.Drawing.Size(248, 48);
this.panelMouse.TabIndex = 12;
this.panelMouse.Visible = false;
//
@@ -4633,6 +4642,49 @@ private void InitializeComponent() {
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)));
@@ -6823,6 +6875,10 @@ private void InitializeComponent() {
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;
diff --git a/source/OpenBVE/UserInterface/formMain.cs b/source/OpenBVE/UserInterface/formMain.cs
index ff4bf858fb..f1a2f33fa8 100644
--- a/source/OpenBVE/UserInterface/formMain.cs
+++ b/source/OpenBVE/UserInterface/formMain.cs
@@ -1012,6 +1012,12 @@ private void ApplyLanguage()
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"}));
diff --git a/source/OpenBveApi/Interface/Input/Control.cs b/source/OpenBveApi/Interface/Input/Control.cs
index bbd8a1d5a4..b7a8767b2e 100644
--- a/source/OpenBveApi/Interface/Input/Control.cs
+++ b/source/OpenBveApi/Interface/Input/Control.cs
@@ -60,6 +60,10 @@ public override string ToString()
break;
case ControlMethod.Mouse:
s += Element + ", " + Option;
+ if (Modifier != KeyboardModifier.None)
+ {
+ s += ", " + (int)Modifier;
+ }
break;
}
return s;
From 5ef603b25a2b60ee05091e351bac3c61ab34057b Mon Sep 17 00:00:00 2001
From: adfriz <76892624+adfriz@users.noreply.github.com>
Date: Sat, 22 Aug 2026 13:03:17 +0700
Subject: [PATCH 6/6] Feature: Sequence-sensitive modifier bindings and grab
box modifier capture
- Record the press order of Shift/Ctrl/Alt so e.g. Ctrl->Shift+A and Shift->Ctrl+A are distinct bindings
- Store an optional trailing modifier-order column in controls.cfg (0 = any order, backwards compatible)
- Track held modifier sequence at runtime and enforce it when matching keyboard and mouse bindings
- Options dialog: grab box now captures held modifiers instead of binding the modifier keys themselves
- Checkbox tick order defines the recorded press order; assignment column displays the stored order
---
source/OpenBVE/Game/Menu/Menu.cs | 3 +-
source/OpenBVE/System/Input/Controls.cs | 16 +-
source/OpenBVE/System/Input/Keyboard.cs | 23 +-
source/OpenBVE/System/MainLoop.cs | 55 ++++-
.../UserInterface/formMain.Controls.cs | 209 ++++++++++++++----
source/OpenBveApi/Interface/Input/Control.cs | 9 +-
.../Interface/Input/ModifierOrder.cs | 103 +++++++++
source/OpenBveApi/OpenBveApi.csproj | 1 +
8 files changed, 367 insertions(+), 52 deletions(-)
create mode 100644 source/OpenBveApi/Interface/Input/ModifierOrder.cs
diff --git a/source/OpenBVE/Game/Menu/Menu.cs b/source/OpenBVE/Game/Menu/Menu.cs
index 86fe5527fa..db0b77e004 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/Input/Controls.cs b/source/OpenBVE/System/Input/Controls.cs
index cf3602cd18..4a417321cd 100644
--- a/source/OpenBVE/System/Input/Controls.cs
+++ b/source/OpenBVE/System/Input/Controls.cs
@@ -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;
}
diff --git a/source/OpenBVE/System/Input/Keyboard.cs b/source/OpenBVE/System/Input/Keyboard.cs
index 5f76ce750c..5c0d562208 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
@@ -35,9 +50,10 @@ internal static void KeyDownEvent(object sender, KeyboardKeyEventArgs e)
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
@@ -48,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;
@@ -111,14 +127,17 @@ internal static void KeyUpEvent(object sender, KeyboardKeyEventArgs e)
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
diff --git a/source/OpenBVE/System/MainLoop.cs b/source/OpenBVE/System/MainLoop.cs
index 5022fc2daa..10d2197bf1 100644
--- a/source/OpenBVE/System/MainLoop.cs
+++ b/source/OpenBVE/System/MainLoop.cs
@@ -236,11 +236,16 @@ internal static void mouseWheelEvent(object sender, MouseWheelEventArgs e)
// 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)
+ if (Interface.CurrentControls[i].Method != ControlMethod.Mouse || Interface.CurrentControls[i].Element != element || Interface.CurrentControls[i].Modifier != HeldKeyboardModifiers)
{
- Interface.CurrentControls[i].AnalogState += 1.0;
- Interface.CurrentControls[i].DigitalState = DigitalControlState.Pressed;
+ 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;
@@ -323,6 +328,40 @@ private static void ApplyMouseGrab()
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()
{
@@ -691,8 +730,14 @@ private static void ProcessMouseControl(int element, bool pressed)
{
if (Interface.CurrentControls[i].Method == ControlMethod.Mouse && Interface.CurrentControls[i].Element == element)
{
- // On press require the modifiers to match; on release always release so controls cannot get stuck
- if (!pressed || Interface.CurrentControls[i].Modifier == HeldKeyboardModifiers)
+ // 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/UserInterface/formMain.Controls.cs b/source/OpenBVE/UserInterface/formMain.Controls.cs
index bc0521aad3..ee7263d700 100644
--- a/source/OpenBVE/UserInterface/formMain.Controls.cs
+++ b/source/OpenBVE/UserInterface/formMain.Controls.cs
@@ -78,6 +78,7 @@ 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) {
@@ -85,6 +86,7 @@ private void listviewControls_SelectedIndexChanged(object sender, EventArgs e) {
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;
@@ -94,6 +96,8 @@ private void listviewControls_SelectedIndexChanged(object sender, EventArgs e) {
checkboxMouseShift.Checked = false;
checkboxMouseCtrl.Checked = false;
checkboxMouseAlt.Checked = false;
+ KeyboardCheckboxOrderCount = 0;
+ MouseCheckboxOrderCount = 0;
}
panelJoystick.Enabled = radiobuttonJoystick.Checked;
panelJoystick.Visible = radiobuttonJoystick.Checked;
@@ -122,6 +126,8 @@ private void listviewControls_SelectedIndexChanged(object sender, EventArgs e) {
checkboxMouseShift.Checked = false;
checkboxMouseCtrl.Checked = false;
checkboxMouseAlt.Checked = false;
+ KeyboardCheckboxOrderCount = 0;
+ MouseCheckboxOrderCount = 0;
labelJoystickAssignmentValue.Text = "";
Tag = null;
buttonControlRemove.Enabled = false;
@@ -167,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)
{
@@ -296,10 +299,7 @@ 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;
- 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_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;
@@ -314,6 +314,27 @@ private string GetControlDetails(int Index) {
}
+ // 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++) {
@@ -435,29 +456,22 @@ private void comboboxMouseButton_SelectedIndexChanged(object sender, EventArgs e
// mouse modifiers
private void checkboxMouseShift_CheckedChanged(object sender, EventArgs e) {
- if (Tag == null & listviewControls.SelectedIndices.Count == 1) {
- int i = listviewControls.SelectedIndices[0];
- Interface.CurrentControls[i].Modifier = (checkboxMouseShift.Checked ? KeyboardModifier.Shift : KeyboardModifier.None) |
- (checkboxMouseCtrl.Checked ? KeyboardModifier.Ctrl : KeyboardModifier.None) |
- (checkboxMouseAlt.Checked ? KeyboardModifier.Alt : KeyboardModifier.None);
- UpdateControlListElement(listviewControls.Items[i], i, true);
- }
+ UpdateMouseModifiers(KeyboardModifier.Shift, checkboxMouseShift.Checked);
}
private void checkboxMouseCtrl_CheckedChanged(object sender, EventArgs e) {
- if (Tag == null & listviewControls.SelectedIndices.Count == 1) {
- int i = listviewControls.SelectedIndices[0];
- Interface.CurrentControls[i].Modifier = (checkboxMouseShift.Checked ? KeyboardModifier.Shift : KeyboardModifier.None) |
- (checkboxMouseCtrl.Checked ? KeyboardModifier.Ctrl : KeyboardModifier.None) |
- (checkboxMouseAlt.Checked ? KeyboardModifier.Alt : KeyboardModifier.None);
- UpdateControlListElement(listviewControls.Items[i], i, true);
- }
+ 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);
}
}
@@ -480,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);
}
}
@@ -619,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) {
@@ -627,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;
@@ -654,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/OpenBveApi/Interface/Input/Control.cs b/source/OpenBveApi/Interface/Input/Control.cs
index b7a8767b2e..c980aee3e4 100644
--- a/source/OpenBveApi/Interface/Input/Control.cs
+++ b/source/OpenBveApi/Interface/Input/Control.cs
@@ -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;
diff --git a/source/OpenBveApi/Interface/Input/ModifierOrder.cs b/source/OpenBveApi/Interface/Input/ModifierOrder.cs
new file mode 100644
index 0000000000..1e2f42eb33
--- /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/OpenBveApi.csproj b/source/OpenBveApi/OpenBveApi.csproj
index db099f1922..6a79ce7141 100644
--- a/source/OpenBveApi/OpenBveApi.csproj
+++ b/source/OpenBveApi/OpenBveApi.csproj
@@ -117,6 +117,7 @@
+