Skip to content

Commit ef3551c

Browse files
committed
Fix wizard not getting input events for mapping. Major update to overlays system. Check for updates every hour.
1 parent bfe7c7b commit ef3551c

48 files changed

Lines changed: 2142 additions & 265 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

MarvinsAIRARefactored/App.xaml.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,15 @@ void RunStartupStep( string statusKey, Action initializeAction )
391391
MainWindow.Show();
392392
}
393393

394+
// Start the worker thread and the input polling timer before showing the modal
395+
// first-run wizard. The wizard's button-mapping recorder relies on DirectInput.OnInput,
396+
// which is only fired from PollDevices (driven by OnTimer). ShowDialog() runs a nested
397+
// dispatcher loop, so the timer and worker keep ticking while the wizard is open.
398+
399+
_workerThread.Start();
400+
401+
_timer.Start();
402+
394403
#if !ADMINBOXX
395404

396405
if ( showWindow && !DataContext.DataContext.Instance.Settings.AppWizardHasRun )
@@ -429,10 +438,6 @@ void RunStartupStep( string statusKey, Action initializeAction )
429438

430439
#endif
431440

432-
_workerThread.Start();
433-
434-
_timer.Start();
435-
436441
Simulator.Start();
437442

438443
GC.Collect();
@@ -2955,6 +2960,7 @@ private static void WorkerThread()
29552960
app.Commentary.Tick( app );
29562961
app.Wind.Tick( app );
29572962
app.SeatBeltTensioner.Tick( app );
2963+
app.CloudService.Tick( app );
29582964

