Skip to content

Commit 9e89fa1

Browse files
authored
Merge pull request #2 from Soapwood/design/HoverHighlighting
[Haptics] VXMusic Haptic Feedback
2 parents d608426 + 22566c9 commit 9e89fa1

18 files changed

+907
-151
lines changed

VXMusicDesktop/App.xaml.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ public static void ConfigureServices()
151151
services.AddSingleton<VrChatOscNotificationClient>();
152152
services.AddSingleton<OvrToolkitNotificationClient>();
153153
services.AddSingleton<SteamVROverlayAppsInterface>();
154+
services.AddSingleton<IVRHapticFeedbackService, VRHapticFeedbackService>();
154155

155156
services.AddSingleton<VXMusicUpdateHandler>();
156157

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
using System;
2+
using System.Windows;
3+
using System.Windows.Controls;
4+
using System.Windows.Input;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Logging;
7+
using VXMusicDesktop.Core;
8+
9+
namespace VXMusicDesktop.Behaviors
10+
{
11+
public static class VRHapticBehavior
12+
{
13+
#region EnableHapticFeedback Attached Property
14+
15+
public static readonly DependencyProperty EnableHapticFeedbackProperty =
16+
DependencyProperty.RegisterAttached(
17+
"EnableHapticFeedback",
18+
typeof(bool),
19+
typeof(VRHapticBehavior),
20+
new PropertyMetadata(false, OnEnableHapticFeedbackChanged));
21+
22+
public static bool GetEnableHapticFeedback(DependencyObject obj)
23+
{
24+
return (bool)obj.GetValue(EnableHapticFeedbackProperty);
25+
}
26+
27+
public static void SetEnableHapticFeedback(DependencyObject obj, bool value)
28+
{
29+
obj.SetValue(EnableHapticFeedbackProperty, value);
30+
}
31+
32+
#endregion
33+
34+
#region HapticDuration Attached Property
35+
36+
public static readonly DependencyProperty HapticDurationProperty =
37+
DependencyProperty.RegisterAttached(
38+
"HapticDuration",
39+
typeof(ushort),
40+
typeof(VRHapticBehavior),
41+
new PropertyMetadata((ushort)2000));
42+
43+
public static ushort GetHapticDuration(DependencyObject obj)
44+
{
45+
return (ushort)obj.GetValue(HapticDurationProperty);
46+
}
47+
48+
public static void SetHapticDuration(DependencyObject obj, ushort value)
49+
{
50+
obj.SetValue(HapticDurationProperty, value);
51+
}
52+
53+
#endregion
54+
55+
private static void OnEnableHapticFeedbackChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
56+
{
57+
if (d is FrameworkElement element)
58+
{
59+
bool enableHaptic = (bool)e.NewValue;
60+
61+
if (enableHaptic)
62+
{
63+
element.MouseEnter += OnElementMouseEnter;
64+
element.Unloaded += OnElementUnloaded;
65+
}
66+
else
67+
{
68+
element.MouseEnter -= OnElementMouseEnter;
69+
element.Unloaded -= OnElementUnloaded;
70+
}
71+
}
72+
}
73+
74+
private static void OnElementMouseEnter(object sender, MouseEventArgs e)
75+
{
76+
if (sender is FrameworkElement element)
77+
{
78+
// Only trigger haptic feedback for elements that can be interacted with
79+
if (IsInteractiveElement(element))
80+
{
81+
TriggerHapticFeedback(element);
82+
}
83+
}
84+
}
85+
86+
private static void OnElementUnloaded(object sender, RoutedEventArgs e)
87+
{
88+
if (sender is FrameworkElement element)
89+
{
90+
element.MouseEnter -= OnElementMouseEnter;
91+
element.Unloaded -= OnElementUnloaded;
92+
}
93+
}
94+
95+
private static bool IsInteractiveElement(FrameworkElement element)
96+
{
97+
// Check if element is enabled and visible
98+
if (!element.IsEnabled || element.Visibility != Visibility.Visible)
99+
return false;
100+
101+
// Check for common interactive elements
102+
switch (element)
103+
{
104+
case Button button:
105+
return button.IsEnabled && button.Command?.CanExecute(button.CommandParameter) != false;
106+
case ComboBox comboBox:
107+
return comboBox.IsEnabled;
108+
case RadioButton radioButton:
109+
return radioButton.IsEnabled;
110+
case CheckBox checkBox:
111+
return checkBox.IsEnabled;
112+
case TextBox textBox:
113+
return textBox.IsEnabled && !textBox.IsReadOnly;
114+
case ListBox listBox:
115+
return listBox.IsEnabled;
116+
case MenuItem menuItem:
117+
return menuItem.IsEnabled;
118+
case Slider slider:
119+
return slider.IsEnabled;
120+
default:
121+
// For other elements, check if they have a cursor indicating interactivity
122+
return element.Cursor == Cursors.Hand || element.Cursor == Cursors.Arrow;
123+
}
124+
}
125+
126+
private static void TriggerHapticFeedback(FrameworkElement element)
127+
{
128+
try
129+
{
130+
// Get the haptic feedback service from the DI container
131+
var hapticService = App.ServiceProvider?.GetService<IVRHapticFeedbackService>();
132+
var loggerFactory = App.ServiceProvider?.GetService<ILoggerFactory>();
133+
var logger = loggerFactory?.CreateLogger("VRHapticBehavior");
134+
135+
if (hapticService != null)
136+
{
137+
ushort duration = GetHapticDuration(element);
138+
logger?.LogDebug($"Triggering haptic feedback for {element.GetType().Name} with duration {duration}");
139+
140+
// Log VR system status
141+
logger?.LogDebug($"VR System Available: {hapticService.IsVRSystemAvailable()}");
142+
logger?.LogDebug($"Has Connected Controllers: {hapticService.HasConnectedControllers()}");
143+
144+
hapticService.TriggerHapticFeedback(duration);
145+
}
146+
else
147+
{
148+
logger?.LogWarning("VRHapticFeedbackService not available from DI container");
149+
}
150+
}
151+
catch (System.Exception ex)
152+
{
153+
// Log the exception if we have access to a logger
154+
try
155+
{
156+
// Try to get a logger factory and create a logger for haptic feedback
157+
var loggerFactory = App.ServiceProvider?.GetService<ILoggerFactory>();
158+
var logger = loggerFactory?.CreateLogger("VRHapticBehavior");
159+
logger?.LogWarning(ex, "Failed to trigger haptic feedback from VRHapticBehavior");
160+
}
161+
catch
162+
{
163+
// If we can't even log, just silently fail to avoid breaking the UI
164+
}
165+
}
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)