Skip to content
Closed
3 changes: 3 additions & 0 deletions src/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public partial class App : System.Windows.Application

protected override void OnStartup(StartupEventArgs e)
{
// 启用现代指针支持以修复触屏拖拽卡顿并支持多点触控
AppContext.SetSwitch("Switch.System.Windows.Input.Stylus.EnablePointerSupport", true);

const string appName = @"Global\BASpark_SingleInstance_Mutex";
_mutex = new Mutex(true, appName, out bool createdNew);

Expand Down
3 changes: 3 additions & 0 deletions src/ConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public static class ConfigManager
public static string FilterProfiles { get; set; } = "";
public static string ActiveProfileId { get; set; } = "";
public static bool IsTouchscreenMode { get; set; } = false;
public static bool EnableMultiTouch { get; set; } = false;
public static int ClickTriggerType { get; set; } = 0; // 0:左, 1:右, 2:左右
public static string EnabledScreenIds { get; set; } = "";

Expand Down Expand Up @@ -76,6 +77,7 @@ public static void Load()
HideInFullscreen = Convert.ToBoolean(key.GetValue("HideInFullscreen", true));
ShowEffectOnDesktop = Convert.ToBoolean(key.GetValue("ShowEffectOnDesktop", true));
IsTouchscreenMode = Convert.ToBoolean(key.GetValue("IsTouchscreenMode", false));
EnableMultiTouch = Convert.ToBoolean(key.GetValue("EnableMultiTouch", false));
ClickTriggerType = Convert.ToInt32(key.GetValue("ClickTriggerType", 0));
EnabledScreenIds = key.GetValue("EnabledScreenIds", "")?.ToString() ?? "";

Expand Down Expand Up @@ -255,6 +257,7 @@ public static void ResetAndClear()
ActiveProfileId = "";
_profiles.Clear();
IsTouchscreenMode = false;
EnableMultiTouch = false;
ClickTriggerType = 0;
EnabledScreenIds = "";
}
Expand Down
14 changes: 14 additions & 0 deletions src/ControlPanelWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,20 @@
<TextBlock Text="* 若需在任务管理器等部分高权限窗口显示特效,请勾选此项。" FontSize="11" Foreground="#9BA3AF" Margin="34,0,0,10" TextWrapping="Wrap" Opacity="0.8"/>
<CheckBox Name="CheckTouchscreenMode" Content="触摸屏模式" Style="{StaticResource ToggleSwitch}" Margin="0,0,0,2"/>
<TextBlock Text="* 开启后,即便系统隐藏了鼠标指针特效依然生效,仅适用于触摸屏或特定的全屏应用环境。" FontSize="11" Foreground="#9BA3AF" Margin="34,0,0,12" TextWrapping="Wrap" Opacity="0.8"/>
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=CheckTouchscreenMode}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<CheckBox Name="CheckMultiTouch" Content="启用多点触控支持" Style="{StaticResource ToggleSwitch}" Margin="0,0,0,2"/>
<TextBlock Text="* 开启后,支持多指同时触发独立拖尾与点击特效。" FontSize="11" Foreground="#9BA3AF" Margin="34,0,0,12" TextWrapping="Wrap" Opacity="0.8"/>
</StackPanel>
<CheckBox Name="CheckTelemetry" Content="允许发送匿名运行数据以改进软件" Style="{StaticResource ToggleSwitch}" FontSize="12"/>
</StackPanel>
</Border>
Expand Down
4 changes: 4 additions & 0 deletions src/ControlPanelWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@
CheckShowEffectOnDesktop.IsChecked = ConfigManager.ShowEffectOnDesktop;
CheckRunAsAdmin.IsChecked = ConfigManager.RunAsAdmin;
CheckTouchscreenMode.IsChecked = ConfigManager.IsTouchscreenMode;
CheckMultiTouch.IsChecked = ConfigManager.EnableMultiTouch;

int mode = ConfigManager.ClickTriggerType;
if (mode == 1) RadioRightClick.IsChecked = true;
Expand Down Expand Up @@ -765,6 +766,7 @@
bool startSilentEnabled = CheckStartSilent.IsChecked ?? false;
bool runAsAdminEnabled = CheckRunAsAdmin.IsChecked ?? false;
bool isTouchscreenEnabled = CheckTouchscreenMode?.IsChecked ?? false;
bool isMultiTouchEnabled = CheckMultiTouch?.IsChecked ?? false;