29592965
app.GripOMeterWindow?.Tick( app );
29602966
app.GapMonitorWindow?.Tick( app );
@@ -3142,7 +3148,7 @@ public void UpdateGripOMeterWindowVisibility()
31423148
{
31433149
var settings = DataContext.DataContext.Instance.Settings;
31443150

3145-
if ( OverlaysDraggable || ( settings.OverlaysShowGripOMeterWindow && Simulator.IsConnected && Simulator.IsOnTrack ) )
3151+
if ( settings.OverlaysShowGripOMeterWindow && ( OverlaysDraggable || ( Simulator.IsConnected && ( Simulator.IsOnTrack || settings.OverlaysShowWhenOffTrack ) && ( !Simulator.IsReplayPlaying || settings.OverlaysShowInReplayMode ) ) ) )
31463152
{
31473153
EnsureGripOMeterWindowExists();
31483154

@@ -3163,7 +3169,7 @@ public void UpdateGapMonitorWindowVisibility()
31633169
{
31643170
var settings = DataContext.DataContext.Instance.Settings;
31653171

3166-
if ( OverlaysDraggable || ( settings.OverlaysShowGapMonitorWindow && Simulator.IsConnected && Simulator.IsOnTrack ) )
3172+
if ( settings.OverlaysShowGapMonitorWindow && ( OverlaysDraggable || ( Simulator.IsConnected && ( Simulator.IsOnTrack || settings.OverlaysShowWhenOffTrack ) && ( !Simulator.IsReplayPlaying || settings.OverlaysShowInReplayMode ) ) ) )
31673173
{
31683174
EnsureGapMonitorWindowExists();
31693175

@@ -3184,7 +3190,7 @@ public void UpdateDeltaMonitorWindowVisibility()
31843190
{
31853191
var settings = DataContext.DataContext.Instance.Settings;
31863192

3187-
if ( OverlaysDraggable || ( settings.OverlaysShowDeltaMonitorWindow && Simulator.IsConnected && Simulator.IsOnTrack ) )
3193+
if ( settings.OverlaysShowDeltaMonitorWindow && ( OverlaysDraggable || ( Simulator.IsConnected && ( Simulator.IsOnTrack || settings.OverlaysShowWhenOffTrack ) && ( !Simulator.IsReplayPlaying || settings.OverlaysShowInReplayMode ) ) ) )
31883194
{
31893195
EnsureDeltaMonitorWindowExists();
31903196

@@ -3205,7 +3211,7 @@ public void UpdateSpeechToTextWindowVisibility()
32053211
{
32063212
var settings = DataContext.DataContext.Instance.Settings;
32073213

3208-
if ( OverlaysDraggable )
3214+
if ( OverlaysDraggable && settings.SpeechToTextEnabled )
32093215
{
32103216
EnsureSpeechToTextWindowExists();
32113217

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using System.Windows;
2+
using System.Windows.Input;
3+
4+
using WinFormsCursor = System.Windows.Forms.Cursor;
5+
6+
namespace MarvinsAIRARefactored.Classes;
7+
8+
// Drives an overlay window's position by tracking mouse movement after the user clicks the move icon.
9+
// While moving, the cursor is hidden and locked to a fixed anchor so the movement range is unbounded.
10+
// Moving the mouse drags the window by the same amount.
11+
public sealed class OverlayWindowMover
12+
{
13+
private readonly Window _window;
14+
private readonly FrameworkElement _dragHandle;
15+
16+
private bool _isMoving = false;
17+
private System.Drawing.Point _anchor;
18+
19+
public OverlayWindowMover( Window window, FrameworkElement dragHandle )
20+
{
21+
_window = window;
22+
_dragHandle = dragHandle;
23+
}
24+
25+
public bool IsMoving => _isMoving;
26+
27+
public void Start()
28+
{
29+
if ( _isMoving )
30+
{
31+
return;
32+
}
33+
34+
_isMoving = true;
35+
36+
_anchor = WinFormsCursor.Position;
37+
38+
Mouse.OverrideCursor = System.Windows.Input.Cursors.None;
39+
40+
_window.LostMouseCapture += Window_LostMouseCapture;
41+
42+
_window.CaptureMouse();
43+
}
44+
45+
public void Update()
46+
{
47+
if ( !_isMoving )
48+
{
49+
return;
50+
}
51+
52+
var position = WinFormsCursor.Position;
53+
54+
var deltaX = position.X - _anchor.X;
55+
var deltaY = position.Y - _anchor.Y;
56+
57+
if ( ( deltaX == 0 ) && ( deltaY == 0 ) )
58+
{
59+
return;
60+
}
61+
62+
_window.Left += deltaX;
63+
_window.Top += deltaY;
64+
65+
// recenter the hidden cursor so the user can keep moving in the same direction indefinitely
66+
WinFormsCursor.Position = _anchor;
67+
}
68+
69+
public void Stop()
70+
{
71+
if ( !_isMoving )
72+
{
73+
return;
74+
}
75+
76+
_isMoving = false;
77+
78+
// place the cursor at the center of the drag handle's new on-screen location so it reappears under the button the user was holding,
79+
// instead of back at the anchor point where the drag started
80+
var center = _dragHandle.PointToScreen( new System.Windows.Point( _dragHandle.ActualWidth / 2, _dragHandle.ActualHeight / 2 ) );
81+
82+
WinFormsCursor.Position = new System.Drawing.Point( (int) center.X, (int) center.Y );
83+
84+
_window.LostMouseCapture -= Window_LostMouseCapture;
85+
86+
_window.ReleaseMouseCapture();
87+
88+
Mouse.OverrideCursor = null;
89+
}
90+
91+
private void Window_LostMouseCapture( object sender, System.Windows.Input.MouseEventArgs e )
92+
{
93+
Stop();
94+
}
95+
}

MarvinsAIRARefactored/Classes/OverlayWindowScaler.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ public sealed class OverlayWindowScaler
1414
private const float Sensitivity = 0.0015f;
1515

1616
private readonly Window _window;
17+
private readonly FrameworkElement _scaleHandle;
1718
private readonly Func<float> _getScale;
1819
private readonly Action<float> _setScale;
1920

2021
private bool _isScaling = false;
2122
private System.Drawing.Point _anchor;
2223

23-
public OverlayWindowScaler( Window window, Func<float> getScale, Action<float> setScale )
24+
public OverlayWindowScaler( Window window, FrameworkElement scaleHandle, Func<float> getScale, Action<float> setScale )
2425
{
2526
_window = window;
27+
_scaleHandle = scaleHandle;
2628
_getScale = getScale;
2729
_setScale = setScale;
2830
}
@@ -80,6 +82,12 @@ public void Stop()
8082

8183
_isScaling = false;
8284

85+
// place the cursor at the center of the scale handle's new on-screen location so it reappears under the button the user was holding,
86+
// instead of back at the anchor point where scaling started
87+
var center = _scaleHandle.PointToScreen( new System.Windows.Point( _scaleHandle.ActualWidth / 2, _scaleHandle.ActualHeight / 2 ) );
88+
89+
WinFormsCursor.Position = new System.Drawing.Point( (int) center.X, (int) center.Y );
90+
8391
_window.LostMouseCapture -= Window_LostMouseCapture;
8492

8593
_window.ReleaseMouseCapture();

MarvinsAIRARefactored/Components/CloudService.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,21 @@ public class CloudService
1717
public bool CheckingForUpdate { get; private set; } = false;
1818
public bool DownloadingUpdate { get; private set; } = false;
1919

20+
private static readonly TimeSpan UpdateCheckInterval = TimeSpan.FromHours( 1 );
21+
22+
private DateTime _nextUpdateCheckUtc = DateTime.MaxValue;
23+
2024
public void Initialize()
2125
{
2226
var app = App.Instance!;
2327

2428
app.Logger.WriteLine( "[CloudService] Initialize >>>" );
2529

30+
// Schedule the first recurring update check one interval from now. The initial
31+
// (startup) check is fired separately from App_Startup; this keeps users who never
32+
// restart the app current by re-checking periodically while iRacing is not running.
33+
_nextUpdateCheckUtc = DateTime.UtcNow + UpdateCheckInterval;
34+
2635
var networkInterfaceList = NetworkInterface.GetAllNetworkInterfaces();
2736

2837
var networkInterface = networkInterfaceList.FirstOrDefault();
@@ -40,6 +49,34 @@ public void Initialize()
4049
app.Logger.WriteLine( "[CloudService] <<< Initialize" );
4150
}
4251

52+
public void Tick( App app )
53+
{
54+
if ( !DataContext.DataContext.Instance.Settings.AppCheckForUpdates )
55+
{
56+
return;
57+
}
58+
59+
// Never check for updates while the iRacing simulator is running.
60+
if ( app.Simulator.IsConnected )
61+
{
62+
return;
63+
}
64+
65+
if ( CheckingForUpdate || DownloadingUpdate )
66+
{
67+
return;
68+
}
69+
70+
if ( DateTime.UtcNow < _nextUpdateCheckUtc )
71+
{
72+
return;
73+
}
74+
75+
_nextUpdateCheckUtc = DateTime.UtcNow + UpdateCheckInterval;
76+
77+
_ = CheckForUpdates( false );
78+
}
79+
4380
class GetCurrentVersionResponse
4481
{
4582
public string currentVersion = string.Empty;

MarvinsAIRARefactored/Components/Simulator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ private void OnTelemetryData()
10971097

10981098
// update visibility of overlays
10991099

1100-
if ( IsOnTrack != WasOnTrack )
1100+
if ( ( IsOnTrack != WasOnTrack ) || ( IsReplayPlaying != _isReplayPlayingLastFrame ) )
11011101
{
11021102
app.UpdateGripOMeterWindowVisibility();
11031103
app.UpdateGapMonitorWindowVisibility();

MarvinsAIRARefactored/Controls/MairaMappableButton.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11

2+
using System.Runtime.CompilerServices;
23
using System.Windows;
34
using System.Windows.Input;
45

@@ -10,12 +11,28 @@ namespace MarvinsAIRARefactored.Controls;
1011

1112
public class MairaMappableButton : MairaButton
1213
{
14+
private static readonly ConditionalWeakTable<MairaMappableButton, object?> _instances = new();
15+
1316
public MairaMappableButton()
1417
{
1518
Loaded += MairaMappableButton_Loaded;
1619

1720
TextBlock.PreviewMouseRightButtonDown += MappableMairaButton_Label_PreviewMouseRightButtonDown;
1821
Button.PreviewMouseRightButtonDown += MappableMairaButton_Button_PreviewMouseRightButtonDown;
22+
23+
_instances.Add( this, null );
24+
}
25+
26+
// Re-evaluates the mapped (orange border) state for every mappable button in the app. The
27+
// per-button IsMapped state is only computed on Loaded and after the button's own right-click
28+
// mapping flow, so when mappings are changed elsewhere (e.g. the first-run wizard mutating the
29+
// same ButtonMappings objects) the already-loaded buttons would otherwise stay stale.
30+
public static void RefreshAll()
31+
{
32+
foreach ( var instance in _instances )
33+
{
34+
instance.Key.IsMapped = instance.Key.HasAnyMappedButton();
35+
}
1936
}
2037

2138
#region User Control Events
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Globalization;
2+
using System.Windows.Data;
3+
4+
using Color = System.Windows.Media.Color;
5+
using Colors = System.Windows.Media.Colors;
6+
using ColorConverter = System.Windows.Media.ColorConverter;
7+
using SolidColorBrush = System.Windows.Media.SolidColorBrush;
8+
9+
namespace MarvinsAIRARefactored.Converters;
10+
11+
// Combines a background color (hex string like "#RRGGBB") and an opacity (0..1 float) into a SolidColorBrush.
12+
// The opacity is applied as the brush alpha channel only, so overlay text/borders painted with other brushes stay fully opaque.
13+
public class ColorAndOpacityToBrushConverter : IMultiValueConverter
14+
{
15+
public object Convert( object[] values, Type targetType, object parameter, CultureInfo culture )
16+
{
17+
var color = Colors.Black;
18+
19+
if ( ( values.Length > 0 ) && ( values[ 0 ] is string hexColor ) && !string.IsNullOrWhiteSpace( hexColor ) )
20+
{
21+
try
22+
{
23+
color = (Color) ColorConverter.ConvertFromString( hexColor );
24+
}
25+
catch
26+
{
27+
color = Colors.Black;
28+
}
29+
}
30+
31+
var opacity = 1f;
32+
33+
if ( ( values.Length > 1 ) && ( values[ 1 ] is float opacityValue ) )
34+
{
35+
opacity = Math.Clamp( opacityValue, 0f, 1f );
36+
}
37+
38+
color.A = (byte) Math.Round( opacity * 255f );
39+
40+
var brush = new SolidColorBrush( color );
41+
42+
brush.Freeze();
43+
44+
return brush;
45+
}
46+
47+
public object[] ConvertBack( object value, Type[] targetTypes, object parameter, CultureInfo culture )
48+
{
49+
throw new NotSupportedException();
50+
}
51+
}

0 commit comments

Comments
 (0)