int clickType = 0;
if (RadioRightClick.IsChecked == true) clickType = 1;
Expand All @@ -776,6 +778,7 @@

ConfigManager.Save("RunAsAdmin", runAsAdminEnabled);
ConfigManager.Save("IsTouchscreenMode", isTouchscreenEnabled);
ConfigManager.Save("EnableMultiTouch", isMultiTouchEnabled);
ConfigManager.Save("IsEffectEnabled", CheckMasterSwitch.IsChecked ?? true);
ConfigManager.Save("AutoStart", autoStartEnabled);
ConfigManager.Save("EnableTelemetry", CheckTelemetry.IsChecked ?? false);
Expand Down Expand Up @@ -814,6 +817,7 @@
App.Overlay?.UpdateTrailRefreshRate(trailRefreshRate);
App.Overlay?.RefreshEnvironmentFilterState();
App.Overlay?.UpdateTouchMode(isTouchscreenEnabled);
App.Overlay?.UpdateMultiTouchMode(isMultiTouchEnabled);
if (!enabledScreenIds.SetEquals(selectedIds))
{
App.Overlay?.RefreshScreenSelection();
Expand Down Expand Up @@ -856,7 +860,7 @@
bool autoStart = CheckAutoStart.IsChecked == true;
bool runAsAdmin = CheckRunAsAdmin.IsChecked == true;

string? exePath = Assembly.GetExecutingAssembly().Location;

Check warning on line 863 in src/ControlPanelWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

'System.Reflection.Assembly.Location' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'.
if (string.IsNullOrEmpty(exePath))
{
exePath = Process.GetCurrentProcess().MainModule?.FileName;
Expand Down
36 changes: 33 additions & 3 deletions src/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Drawing;
using System.Globalization;
using System.Linq;
Expand Down Expand Up @@ -72,6 +72,7 @@ private struct RECT
private IntPtr _hwnd;
private string? _lastReportedInputMode;
private bool? _lastReportedAlwaysTrail;
private bool? _lastReportedMultiTouch;
private const string InputModeMouse = "mouse";
private const string InputModeTouch = "touch";

Expand Down Expand Up @@ -264,15 +265,23 @@ private static bool IsCursorVisible()
private string BuildInputContextScript(string inputMode)
{
bool alwaysTrailEnabled = ConfigManager.EnableAlwaysTrailEffect;
if (_lastReportedInputMode == inputMode && _lastReportedAlwaysTrail == alwaysTrailEnabled)
bool multiTouchEnabled = ConfigManager.EnableMultiTouch;

if (_lastReportedInputMode == inputMode &&
_lastReportedAlwaysTrail == alwaysTrailEnabled &&
_lastReportedMultiTouch == multiTouchEnabled)
{
return string.Empty;
}

_lastReportedInputMode = inputMode;
_lastReportedAlwaysTrail = alwaysTrailEnabled;
_lastReportedMultiTouch = multiTouchEnabled;

string alwaysTrailLiteral = alwaysTrailEnabled ? "true" : "false";
return $"if(window.setInputContext) window.setInputContext('{inputMode}', {alwaysTrailLiteral});";
string multiTouchLiteral = multiTouchEnabled ? "true" : "false";

return $"if(window.setInputContext) window.setInputContext('{inputMode}', {alwaysTrailLiteral}, {multiTouchLiteral});";
}

private void SyncInputContext(string inputMode)
Expand Down Expand Up @@ -323,6 +332,27 @@ public void EmitUp(bool touchLike)
ExecuteWithInputContext(inputMode, "if(window.externalUp) window.externalUp();");
}

public void EmitTouchDown(uint pointerId, int x, int y)
{
if (!TryConvertScreenToOverlayPoint(x, y, out System.Windows.Point clientPoint)) return;
string px = FormatCoordinate(clientPoint.X);
string py = FormatCoordinate(clientPoint.Y);
ExecuteWithInputContext(InputModeTouch, $"if(window.externalTouchDown) window.externalTouchDown({pointerId}, {px}, {py});");
}

public void EmitTouchMove(uint pointerId, int x, int y, bool touchLike)
{
if (!TryConvertScreenToOverlayPoint(x, y, out System.Windows.Point clientPoint)) return;
string px = FormatCoordinate(clientPoint.X);
string py = FormatCoordinate(clientPoint.Y);
ExecuteWithInputContext(InputModeTouch, $"if(window.externalTouchMove) window.externalTouchMove({pointerId}, {px}, {py});");
}

public void EmitTouchUp(uint pointerId, bool touchLike)
{
ExecuteWithInputContext(InputModeTouch, $"if(window.externalTouchUp) window.externalTouchUp({pointerId});");
}

private void UpdateOverlayBounds()
{
Rectangle bounds = GetScreenBounds();
Expand Down
131 changes: 128 additions & 3 deletions src/OverlayManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,12 @@ public sealed class OverlayManager : IDisposable
private static extern IntPtr WindowFromPoint(POINT Point);
[DllImport("user32.dll")]
private static extern IntPtr GetAncestor(IntPtr hwnd, uint gaFlags);
[DllImport("user32.dll")]
private static extern IntPtr GetMessageExtraInfo();

[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int x; public int y; }

[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
Expand All @@ -74,24 +77,100 @@ private struct MONITORINFO

private readonly Dictionary<string, MainWindow> _overlays = new(StringComparer.OrdinalIgnoreCase);
private IKeyboardMouseEvents? _globalHook;
private TouchInputCapture? _touchCapture;
private MainWindow? _activePointerOverlay;
private long _lastMoveTicks;
private long _lastClickTicks;
private long _moveIntervalTicks = 250000;
private long _moveIntervalTicks = 250000; // 默认 40Hz
private long _touchMoveIntervalTicks = 80000; // 触摸专用,约 120Hz,降低拖尾延迟
private bool _isPrimaryPointerDown;
private bool _isTouchLikeInput;
private bool _isSuppressedByEnvironment;
private long _suppressionCacheValidUntilTicks;
private IntPtr _lastForegroundWindow = IntPtr.Zero;
private bool _disposed;

private readonly Dictionary<uint, TouchPointState> _activePointers = new();

private class TouchPointState
{
public uint PointerId;
public int LastX, LastY;
public bool IsDown;
public long LastMoveTicks;
public MainWindow? TargetOverlay;
}

public void Start()
{
RebuildWindows(forceRebuild: true);
SetupGlobalHooks();
SetupTouchCapture();
SystemEvents.DisplaySettingsChanged += HandleDisplaySettingsChanged;
}

private void SetupTouchCapture()
{
if (ConfigManager.EnableMultiTouch)
{
_touchCapture = new TouchInputCapture();
_touchCapture.TouchDown += (id, x, y) => EmitTouchDown(id, x, y);
_touchCapture.TouchMove += (id, x, y) => EmitTouchMove(id, x, y);
_touchCapture.TouchUp += (id) => EmitTouchUp(id);
_touchCapture.Start();
}
}

private void HandleRawInput(IntPtr hRawInput)
{
// 已废弃,多点触控改由 TouchInputCapture 处理
}

public void EmitTouchDown(uint pointerId, int x, int y)
{
if (!CanRenderEffects()) return;

MainWindow? target = ResolveTargetOverlay(x, y);
if (target == null) return;

_activePointers[pointerId] = new TouchPointState
{
PointerId = pointerId,
LastX = x,
LastY = y,
IsDown = true,
TargetOverlay = target
};

ConfigManager.TotalClicks++;
target.EmitTouchDown(pointerId, x, y);
}

public void EmitTouchMove(uint pointerId, int x, int y)
{
if (!_activePointers.TryGetValue(pointerId, out var state) || !state.IsDown) return;
if (state.TargetOverlay == null) return;

// 每个触控点独立节流,避免多指互相阻塞
long currentTicks = DateTime.Now.Ticks;
if (currentTicks - state.LastMoveTicks < _touchMoveIntervalTicks) return;
state.LastMoveTicks = currentTicks;

state.LastX = x;
state.LastY = y;
state.TargetOverlay.EmitTouchMove(pointerId, x, y, true);
}

public void EmitTouchUp(uint pointerId)
{
if (_activePointers.TryGetValue(pointerId, out var state))
{
state.IsDown = false;
state.TargetOverlay?.EmitTouchUp(pointerId, true);
_activePointers.Remove(pointerId);
}
}

public void UpdateColor(string color) => ForEachOverlay(w => w.UpdateColor(color));
public void UpdateEffectSettings(double scale, double opacity, double speed) => ForEachOverlay(w => w.UpdateEffectSettings(scale, opacity, speed));
public void UpdateTrailRefreshRate(int hz)
Expand All @@ -101,6 +180,23 @@ public void UpdateTrailRefreshRate(int hz)
ForEachOverlay(w => w.UpdateTrailRefreshRate(hz));
}
public void UpdateTouchMode(bool enabled) => ForEachOverlay(w => w.UpdateTouchMode(enabled));
public void UpdateMultiTouchMode(bool enabled)
{
if (enabled && _touchCapture == null)
{
_touchCapture = new TouchInputCapture();
_touchCapture.TouchDown += (id, x, y) => EmitTouchDown(id, x, y);
_touchCapture.TouchMove += (id, x, y) => EmitTouchMove(id, x, y);
_touchCapture.TouchUp += (id) => EmitTouchUp(id);
_touchCapture.Start();
}
else if (!enabled && _touchCapture != null)
{
_touchCapture.Dispose();
_touchCapture = null;
_activePointers.Clear();
}
}
public bool IsEffectSuppressedByEnvironment() => ShouldSuppressEffects();
public void RefreshEnvironmentFilterState()
{
Expand All @@ -122,10 +218,24 @@ private void SetupGlobalHooks()
_globalHook.MouseUpExt += OnMouseUpExt;
}

public void EmitDown(int x, int y)
{
if (!CanRenderEffects()) return;

MainWindow? target = ResolveTargetOverlay(x, y);
if (target == null) return;

ConfigManager.TotalClicks++;
target.EmitDown(x, y);
}

private void OnMouseDownExt(object? sender, MouseEventExtArgs e)
{
if (!CanRenderEffects()) return;

// 如果当前正在处理原生多点触控,则跳过合成鼠标事件的按下
if (_activePointers.Count > 0 && !CursorIsVisible()) return;

bool isLeft = e.Button == MouseButtons.Left;
bool isRight = e.Button == MouseButtons.Right;
bool shouldTrigger = ConfigManager.ClickTriggerType switch
Expand Down Expand Up @@ -161,11 +271,16 @@ private void OnMouseMoveExt(object? sender, MouseEventExtArgs e)
{
if (!CanRenderEffects()) return;

// 如果已有原生触控点在处理,则跳过合成鼠标事件以消除多余延迟
if (_activePointers.Count > 0 && !CursorIsVisible()) return;

bool cursorVisible = CursorIsVisible();
if (!cursorVisible && !_isPrimaryPointerDown) return;

// 触摸/触控笔事件使用更低的节流阈值,修复拖拽卡顿
long currentTicks = DateTime.Now.Ticks;
if (currentTicks - _lastMoveTicks < _moveIntervalTicks) return;
long interval = (!cursorVisible || _isTouchLikeInput) ? _touchMoveIntervalTicks : _moveIntervalTicks;
if (currentTicks - _lastMoveTicks < interval) return;
_lastMoveTicks = currentTicks;

if (!TryGetPhysicalCursorPosition(out int cursorX, out int cursorY))
Expand All @@ -181,6 +296,8 @@ private void OnMouseMoveExt(object? sender, MouseEventExtArgs e)
private void OnMouseUpExt(object? sender, MouseEventExtArgs e)
{
_ = e;
// 如果已有原生触控点在处理,跳过
if (_activePointers.Count > 0 && !CursorIsVisible()) return;
if (!_isPrimaryPointerDown)
{
_isTouchLikeInput = false;
Expand All @@ -207,7 +324,7 @@ private bool CanRenderEffects()
return false;
}

if (!ConfigManager.IsTouchscreenMode && !CursorIsVisible())
if (!ConfigManager.IsTouchscreenMode && !CursorIsVisible() && _activePointers.Count == 0)
{
ReleasePointerState();
return false;
Expand Down Expand Up @@ -543,6 +660,14 @@ public void Dispose()
_globalHook = null;
}

if (_touchCapture != null)
{
_touchCapture.Dispose();
_touchCapture = null;
}

_activePointers.Clear();

CloseWindows();
}
}
Expand Down
Loading
Loading