commit d80276d86709b0fcd96269e41f4412eb77daa44f Author: RL-Xiang Date: Wed Sep 2 20:08:33 2026 +0800 FancyInput diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af9ff79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Visual Studio user and workspace files +.vs/ +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio Code workspace files +.vscode/ + +# .NET build outputs +[Bb]in/ +[Oo]bj/ + +# Test and coverage output +TestResults/ +coverage/ +*.coverage +*.coveragexml + +# NuGet and publish output +packages/ +*.nupkg +*.snupkg +publish/ + +# Generated logs and temporary files +*.log +*.tmp +*.temp + +# OS metadata +.DS_Store +Thumbs.db + +# folders +FancyInput/Docs +FancyInput/Properties +FancyInput/Resources/Licenses \ No newline at end of file diff --git a/FancyInput.sln b/FancyInput.sln new file mode 100644 index 0000000..3173d18 --- /dev/null +++ b/FancyInput.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.36129.13 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyInput", "FancyInput\FancyInput.csproj", "{CE7F092D-2694-47BC-A610-BC915DDF7970}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CE7F092D-2694-47BC-A610-BC915DDF7970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE7F092D-2694-47BC-A610-BC915DDF7970}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE7F092D-2694-47BC-A610-BC915DDF7970}.Debug|x64.ActiveCfg = Debug|x64 + {CE7F092D-2694-47BC-A610-BC915DDF7970}.Debug|x64.Build.0 = Debug|x64 + {CE7F092D-2694-47BC-A610-BC915DDF7970}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE7F092D-2694-47BC-A610-BC915DDF7970}.Release|Any CPU.Build.0 = Release|Any CPU + {CE7F092D-2694-47BC-A610-BC915DDF7970}.Release|x64.ActiveCfg = Release|x64 + {CE7F092D-2694-47BC-A610-BC915DDF7970}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/FancyInput/App.xaml b/FancyInput/App.xaml new file mode 100644 index 0000000..bf757af --- /dev/null +++ b/FancyInput/App.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/FancyInput/App.xaml.cs b/FancyInput/App.xaml.cs new file mode 100644 index 0000000..da7279a --- /dev/null +++ b/FancyInput/App.xaml.cs @@ -0,0 +1,217 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using System.Windows; +using System.Security.Principal; +using System.Security.AccessControl; +using FancyInput.Models; +using System.Windows.Interop; + + + +namespace FancyInput +{ + public static class WindowHelper + { + public static void TryAddRawInputHook(Window window) + { + try + { + var hwndSource = HwndSource.FromHwnd(new WindowInteropHelper(window).Handle); + hwndSource.AddHook(RawInputParser.WndDragProc); + } + catch { } + } + + public static void TryRemoveRawInputHook(Window window) + { + try + { + var hwndSource = HwndSource.FromHwnd(new WindowInteropHelper(window).Handle); + hwndSource.RemoveHook(RawInputParser.WndDragProc); + } + catch { } + } + } + + public partial class App : Application + { + private static Mutex? _mutex; + private const string MutexName = "FancyInput_SingleInstance_Mutex"; + private const string PipeName = "FancyInputPipe"; + + private volatile bool _pipeServerRunning = true; + private Thread? _pipeThread; + + protected override void OnStartup(StartupEventArgs e) + { + if (e.Args.Length > 0 && e.Args[0] == "--restart-as-admin") + { + if (Application.Current.MainWindow is FancyInputMainWindow mainWindow) + { + mainWindow.AllowExit(); + } + try + { + using var pipe = new NamedPipeClientStream(".", PipeName, PipeDirection.Out); + pipe.Connect(1000); + using var writer = new StreamWriter(pipe); + string allArgs = string.Join("|", e.Args); + writer.WriteLine(allArgs); + writer.Flush(); + } + catch + { + // 主进程未启动或管道不可用,忽略 + } + Application.Current.Shutdown(); + return; + } + + bool createdNew; + _mutex = new Mutex(true, MutexName, out createdNew); + if (!createdNew) + { + if (Application.Current.MainWindow is FancyInputMainWindow mainWindow) + mainWindow.AllowExit(); + try + { + if (e.Args.Length > 0) + { + using var pipe = new NamedPipeClientStream(".", PipeName, PipeDirection.Out); + pipe.Connect(1000); + using var writer = new StreamWriter(pipe); + string allArgs = string.Join("|", e.Args); + writer.WriteLine(allArgs); + writer.Flush(); + } + else + { + using var pipe = new NamedPipeClientStream(".", PipeName, PipeDirection.Out); + pipe.Connect(1000); + using var writer = new StreamWriter(pipe); + writer.WriteLine("--show"); + writer.Flush(); + } + } + catch + { + + } + Application.Current.Shutdown(); + return; + } + // 主进程启动管道监听 + StartPipeServer(); + base.OnStartup(e); + } + + + private void StartPipeServer() + { + _pipeServerRunning = true; + _pipeThread = new Thread(() => + { + while (_pipeServerRunning) + { + var identity = WindowsIdentity.GetCurrent(); + var userSid = identity.User; + + var pipeSecurity = new PipeSecurity(); + pipeSecurity.AddAccessRule(new PipeAccessRule( + userSid!, + PipeAccessRights.FullControl, + AccessControlType.Allow)); + + using var pipe = NamedPipeServerStreamAcl.Create( + PipeName, + PipeDirection.In, + 1, + PipeTransmissionMode.Byte, + PipeOptions.None, + 0, 0, + pipeSecurity); + + try + { + pipe.WaitForConnection(); + + using var reader = new StreamReader(pipe); + string? argLine = reader.ReadLine(); + if (!string.IsNullOrEmpty(argLine)) + { + string[] args = argLine.Split('|'); + if (File.Exists(args[0])) + { + Application.Current.Dispatcher.Invoke(() => + { + if (Application.Current.MainWindow is FancyInputMainWindow mainWindow) + { + mainWindow.OpenProjectFile(args[0]); + } + }); + } + + if (args[0] == "--restart-as-admin") + { + _pipeServerRunning = false; + Application.Current.Dispatcher.Invoke(() => + { + if (Application.Current.MainWindow is FancyInputMainWindow mainWindow) + { + mainWindow.AllowExit(); + mainWindow.Close(); + } + string[] extraArgs = args.Skip(1).ToArray(); + string arguments = string.Join(" ", extraArgs.Select(a => $"\"{a}\"")); + var processInfo = new ProcessStartInfo + { + FileName = Process.GetCurrentProcess().MainModule!.FileName, + Arguments = arguments, + UseShellExecute = true, + Verb = "runas" + }; + + // 主动关闭管道 + pipe.Dispose(); + _mutex?.ReleaseMutex(); + _mutex?.Dispose(); + _mutex = null; + try + { + Process.Start(processInfo); + } + catch + { + } + }); + } + + if (args[0] == "--show") + { + Application.Current.Dispatcher.Invoke(() => + { + if (Application.Current.MainWindow is FancyInputMainWindow mainWindow) + { + mainWindow.Show(); + if (mainWindow.WindowState == WindowState.Minimized) + mainWindow.WindowState = WindowState.Normal; + mainWindow.Activate(); + } + }); + } + } + } + catch + { + // 忽略异常,继续循环 + } + } + }); + _pipeThread.IsBackground = true; + _pipeThread.Start(); + } + } +} diff --git a/FancyInput/AppMessageBox.cs b/FancyInput/AppMessageBox.cs new file mode 100644 index 0000000..be6ae09 --- /dev/null +++ b/FancyInput/AppMessageBox.cs @@ -0,0 +1,50 @@ +using System.Linq; +using System.Windows; + +namespace FancyInput +{ + public static class AppMessageBox + { + public static MessageBoxResult Show(string messageBoxText) + => Show(messageBoxText, "提示", MessageBoxButton.OK, MessageBoxImage.None); + + public static MessageBoxResult Show(string messageBoxText, string caption) + => Show(messageBoxText, caption, MessageBoxButton.OK, MessageBoxImage.None); + + public static MessageBoxResult Show(string messageBoxText, string caption, MessageBoxButton button) + => Show(messageBoxText, caption, button, MessageBoxImage.None); + + public static MessageBoxResult Show(string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon) + { + if (Application.Current == null) + { + return MessageBox.Show(messageBoxText, caption, button, icon); + } + + if (!Application.Current.Dispatcher.CheckAccess()) + { + return Application.Current.Dispatcher.Invoke(() => Show(messageBoxText, caption, button, icon)); + } + + var windows = Application.Current.Windows.OfType().Where(w => w.IsVisible).ToList(); + Window? mainWindow = Application.Current.MainWindow is { IsVisible: true } mw ? mw : null; + Window? owner = windows.FirstOrDefault(w => w.IsActive) + ?? mainWindow + ?? windows.FirstOrDefault(); + + var dialog = new AppMessageBoxWindow(messageBoxText, caption, button, icon); + if (owner != null) + { + dialog.Owner = owner; + dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner; + } + else + { + dialog.WindowStartupLocation = WindowStartupLocation.CenterScreen; + } + + dialog.ShowDialog(); + return dialog.Result; + } + } +} diff --git a/FancyInput/AppMessageBoxWindow.xaml b/FancyInput/AppMessageBoxWindow.xaml new file mode 100644 index 0000000..67438dc --- /dev/null +++ b/FancyInput/AppMessageBoxWindow.xaml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FancyInput/MainWindow.xaml.cs b/FancyInput/MainWindow.xaml.cs new file mode 100644 index 0000000..1001202 --- /dev/null +++ b/FancyInput/MainWindow.xaml.cs @@ -0,0 +1,729 @@ +using FancyInput.Models; +using FancyInput.ViewModels; +using FancyInput.Views; +using System.Diagnostics; +using System.Net.Http; +using System.Text.Json; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using System.Reflection; +using System.IO; +using WpfAnimatedGif; + +using MessageBox = System.Windows.MessageBox; +using FancyInput.Views.Controls; +using System.Globalization; +using FancyInput.Views.Windows; +using System.Numerics; +namespace FancyInput +{ + + public static class VersionInfo + { + public static string CurrentVersion + { + get + { + var v = Assembly.GetExecutingAssembly().GetName().Version; + if (v == null) return "1.0.0"; + return $"{v.Major}.{v.Minor}.{v.Build}"; + } + } + public const string Date = "2026-6-27"; + public const string Mail = "contact@xlworkspace.com"; + public const string HealthAPI = "http://xlworkspace.com/api/fancyinput/health"; + public const string LatestVersionAPI = "http://xlworkspace.com/api/fancyinput/latest-version"; + public const string LatestDownloadUrl = "http://xlworkspace.com/api/fancyinput/latest-download-url"; + public const string UpdateInfoUrlPrefix = "http://xlworkspace.com/api/fancyinput/update-info/"; + public const string DownloadCountAllAPI = "http://xlworkspace.com/api/fancyinput/download-count/all"; + } + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class FancyInputMainWindow : Window + { + private bool _isExit = false; + //private bool _hidHideInitialized = false; + //private bool _hidHideEnabled = false; + public bool IsConfigLoaded= false; + public MainWindowViewModel ViewModel { get; set; } + //public ConsoleWindow ConsoleWindow { get; set; } = new ConsoleWindow(); + public FancyInputMainWindow() + { + InitializeComponent(); + ViewModel = new MainWindowViewModel(this); + ViewModel.LoadConfig(); + if (!string.IsNullOrEmpty(ViewModel.MacroSavePath) && Directory.Exists(ViewModel.MacroSavePath)) //自动加载宏文件夹 + ViewModel.MacroWindowViewModel.LoadMacroFromFolder(ViewModel.MacroSavePath); + ViewModel.CurrentVersion = VersionInfo.CurrentVersion; + this.Title = $"FancyInput v{VersionInfo.CurrentVersion}"; + + ViewModel.PanelType = MainWindowPanelType.Read; + ViewModel.InputParser.GetInput += DebubTest; + ParseArgs(); + + this.DataContext = ViewModel; + this.Loaded += FancyInputMainWindow_Loaded; + + } + + private void DebubTest(object? sender, InputArgs e) + { + if (ViewModel.DetectInput) + { + //if (e.Device == Models.InputDevice.MouseMove && e.MoveState == MoveState.Moving) return; + //if (e.Device == Models.InputDevice.MouseMove || e.Device == Models.InputDevice.MouseButton || e.Device == Models.InputDevice.MouseWheel) return; + //if (e.Device is Models.InputDevice.XInputButton or Models.InputDevice.XInputTrigger or Models.InputDevice.XInputStick + // or Models.InputDevice.SDLButton or Models.InputDevice.SDLTrigger or Models.InputDevice.SDLStick + // && e.GamepadUserIdx == GamepadUserIdx.One) + //{ + // if (TryMirrorRealGamepadToVirtual(e)) + // { + // //ViewModel.InputDetectString = $"MirrorGamepad: {e}"; + // return; + // } + //} + //if (e.Device == Models.InputDevice.Keyboard && e.RawKey.HasValue) + //{ + // if (TrySendVirtualGamepadTestInput(e.RawKey.Value, e.State ?? ButtonState.Released)) + // { + // ViewModel.InputDetectString = $"GamepadTest: {e.RawKey} {e.State}"; + // return; + // } + //} + //else + ViewModel.InputDetectString = $"{e}"; + } + } + + private static bool TryMirrorRealGamepadToVirtual(InputArgs e) + { + switch (e.Device) + { + case Models.InputDevice.XInputButton: + case Models.InputDevice.SDLButton: + if (e.Flag.HasValue) + { + if (e.Flag.Value == FIPGamepadButtonflags.RS) + { + var macroState = e.State ?? ButtonState.Released; + InputGenerator.SendXInputButton(FIPGamepadButtonflags.A, macroState); + InputGenerator.SendXInputButton(FIPGamepadButtonflags.X, macroState); + return true; + } + + InputGenerator.SendXInputButton(e.Flag.Value, e.State ?? ButtonState.Released); + return true; + } + return false; + + case Models.InputDevice.XInputTrigger: + case Models.InputDevice.SDLTrigger: + if (e.Side.HasValue && e.Value.HasValue) + { + InputGenerator.SendXInpuTrigger(e.Value.Value, e.Side.Value); + return true; + } + return false; + + case Models.InputDevice.XInputStick: + case Models.InputDevice.SDLStick: + if (e.Side.HasValue && e.ValueVector is { Length: >= 2 }) + { + InputGenerator.SendXInputStick(e.ValueVector[0], e.ValueVector[1], e.Side.Value); + return true; + } + return false; + + default: + return false; + } + } + + + + private void ShowMainWindow(object sender, RoutedEventArgs e) + { + Show(); + if (WindowState == WindowState.Minimized) + WindowState = WindowState.Normal; + this.Activate(); + } + + private void Exit_Click(object sender, RoutedEventArgs e) + { + AllowExit(); + Close(); + } + + public void AllowExit() => _isExit = true; + + protected override void OnClosing(System.ComponentModel.CancelEventArgs e) + { + if (!_isExit && ViewModel.MinimizeToTray) + { + e.Cancel = true; + Hide(); + } + else + { + ViewModel.TrayIconVisibility = Visibility.Collapsed; + NotifyIcon.Dispose(); + ViewModel.DetectInput = false; + // Keep physical pad hidden until virtual pad is detached. + // Use finally so hide cleanup still runs even if detach path changes in future. + + //VirtualControllerManager.Instance.Unplug(); + //TryDisableRealGamepadHideMode(); + } + base.OnClosing(e); + + + } + + private async void FancyInputMainWindow_Loaded(object sender, RoutedEventArgs e) + { + if (ViewModel.RunAsAdmin && !MainWindowViewModel.IsRunAsAdmin()) + { + string adminArg = ViewModel.AdminRestartArg; + string[] args = Environment.GetCommandLineArgs(); + string[] effectiveArgs = args.Skip(1).ToArray(); + string arguments = string.Join(" ", effectiveArgs.Select(a => $"\"{a}\"")); + arguments = $"{adminArg} {arguments}".Trim(); + var processInfo = new ProcessStartInfo + { + FileName = Process.GetCurrentProcess().MainModule!.FileName, + UseShellExecute = true, + Arguments = arguments + }; + Process.Start(processInfo); + } + if (!ViewModel.RunAsAdmin && MainWindowViewModel.IsRunAsAdmin()) + { + ViewModel.RunAsAdmin = true; + } + IsConfigLoaded = true; + InputLanguageManager.Current.CurrentInputLanguage = new CultureInfo("en-US"); + await CheckForUpdateAsync(latestRemind:false, isLaunch:true); + WindowHelper.TryAddRawInputHook(this); + + //TryEnableRealGamepadHideMode(); + } + + private void OpenMacroWindow(object sender, RoutedEventArgs e) + { + var macroWindowViewModel = ViewModel.MacroWindowViewModel; + MacroWindow macroWindow = new MacroWindow(macroWindowViewModel); + macroWindow.Owner = this; + macroWindow.WindowStartupLocation = WindowStartupLocation.CenterOwner; + macroWindow.ShowDialog(); + } + + //private void TryEnableRealGamepadHideMode() + //{ + // if (_hidHideInitialized) return; + // _hidHideInitialized = true; + + // var result = HidHideManager.TryEnableForCurrentApp(out string detail); + // Debug.WriteLine($"HidHide init: {detail}"); + + // _hidHideEnabled = result; + + // if (result) return; + + // // 提示一次即可:需要管理员权限 + 已安装 HidHide,且已在 HidHide 中勾选要隐藏的真实手柄。 + // AppMessageBox.Show( + // "未能启用“隐藏手柄”。\n\n" + + // "请确认:\n" + + // "1) 已安装 HidHide;\n" + + // "2) 以管理员权限运行 FancyInput;\n" + + // "3) 在 HidHide 配置中把真实手柄加入隐藏设备列表。\n\n" + + // $"详情:{detail}", + // "HidHide 提示", + // MessageBoxButton.OK, + // MessageBoxImage.Information); + //} + + //private void TryDisableRealGamepadHideMode() + //{ + // if (!_hidHideEnabled) return; + + // bool result = HidHideManager.TryDisableForCurrentApp(out string detail); + // Debug.WriteLine($"HidHide shutdown: {detail}"); + + // if (result) + // { + // _hidHideEnabled = false; + // } + //} + + private async Task CheckForUpdateAsync(bool latestRemind,bool isLaunch=false) + { + try + { + using var httpClient = new HttpClient(); + // 假设服务器返回纯文本版本号,如 "1.0.1" + var versionInfo = await httpClient.GetAsync(VersionInfo.LatestDownloadUrl); + versionInfo.EnsureSuccessStatusCode(); + using var jsonStream = await versionInfo.Content.ReadAsStreamAsync(); + var payload = await JsonSerializer.DeserializeAsync(jsonStream); + var latestVersion = payload.GetProperty("version").GetString(); + var downloadUrl = payload.GetProperty("url").GetString(); + var updateInfo = payload.GetProperty("update_info").GetString(); + latestVersion = latestVersion!.Trim(); + + if (IsNewerVersion(latestVersion, VersionInfo.CurrentVersion)) + { + if (isLaunch) + { + bool ignore = false; + foreach (string version in ViewModel.LaunchUpdateIgnoreVersions) + { + if (latestVersion == version) + { + ignore = true; + break; + } + } + if (ignore) return; + } + + var prompt = new UpdatePromptWindow( + UpdatePromptMode.UpdateAvailable, + VersionInfo.CurrentVersion, + latestVersion, + NormalizeUpdateInfo(updateInfo), + isLaunch) + { + Owner = this, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + + prompt.ShowDialog(); + + if (prompt.Action == UpdatePromptAction.Download) + { + this.AllowExit(); + this.Close(); + Process.Start(new ProcessStartInfo + { + FileName = downloadUrl, + UseShellExecute = true + }); + //var installerBytes = await httpClient.GetByteArrayAsync(downloadUrl); + //var outputPath = Path.Combine(Environment.CurrentDirectory, $"installer-{latestVersion}.exe"); + //await File.WriteAllBytesAsync(outputPath, installerBytes); + //Process.Start(new ProcessStartInfo + //{ + // FileName = outputPath, + // UseShellExecute = true + //}); + } + else if (isLaunch && prompt.IgnoreThisVersionOnLaunch) + { + ViewModel.LaunchUpdateIgnoreVersions = ViewModel.LaunchUpdateIgnoreVersions.Append(latestVersion).ToArray(); + ViewModel.SaveConfig(); + } + + } + else + { + if (latestRemind) + { + var prompt = new UpdatePromptWindow( + UpdatePromptMode.Latest, + VersionInfo.CurrentVersion, + latestVersion, + string.Empty, + false) + { + Owner = this, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + prompt.ShowDialog(); + } + } + } + catch (Exception ex) + { + if (latestRemind) + { + var prompt = new UpdatePromptWindow( + UpdatePromptMode.Error, + VersionInfo.CurrentVersion, + "未知", + ex.Message, + false) + { + Owner = this, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + prompt.ShowDialog(); + } + } + } + + private static string NormalizeUpdateInfo(string? updateInfo) + { + if (string.IsNullOrWhiteSpace(updateInfo)) + { + return "暂无更新说明。"; + } + + string text = updateInfo + .Replace("\\r\\n", "\n") + .Replace("\\n", "\n") + .Replace("\r\n", "\n") + .Trim(); + + if (!text.Contains('\n')) + { + text = text.Replace(";", ";\n").Replace(";", ";\n"); + } + + return text; + } + + private bool IsNewerVersion(string latest, string current) + { + Version? vLatest, vCurrent; + if (Version.TryParse(latest, out vLatest) && Version.TryParse(current, out vCurrent)) + return vLatest > vCurrent; + return false; + } + + public void ParseArgs() + { + string[] args = Environment.GetCommandLineArgs(); + if (args.Length > 1) + { + string filePath = args[1]; + OpenProjectFile(filePath); + } + } + + public void OpenProjectFile(string filePath) + { + if (System.IO.File.Exists(filePath)) + { + if (filePath.EndsWith(".fip", StringComparison.OrdinalIgnoreCase)) + { + ViewModel.LoadMode = LoadMode.ProjectFile; + ViewModel.ProjectFilePath = filePath; + LoadConfig(); + } + else if (filePath.EndsWith(".fips", StringComparison.OrdinalIgnoreCase)) + { + ViewModel.OpenFips(filePath); + } + } + this.Activate(); // 激活窗口 + } + + + + private void ReadPage_ConfigLoadClick(object sender, RoutedEventArgs e) => LoadConfig(); + + private void ReadPage_ConfigEditClick(object sender, RoutedEventArgs e) + { + try + { + ElementTreeViewModel elementViewModel = (ViewModel.LoadMode == LoadMode.PngJson) ? + new ElementTreeViewModel(ViewModel.PngFilePath, ViewModel.JsonFilePath) : new ElementTreeViewModel(ViewModel.ProjectFilePath); + ElementTreeWindow elementTreeWindow = new ElementTreeWindow(elementViewModel); + elementTreeWindow.ElementTreeViewModel.InputParser = ViewModel.InputParser; + elementTreeWindow.Owner = this; + elementTreeWindow.ShowDialog(); + } + catch (Exception ex) + { + FancyInput.AppMessageBox.Show($"发生错误:{ex.Message}\n\n详细信息:\n{ex}", + "加载失败", + MessageBoxButton.OK, + MessageBoxImage.Error + ); + } + } + + + public OverlayWindowViewModel? LoadConfig() + { + try + { + LoadMode loadMode = ViewModel.LoadMode; + ElementTreeViewModel vm; + switch (loadMode) + { + case LoadMode.PngJson: + XImage inputImage = new XImage(ViewModel.PngFilePath); + OverlayRoot overlayRoot = OverlayParser.Parse(ViewModel.JsonFilePath); + vm = new ElementTreeViewModel(overlayRoot, inputImage); + break; + case LoadMode.ProjectFile: + vm = new ElementTreeViewModel(ViewModel.ProjectFilePath); + break; + default: + throw new Exception($"无法识别的加载类型:{loadMode}"); + } + vm.LoadMode = loadMode; + vm.PngFilePath = ViewModel.PngFilePath; + vm.JsonFilePath = ViewModel.JsonFilePath; + vm.ProjectFilePath = ViewModel.ProjectFilePath; + OverlayWindow overlayWindow = new OverlayWindow(vm); + overlayWindow.Show(); + + ViewModel.InputParser.GetInput += overlayWindow.ElementTreeViewModel.InputParser_GetInput; + overlayWindow.Closed += (s, e) => + { + overlayWindow.Dispose(); + ViewModel.OverlayWindowViewModels.RemoveAt(overlayWindow.OverlayWindowViewModel.Idx); + ViewModel.InputParser.GetInput -= overlayWindow.ElementTreeViewModel.InputParser_GetInput; + ViewModel.RefreshOverlayIndices(); + }; + + ViewModel.OverlayWindowViewModels.Add(overlayWindow.OverlayWindowViewModel); + ViewModel.RefreshOverlayIndices(); + + string pngName = System.IO.Path.GetFileName(ViewModel.PngFilePath); + string jsonName = System.IO.Path.GetFileName(ViewModel.JsonFilePath); + string projectFileName = System.IO.Path.GetFileName(ViewModel.ProjectFilePath); + ViewModel.DebugCircleFill = new SolidColorBrush(Colors.Green); + ViewModel.DebugLabelString = (loadMode == LoadMode.PngJson) ? $"已加载:{pngName},{jsonName}" : $"已加载:{projectFileName}"; + + return overlayWindow.OverlayWindowViewModel; + } + catch (Exception ex) + { + ViewModel.DebugCircleFill = new SolidColorBrush(Colors.Red); + ViewModel.DebugLabelString = $"加载出错:{ex.Message}"; + FancyInput.AppMessageBox.Show($"发生错误:{ex.Message}\n\n详细信息:\n{ex}", + "加载失败", + MessageBoxButton.OK, + MessageBoxImage.Error + ); + return null; + } + } + + public OverlayWindowViewModel? LoadElementTreeViewModel(ElementTreeViewModel vm) + { + OverlayWindow overlayWindow = new OverlayWindow(vm); + overlayWindow.Show(); + + ViewModel.InputParser.GetInput += overlayWindow.ElementTreeViewModel.InputParser_GetInput; + overlayWindow.Closed += (s, e) => + { + overlayWindow.Dispose(); + ViewModel.OverlayWindowViewModels.RemoveAt(overlayWindow.OverlayWindowViewModel.Idx); + ViewModel.InputParser.GetInput -= overlayWindow.ElementTreeViewModel.InputParser_GetInput; + ViewModel.RefreshOverlayIndices(); + }; + + ViewModel.OverlayWindowViewModels.Add(overlayWindow.OverlayWindowViewModel); + ViewModel.RefreshOverlayIndices(); + return overlayWindow.OverlayWindowViewModel; + } + + + private void Window_Closed(object sender, EventArgs e) + { + if (ViewModel == null) return; + int count = ViewModel.OverlayWindowViewModels.Count; + for (int i = count-1; i >=0; i--) + { + ViewModel.OverlayWindowViewModels[i].OverlayWindow.Close(); + } + ViewModel.InputParser.Dispose(); + //VirtualControllerManager.Instance.Unplug(); + } + + + private void ChangePanel(object sender, RoutedEventArgs e) + { + Button? btn = sender as Button; + if (btn == null) return; + MainWindowPanelType panelType = btn.Tag is MainWindowPanelType pnl ? pnl : MainWindowPanelType.Read; + ViewModel.PanelType = panelType; + } + + + + private void CreatePage_CreateFromEmptyClick(object sender, RoutedEventArgs e) + { + ElementTreeViewModel elementTreeViewModel = new ElementTreeViewModel(); + ElementTreeWindow elementTreeWindow = new ElementTreeWindow(elementTreeViewModel); + elementTreeWindow.ElementTreeViewModel.InputParser = ViewModel.InputParser; + elementTreeWindow.DataContext = elementTreeViewModel; + elementTreeWindow.Owner = this; + elementTreeWindow.ShowDialog(); + } + + private void About(object sender, RoutedEventArgs e) + { + AboutWindow aboutWindow = new AboutWindow(VersionInfo.CurrentVersion, VersionInfo.Date,VersionInfo.Mail); + aboutWindow.Owner = this; + aboutWindow.WindowStartupLocation = WindowStartupLocation.CenterOwner; + aboutWindow.CheckUpdateRequested += async () => + { + await CheckForUpdateAsync(latestRemind:true); + }; + aboutWindow.UpdateTimelineRequested += async () => + { + await ShowUpdateTimelineWindowAsync(); + }; + aboutWindow.ShowDialog(); + } + + private async Task ShowUpdateTimelineWindowAsync() + { + IReadOnlyList entries = await FetchTimelineEntriesAsync(); + var timelineWindow = new UpdateTimelineWindow(VersionInfo.CurrentVersion, VersionInfo.Date, entries) + { + Owner = this, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + timelineWindow.ShowDialog(); + } + + private async Task> FetchTimelineEntriesAsync() + { + try + { + using var httpClient = new HttpClient(); + var response = await httpClient.GetAsync(VersionInfo.DownloadCountAllAPI); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + var payload = await JsonSerializer.DeserializeAsync(stream); + var entries = new List(); + + if (payload.TryGetProperty("versions", out var versions) && versions.ValueKind == JsonValueKind.Array) + { + foreach (var item in versions.EnumerateArray()) + { + if (!item.TryGetProperty("version", out var versionElement)) + { + continue; + } + + string version = (versionElement.GetString() ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(version)) + { + continue; + } + + int downloads = 0; + if (item.TryGetProperty("downloads", out var downloadElement) && downloadElement.TryGetInt32(out var parsedDownloads)) + { + downloads = parsedDownloads; + } + + string updateInfo = await TryFetchUpdateInfoAsync(httpClient, version); + entries.Add(new VersionTimelineEntry + { + Version = version, + DownloadCount = downloads, + UpdateInfo = NormalizeUpdateInfo(updateInfo), + IsCurrent = version == VersionInfo.CurrentVersion, + IsExpanded = version == VersionInfo.CurrentVersion, + }); + } + } + + if (entries.Count == 0) + { + return BuildFallbackTimelineEntries(); + } + + if (!entries.Any(e => e.IsCurrent)) + { + entries.Insert(0, new VersionTimelineEntry + { + Version = VersionInfo.CurrentVersion, + DownloadCount = 0, + UpdateInfo = NormalizeUpdateInfo(ReadLocalUpdateInfo()), + IsCurrent = true, + IsExpanded = true, + }); + } + + return entries; + } + catch + { + return BuildFallbackTimelineEntries(); + } + } + + private async Task TryFetchUpdateInfoAsync(HttpClient httpClient, string version) + { + try + { + string url = VersionInfo.UpdateInfoUrlPrefix + Uri.EscapeDataString(version); + var response = await httpClient.GetAsync(url); + if (!response.IsSuccessStatusCode) + { + return "暂无更新说明。"; + } + + using var stream = await response.Content.ReadAsStreamAsync(); + var payload = await JsonSerializer.DeserializeAsync(stream); + if (payload.TryGetProperty("info", out var infoElement)) + { + return infoElement.GetString() ?? "暂无更新说明。"; + } + + return "暂无更新说明。"; + } + catch + { + return "暂无更新说明。"; + } + } + + private IReadOnlyList BuildFallbackTimelineEntries() + { + return new List + { + new VersionTimelineEntry + { + Version = VersionInfo.CurrentVersion, + DownloadCount = 0, + UpdateInfo = NormalizeUpdateInfo(ReadLocalUpdateInfo()), + IsCurrent = true, + IsExpanded = true, + } + }; + } + + private static string ReadLocalUpdateInfo() + { + string[] candidates = + { + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Update.txt"), + Path.Combine(Environment.CurrentDirectory, "Update.txt"), + }; + + foreach (var path in candidates) + { + if (File.Exists(path)) + { + return File.ReadAllText(path); + } + } + + return "暂无本地更新说明。"; + } + + + private void ManagePage_SaveGroupClick(object sender, RoutedEventArgs e)=> ViewModel.SaveLoadedGroup(); + private void ManagePage_LoadGroupClick(object sender, RoutedEventArgs e)=> ViewModel.AddLoadedGroup(); + private void ManagePage_UpMoveClick(object sender, ElementMoveActionEventArgs e) => ViewModel.UpMoveAt(e.Index); + private void ManagePage_DownMoveClick(object sender, ElementMoveActionEventArgs e) => ViewModel.DownMoveAt(e.Index); + + + } +} diff --git a/FancyInput/Models/EnumDescriptionConverter.cs b/FancyInput/Models/EnumDescriptionConverter.cs new file mode 100644 index 0000000..da22a79 --- /dev/null +++ b/FancyInput/Models/EnumDescriptionConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace FancyInput.Models +{ + public class EnumDescriptionConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Enum e) + return e.GetDescription(); + return value?.ToString() ?? ""; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/FancyInput/Models/HidHideManager.cs b/FancyInput/Models/HidHideManager.cs new file mode 100644 index 0000000..1d8b98a --- /dev/null +++ b/FancyInput/Models/HidHideManager.cs @@ -0,0 +1,350 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json; +using FancyInput.ViewModels; + +namespace FancyInput.Models +{ + internal static class HidHideManager + { + private static readonly HashSet HiddenByFancyInput = new(StringComparer.OrdinalIgnoreCase); + + public static bool TryEnableForCurrentApp(out string detail) + { + detail = string.Empty; + + if (!MainWindowViewModel.IsRunAsAdmin()) + { + detail = "当前进程不是管理员权限。"; + return false; + } + + if (!TryFindCliPath(out string? cliPath)) + { + detail = "未找到 HidHideCLI.exe(请先安装 HidHide)。"; + return false; + } + + string? appPath = Process.GetCurrentProcess().MainModule?.FileName; + if (string.IsNullOrWhiteSpace(appPath) || !File.Exists(appPath)) + { + detail = "无法获取当前程序路径。"; + return false; + } + + if (!RunCli(cliPath!, $"--app-reg \"{appPath}\"", out string appRegErr)) + { + detail = $"注册白名单失败:{appRegErr}"; + return false; + } + + if (!RunCli(cliPath!, "--cloak-on", out string cloakErr)) + { + detail = $"开启 HidHide cloak 失败:{cloakErr}"; + return false; + } + + if (!TryHidePresentGamingDevices(cliPath!, out int hiddenCount, out string hideErr)) + { + detail = $"已开启 cloak,但自动隐藏真实手柄失败:{hideErr}"; + return false; + } + + detail = $"已注册白名单、开启 cloak,并自动隐藏 {hiddenCount} 个真实手柄设备。"; + return true; + } + + public static bool TryDisableForCurrentApp(out string detail) + { + detail = string.Empty; + + if (!MainWindowViewModel.IsRunAsAdmin()) + { + detail = "当前进程不是管理员权限。"; + return false; + } + + if (!TryFindCliPath(out string? cliPath)) + { + detail = "未找到 HidHideCLI.exe(请先安装 HidHide)。"; + return false; + } + + string? appPath = Process.GetCurrentProcess().MainModule?.FileName; + if (string.IsNullOrWhiteSpace(appPath) || !File.Exists(appPath)) + { + detail = "无法获取当前程序路径。"; + return false; + } + + bool unhideOk = TryUnhideDevicesAddedByFancyInput(cliPath!, out string unhideErr); + bool cloakOk = RunCli(cliPath!, "--cloak-off", out string cloakErr); + bool unregOk = RunCli(cliPath!, $"--app-unreg \"{appPath}\"", out string unregErr); + + if (unhideOk && cloakOk && unregOk) + { + detail = "已恢复被隐藏设备、关闭 HidHide cloak 并移除白名单。"; + return true; + } + + var errors = new List(); + if (!unhideOk) errors.Add($"恢复设备失败:{unhideErr}"); + if (!cloakOk) errors.Add($"关闭 cloak 失败:{cloakErr}"); + if (!unregOk) errors.Add($"移除白名单失败:{unregErr}"); + + if (errors.Count > 0) + { + detail = string.Join(";", errors); + return false; + } + + detail = "已执行 HidHide 退出清理。"; + return true; + } + + private static bool TryHidePresentGamingDevices(string cliPath, out int hiddenCount, out string error) + { + hiddenCount = 0; + error = string.Empty; + + if (!RunCliWithOutput(cliPath, "--dev-gaming", out string output, out string listErr)) + { + error = $"读取游戏设备列表失败:{listErr}"; + return false; + } + + var paths = ParsePresentGamingDevicePaths(output); + if (paths.Count == 0) + { + return true; + } + + foreach (string path in paths) + { + if (!RunCli(cliPath, $"--dev-hide \"{path}\"", out string hideErr)) + { + error = $"隐藏设备失败:{path},{hideErr}"; + return false; + } + + HiddenByFancyInput.Add(path); + hiddenCount++; + } + + return true; + } + + private static bool TryUnhideDevicesAddedByFancyInput(string cliPath, out string error) + { + error = string.Empty; + + if (HiddenByFancyInput.Count == 0) + { + return true; + } + + foreach (string path in HiddenByFancyInput.ToArray()) + { + if (!RunCli(cliPath, $"--dev-unhide \"{path}\"", out string unhideErr)) + { + error = $"{path} -> {unhideErr}"; + return false; + } + + HiddenByFancyInput.Remove(path); + } + + return true; + } + + private static List ParsePresentGamingDevicePaths(string output) + { + var result = new List(); + if (string.IsNullOrWhiteSpace(output)) + { + return result; + } + + try + { + using JsonDocument document = JsonDocument.Parse(output); + if (document.RootElement.ValueKind != JsonValueKind.Array) + { + return result; + } + + foreach (JsonElement container in document.RootElement.EnumerateArray()) + { + if (!container.TryGetProperty("devices", out JsonElement devices) || devices.ValueKind != JsonValueKind.Array) + { + continue; + } + + foreach (JsonElement device in devices.EnumerateArray()) + { + if (!TryGetBoolean(device, "present", out bool present) || !present) continue; + if (!TryGetBoolean(device, "gamingDevice", out bool gaming) || !gaming) continue; + + string instancePath = TryGetString(device, "deviceInstancePath"); + if (string.IsNullOrWhiteSpace(instancePath)) continue; + + if (IsVirtualDevice(device, instancePath)) continue; + result.Add(instancePath); + } + } + } + catch + { + // Ignore parse issues and return empty so caller can continue safely. + } + + return result; + } + + private static bool IsVirtualDevice(JsonElement device, string instancePath) + { + string[] virtualHints = + { + "VIGEM", + "NEFARIUS", + "VIRTUAL", + "ROOT\\" + }; + + string baseContainer = TryGetString(device, "baseContainerDeviceInstancePath"); + string friendly = TryGetString(device, "friendlyName"); + string product = TryGetString(device, "product"); + + string merged = string.Join("|", new[] { instancePath, baseContainer, friendly, product }); + return virtualHints.Any(h => merged.Contains(h, StringComparison.OrdinalIgnoreCase)); + } + + private static bool TryGetBoolean(JsonElement element, string propertyName, out bool value) + { + value = false; + if (!element.TryGetProperty(propertyName, out JsonElement property)) return false; + if (property.ValueKind != JsonValueKind.True && property.ValueKind != JsonValueKind.False) return false; + value = property.GetBoolean(); + return true; + } + + private static string TryGetString(JsonElement element, string propertyName) + { + if (!element.TryGetProperty(propertyName, out JsonElement property)) return string.Empty; + if (property.ValueKind != JsonValueKind.String) return string.Empty; + return property.GetString() ?? string.Empty; + } + + private static bool TryFindCliPath(out string? cliPath) + { + string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string[] candidates = + { + Path.Combine(programFiles, "Nefarius Software Solutions", "HidHide", "x64", "HidHideCLI.exe"), + Path.Combine(programFiles, "Nefarius Software Solutions", "HidHide", "HidHideCLI.exe"), + Path.Combine(programFiles, "Nefarius Software Solutions e.U", "HidHide", "x64", "HidHideCLI.exe"), + Path.Combine(programFiles, "Nefarius Software Solutions e.U", "HidHide", "HidHideCLI.exe") + }; + + foreach (string path in candidates) + { + if (File.Exists(path)) + { + cliPath = path; + return true; + } + } + + cliPath = null; + return false; + } + + private static bool RunCli(string cliPath, string args, out string error) + { + error = string.Empty; + + try + { + var psi = new ProcessStartInfo + { + FileName = cliPath, + Arguments = args, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + WorkingDirectory = Path.GetDirectoryName(cliPath) ?? AppContext.BaseDirectory + }; + + using var p = Process.Start(psi); + if (p == null) + { + error = "无法启动 HidHideCLI。"; + return false; + } + + p.WaitForExit(5000); + string stderr = p.StandardError.ReadToEnd().Trim(); + if (p.ExitCode != 0) + { + error = string.IsNullOrEmpty(stderr) ? $"退出码 {p.ExitCode}" : stderr; + return false; + } + + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + private static bool RunCliWithOutput(string cliPath, string args, out string output, out string error) + { + output = string.Empty; + error = string.Empty; + + try + { + var psi = new ProcessStartInfo + { + FileName = cliPath, + Arguments = args, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + WorkingDirectory = Path.GetDirectoryName(cliPath) ?? AppContext.BaseDirectory + }; + + using var p = Process.Start(psi); + if (p == null) + { + error = "无法启动 HidHideCLI。"; + return false; + } + + output = p.StandardOutput.ReadToEnd(); + string stderr = p.StandardError.ReadToEnd().Trim(); + p.WaitForExit(5000); + + if (p.ExitCode != 0) + { + error = string.IsNullOrEmpty(stderr) ? $"退出码 {p.ExitCode}" : stderr; + return false; + } + + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + } +} \ No newline at end of file diff --git a/FancyInput/Models/InputGenerator.cs b/FancyInput/Models/InputGenerator.cs new file mode 100644 index 0000000..aceeaa8 --- /dev/null +++ b/FancyInput/Models/InputGenerator.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace FancyInput.Models +{ + public class InputGenerator + { + public static bool PlugVirtualGamepad() => VirtualControllerManager.Instance.PlugIn(); + + public static void UnplugVirtualGamepad() => VirtualControllerManager.Instance.Unplug(); + + public static bool IsVirtualGamepadPlugged => VirtualControllerManager.Instance.IsAvailable; + + public static int VirtualGamepadIndex => VirtualControllerManager.Instance.Index; + + public static void SendKey(FipRawKeys rawKey, ButtonState state = ButtonState.Pressed) + { + bool keyUp = state == ButtonState.Released; + var keyInfo = MapRawKey(rawKey); + if (keyInfo is null) return; + SendInputs( new[] { CreateKeyboardInput(keyInfo.Value.VirtualKey, keyInfo.Value.ScanCode, keyInfo.Value.IsExtended, keyUp) }); + } + + public static void SendMouseButton(MouseButtons button, ButtonState state = ButtonState.Pressed) + =>SendInputs(new[] { CreateMouseButtonInput(button, state == ButtonState.Pressed) }); + + public static void SendMouseMove(int x, int y, bool absolute = true) => SendInputs(new[] { CreateMouseMoveInput(x, y, absolute) }); + + public static void SendMouseWheel(int delta)=>SendInputs(new[] { CreateMouseWheelInput(delta) }); + + public static void SendXInputButton(FIPGamepadButtonflags button, ButtonState state = ButtonState.Pressed) + { + if (!VirtualControllerManager.Instance.IsAvailable) + { + if (!VirtualControllerManager.Instance.Initialize()) + throw new NotSupportedException("Virtual XInput controller is not available. Ensure ViGEm client DLL is present and ViGEmBus is installed."); + } + + bool pressed = state == ButtonState.Pressed; + VirtualControllerManager.Instance.SetButton(button, pressed); + } + + public static void SendXInpuTrigger(int triggerValue,Side side) + { + if (!VirtualControllerManager.Instance.IsAvailable) + { + if (!VirtualControllerManager.Instance.Initialize()) + throw new NotSupportedException("Virtual XInput controller is not available. Ensure ViGEm client DLL is present and ViGEmBus is installed."); + } + + VirtualControllerManager.Instance.SetTrigger(triggerValue, side); + } + + public static void SendXInputStick(int x, int y, Side side) + { + if (!VirtualControllerManager.Instance.IsAvailable) + { + if (!VirtualControllerManager.Instance.Initialize()) + throw new NotSupportedException("Virtual XInput controller is not available. Ensure ViGEm client DLL is present and ViGEmBus is installed."); + } + + VirtualControllerManager.Instance.SetStick(x, y, side); + } + + private static void SendInputs(IEnumerable inputs) + { + var inputList = inputs as INPUT[] ?? new List(inputs).ToArray(); + if (inputList.Length == 0) return; + + if (SendInput((uint)inputList.Length, inputList, Marshal.SizeOf(typeof(INPUT))) == 0) + { + int error = Marshal.GetLastWin32Error(); + throw new InvalidOperationException($"SendInput failed with error code {error}."); + } + } + + private static INPUT CreateKeyboardInput(ushort virtualKey, ushort scanCode, bool isExtended, bool keyUp) + { + return new INPUT + { + type = INPUT_KEYBOARD, + U = new InputUnion + { + ki = new KEYBDINPUT + { + wVk = virtualKey, + wScan = scanCode, + dwFlags = KEYEVENTF_SCANCODE | (isExtended ? KEYEVENTF_EXTENDEDKEY : 0) | (keyUp ? KEYEVENTF_KEYUP : 0) + } + } + }; + } + + private static INPUT CreateMouseButtonInput(MouseButtons button, bool keyDown) + { + return new INPUT + { + type = INPUT_MOUSE, + U = new InputUnion + { + mi = new MOUSEINPUT + { + dwFlags = button switch + { + MouseButtons.Left => keyDown ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP, + MouseButtons.Right => keyDown ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP, + MouseButtons.Middle => keyDown ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP, + MouseButtons.XButton1 => keyDown ? MOUSEEVENTF_XDOWN : MOUSEEVENTF_XUP, + MouseButtons.XButton2 => keyDown ? MOUSEEVENTF_XDOWN : MOUSEEVENTF_XUP, + _ => throw new NotSupportedException($"Mouse button {button} is not supported.") + }, + mouseData = button switch + { + MouseButtons.XButton1 => XBUTTON1, + MouseButtons.XButton2 => XBUTTON2, + _ => 0 + } + } + } + }; + } + + private static INPUT CreateMouseMoveInput(int x, int y, bool absolute) + { + if (absolute) + { + int normalizedX = NormalizeAbsoluteCoordinate(x, Screen.PrimaryScreen?.Bounds.Width ?? 1); + int normalizedY = NormalizeAbsoluteCoordinate(y, Screen.PrimaryScreen?.Bounds.Height ?? 1); + + return new INPUT + { + type = INPUT_MOUSE, + U = new InputUnion + { + mi = new MOUSEINPUT + { + dx = normalizedX, + dy = normalizedY, + dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE + } + } + }; + } + + // Relative move: pass deltas directly and do not set ABSOLUTE + return new INPUT + { + type = INPUT_MOUSE, + U = new InputUnion + { + mi = new MOUSEINPUT + { + dx = x, + dy = y, + dwFlags = MOUSEEVENTF_MOVE + } + } + }; + } + + private static INPUT CreateMouseWheelInput(int delta) + { + return new INPUT + { + type = INPUT_MOUSE, + U = new InputUnion + { + mi = new MOUSEINPUT + { + mouseData = unchecked((uint)delta), + dwFlags = MOUSEEVENTF_WHEEL + } + } + }; + } + + + private static KeyInfo? MapRawKey(FipRawKeys rawKey) + { + uint code = (uint)rawKey; + if (code == 0) return null; + + // Detect E0/E1 prefix by high byte(s). Common pattern: 0xE0xx or 0xE1xxxx + bool isExtended = (code & 0xFF00u) == 0xE000u || (code & 0xFF0000u) == 0xE10000u; + ushort scan = (ushort)(code & 0xFFu); + // Determine a sensible virtual-key (vKey). For numpad keys this depends on NumLock state. + ushort vKey = 0; + + try + { + bool numLockOn = System.Windows.Forms.Control.IsKeyLocked(Keys.NumLock); + + // Handle numpad-specific behavior when NumLock is toggled + if (RawInputParser.NumLockAffectedFipRawKeys.Contains(rawKey)) + { + vKey = rawKey switch + { + FipRawKeys.NumPad0 => (ushort)(numLockOn ? Keys.NumPad0 : Keys.Insert), + FipRawKeys.NumPad1 => (ushort)(numLockOn ? Keys.NumPad1 : Keys.End), + FipRawKeys.NumPad2 => (ushort)(numLockOn ? Keys.NumPad2 : Keys.Down), + FipRawKeys.NumPad3 => (ushort)(numLockOn ? Keys.NumPad3 : Keys.PageDown), + FipRawKeys.NumPad4 => (ushort)(numLockOn ? Keys.NumPad4 : Keys.Left), + FipRawKeys.NumPad5 => (ushort)(numLockOn ? Keys.NumPad5 : Keys.Clear), + FipRawKeys.NumPad6 => (ushort)(numLockOn ? Keys.NumPad6 : Keys.Right), + FipRawKeys.NumPad7 => (ushort)(numLockOn ? Keys.NumPad7 : Keys.Home), + FipRawKeys.NumPad8 => (ushort)(numLockOn ? Keys.NumPad8 : Keys.Up), + FipRawKeys.NumPad9 => (ushort)(numLockOn ? Keys.NumPad9 : Keys.PageUp), + FipRawKeys.Decimal => (ushort)(numLockOn ? Keys.Decimal : Keys.Delete), + FipRawKeys.Multiply => (ushort)Keys.Multiply, + FipRawKeys.Add => (ushort)Keys.Add, + FipRawKeys.Subtract => (ushort)Keys.Subtract, + FipRawKeys.Divide => (ushort)Keys.Divide, + _ => 0 + }; + } + + // Special-case NumpadEnter + if (rawKey == FipRawKeys.NumpadEnter) + { + vKey = (ushort)Keys.Return; + } + + // Fallback: try to parse enum name into Keys, else use MapVirtualKey from scan code + if (vKey == 0) + { + if (Enum.TryParse(rawKey.ToString(), true, out Keys parsed)) + { + vKey = (ushort)parsed; + } + else + { + uint mapped = MapVirtualKey(scan, 1); // MAPVK_VSC_TO_VK + vKey = (ushort)mapped; + } + } + } + catch + { + // On any failure, fall back to mapping via scan code only + vKey = (ushort)MapVirtualKey(scan, 1); + } + + return new KeyInfo(vKey, scan, isExtended); + } + + + private readonly record struct KeyInfo(ushort VirtualKey, ushort ScanCode, bool IsExtended); + + private const uint INPUT_MOUSE = 0; + private const uint INPUT_KEYBOARD = 1; + + private const uint KEYEVENTF_EXTENDEDKEY = 0x0001; + private const uint KEYEVENTF_KEYUP = 0x0002; + private const uint KEYEVENTF_SCANCODE = 0x0008; + + private const uint MOUSEEVENTF_LEFTDOWN = 0x0002; + private const uint MOUSEEVENTF_LEFTUP = 0x0004; + private const uint MOUSEEVENTF_MOVE = 0x0001; + private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008; + private const uint MOUSEEVENTF_RIGHTUP = 0x0010; + private const uint MOUSEEVENTF_MIDDLEDOWN = 0x0020; + private const uint MOUSEEVENTF_MIDDLEUP = 0x0040; + private const uint MOUSEEVENTF_XDOWN = 0x0080; + private const uint MOUSEEVENTF_XUP = 0x0100; + private const uint MOUSEEVENTF_WHEEL = 0x0800; + private const uint MOUSEEVENTF_ABSOLUTE = 0x8000; + + private const uint XBUTTON1 = 0x0001; + private const uint XBUTTON2 = 0x0002; + + private const uint MAPVK_VK_TO_VSC = 0; + + [StructLayout(LayoutKind.Sequential)] + private struct INPUT + { + public uint type; + public InputUnion U; + } + + [StructLayout(LayoutKind.Explicit)] + private struct InputUnion + { + [FieldOffset(0)] + public MOUSEINPUT mi; + + [FieldOffset(0)] + public KEYBDINPUT ki; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MOUSEINPUT + { + public int dx; + public int dy; + public uint mouseData; + public uint dwFlags; + public uint time; + public IntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + private struct KEYBDINPUT + { + public ushort wVk; + public ushort wScan; + public uint dwFlags; + public uint time; + public IntPtr dwExtraInfo; + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); + + [DllImport("user32.dll", SetLastError = true)] + private static extern uint MapVirtualKey(uint uCode, uint uMapType); + + private static int NormalizeAbsoluteCoordinate(int coordinate, int length) + { + if (length <= 1) return 0; + coordinate = Math.Max(0, Math.Min(length - 1, coordinate)); + return (int)Math.Round((coordinate * 65535.0) / (length - 1)); + } + } +} \ No newline at end of file diff --git a/FancyInput/Models/InputHandler.cs b/FancyInput/Models/InputHandler.cs new file mode 100644 index 0000000..4c2b1db --- /dev/null +++ b/FancyInput/Models/InputHandler.cs @@ -0,0 +1,549 @@ +using FancyInput.ViewModels; +using SharpDX.XInput; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Reflection.Emit; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows.Forms; +using System.Windows.Media; +using System.Windows.Threading; +using System.Xml.Linq; +using static System.Windows.Forms.AxHost; +using Timer = System.Threading.Timer; + + +namespace FancyInput.Models +{ + public class InputHandler : IDisposable + { + public static bool USE_CENTER_MOUSEMOVE = true; + public static int MOUSE_SENSE = 20; + public const int TRIGGER_CACHE_RANGE = 9; + public const int BUTTON_TIMER_INTERVAL = 25; + + // 用于滑动平均的Shift + private Queue<(double X, double Y)> _mouseMoveHistory = new Queue<(double X, double Y)>(); + + private ElementTreeViewModel? _elementTreeViewModel; + public ElementTreeViewModel? ElementTreeViewModel => _elementTreeViewModel; + private ObservableCollection _elementViewModels; + public ObservableCollection ElementViewModels => _elementViewModels; + private Dictionary> _respondersIdx; + + private DispatcherTimer _buttonTimer; + private HashSet _buttonRuning = new HashSet(); + + private Dictionary> _keyboardDictionary = new(); + private Dictionary> _rawKeyboardDictionary = new(); + private Dictionary> _gamepadButtonDictionary = new(); + private Dictionary> _mouseDictionary = new(); + + public const int MOUSEMOVE_DEADZONE= 10; + public InputHandler(ElementTreeViewModel elementTreeViewModel) + { + _elementTreeViewModel = elementTreeViewModel; + _elementViewModels = _elementTreeViewModel.ElementViewModels; + _respondersIdx = new Dictionary>(); + ResetRespondersAndDictionaries(); + _buttonTimer = new DispatcherTimer(DispatcherPriority.Send) { Interval = TimeSpan.FromMilliseconds(BUTTON_TIMER_INTERVAL) }; + _buttonTimer.Tick += ButtonTimer_Tick; + _buttonTimer.Start(); + } + + private void ButtonTimer_Tick(object? sender, EventArgs e) + { + List vms = _buttonRuning.ToList(); + foreach (var vm in vms) + { + if (vm.Stopwatch.ElapsedMilliseconds >= vm.StopwatchMilliseconds) + { + vm.CurrentSourceIdx = 0; + vm.Stopwatch.Stop(); + vm.Stopwatch.Reset(); + _buttonRuning.Remove(vm); + } + } + } + + public InputHandler(ElementViewModel elementViewModel) + { + _elementTreeViewModel = null; + _elementViewModels = new ObservableCollection { elementViewModel }; + _respondersIdx = new Dictionary>(); + ResetRespondersAndDictionaries(); + _buttonTimer = new DispatcherTimer(DispatcherPriority.Send) { Interval = TimeSpan.FromMilliseconds(BUTTON_TIMER_INTERVAL) }; + _buttonTimer.Tick += ButtonTimer_Tick; + _buttonTimer.Start(); + } + + public InputHandler(OverlayRoot overlayRoot, XImage image) + { + _elementTreeViewModel = new ElementTreeViewModel(overlayRoot, image); + _elementViewModels = _elementTreeViewModel.ElementViewModels; + _respondersIdx = new Dictionary>(); + ResetRespondersAndDictionaries(); + _buttonTimer = new DispatcherTimer(DispatcherPriority.Send) { Interval = TimeSpan.FromMilliseconds(BUTTON_TIMER_INTERVAL) }; + _buttonTimer.Tick += ButtonTimer_Tick; + _buttonTimer.Start(); + } + + public InputHandler(string pngPath, string jsonPath) + { + _elementTreeViewModel = new ElementTreeViewModel(pngPath, jsonPath); + _elementViewModels = _elementTreeViewModel.ElementViewModels; + _respondersIdx = new Dictionary>(); + ResetRespondersAndDictionaries(); + _buttonTimer = new DispatcherTimer(DispatcherPriority.Send) { Interval = TimeSpan.FromMilliseconds(BUTTON_TIMER_INTERVAL) }; + _buttonTimer.Tick += ButtonTimer_Tick; + _buttonTimer.Start(); + } + + public void Dispose() + { + _buttonTimer.Stop(); + } + + public void ResetRespondersAndDictionaries() + { + _respondersIdx.Clear(); + _keyboardDictionary.Clear(); + _rawKeyboardDictionary.Clear(); + _mouseDictionary.Clear(); + _gamepadButtonDictionary.Clear(); + foreach (InputDevice device in Enum.GetValues(typeof(InputDevice))) + { + _respondersIdx[device] = new List(); + } + for (int i = 0; i < _elementViewModels.Count; i++) + { + ElementViewModel elementViewModel = _elementViewModels[i]; + switch (elementViewModel.ElementType) + { + case ElementType.GamepadPlayerId: + _respondersIdx[InputDevice.SDLGuide].Add(i); + break; + case ElementType.Texture: + break; + case ElementType.MouseMovement: + _respondersIdx[InputDevice.MouseMove].Add(i); + break; + case ElementType.KeyboardButton: + _respondersIdx[InputDevice.Keyboard].Add(i); + KeyBoardCodeType keyBoardCode = elementViewModel.SelectedKeyBoardButton; + if (Enum.TryParse(keyBoardCode.ToString(), out FipKeys key)) + { + if (!_keyboardDictionary.ContainsKey(key)) + { + List idx = new List() { i }; + _keyboardDictionary[key] = idx; + } + else + _keyboardDictionary[key].Add(i); + } + else + { + throw new Exception($"{keyBoardCode.ToString()} is not a valid enum name for Type: FipKeys"); + } + if (Enum.TryParse(keyBoardCode.ToString(), out FipRawKeys rawKey)) + { + if (!_rawKeyboardDictionary.ContainsKey(rawKey)) + { + List idx = new List() { i }; + _rawKeyboardDictionary[rawKey] = idx; + } + else + _rawKeyboardDictionary[rawKey].Add(i); + } + break; + case ElementType.MouseButton: + _respondersIdx[InputDevice.MouseButton].Add(i); + MouseCodeType mouseButtonCode = elementViewModel.SelectedMouseButton; + if (Enum.TryParse(mouseButtonCode.ToString(), out MouseButtons button)) + { + if (!_mouseDictionary.ContainsKey(button)) + { + List idx = new List() { i }; + _mouseDictionary[button] = idx; + } + else + _mouseDictionary[button].Add(i); + } + + break; + case ElementType.MouseWheel: + _respondersIdx[InputDevice.MouseButton].Add(i); + _respondersIdx[InputDevice.MouseWheel].Add(i); + break; + case ElementType.GamepadButton: + _respondersIdx[InputDevice.XInputButton].Add(i); + _respondersIdx[InputDevice.SDLButton].Add(i); + GamepadCodeType gamepadCode = elementViewModel.SelectedGamepadButton; + string gamepadButtonName = gamepadCode.ToString(); + string prefix = "HitBox_"; + if (gamepadButtonName.StartsWith(prefix)) + { + gamepadButtonName = gamepadButtonName.Substring(prefix.Length); + } + if (Enum.TryParse(gamepadButtonName, out FIPGamepadButtonflags flag)) + { + if (!_gamepadButtonDictionary.ContainsKey(flag)) + { + List idx = new List() { i }; + _gamepadButtonDictionary[flag] = idx; + } + else + _gamepadButtonDictionary[flag].Add(i); + } + else + { + throw new Exception($"{gamepadCode.ToString()} is not a valid enum name for Type: GamepadButtonFlags"); + } + break; + case ElementType.DPad: + _respondersIdx[InputDevice.XInputButton].Add(i); + _respondersIdx[InputDevice.SDLButton].Add(i); + break; + case ElementType.AnalogStick: + _respondersIdx[InputDevice.XInputButton].Add(i); + _respondersIdx[InputDevice.XInputStick].Add(i); + _respondersIdx[InputDevice.SDLButton].Add(i); + _respondersIdx[InputDevice.SDLStick].Add(i); + break; + case ElementType.GamepadTrigger: + _respondersIdx[InputDevice.XInputTrigger].Add(i); + _respondersIdx[InputDevice.SDLTrigger].Add(i); + elementViewModel.MakeCache(TRIGGER_CACHE_RANGE); + break; + default: + throw new Exception("Unknown Element Type"); + } + } + } + + + public int DPadShift(FIPGamepadButtonflags flags) + { + bool isUpPressed = (flags & FIPGamepadButtonflags.Up) == FIPGamepadButtonflags.Up; + bool isDownPressed = (flags & FIPGamepadButtonflags.Down) == FIPGamepadButtonflags.Down; + bool isLeftPressed = (flags & FIPGamepadButtonflags.Left) == FIPGamepadButtonflags.Left; + bool isRightPressed = (flags & FIPGamepadButtonflags.Right) == FIPGamepadButtonflags.Right; + if (isLeftPressed) + { + if (!isUpPressed && !isDownPressed) return 1; + else if (isUpPressed) return 5; + else if (isDownPressed) return 7; + else return 0; + } + else if (isRightPressed) + { + if (!isUpPressed && !isDownPressed) return 2; + else if (isUpPressed) return 6; + else if (isDownPressed) return 8; + else return 0; + } + else if (isUpPressed) return 3; + else if (isDownPressed) return 4; + else return 0; + } + + public void Handle(InputArgs e) + { + var respondersIdx = _respondersIdx[e.Device]; + if (respondersIdx.Count ==0) return; + for (int i = 0; i < respondersIdx.Count; i++) + { + int responderIdx = respondersIdx[i]; + ElementViewModel elementViewModel = ElementViewModels[responderIdx]; + var type = elementViewModel.ElementType; + if (type == ElementType.GamepadButton || type == ElementType.KeyboardButton || type == ElementType.MouseButton) continue; + switch (e.Device) + { + case InputDevice.Keyboard: + break; + case InputDevice.MouseWheel: + if (e.Value.HasValue) + { + int wheelValue = e.Value.Value; + elementViewModel.CurrentSourceIdx = wheelValue > 0 ? 2 : 3; + } + else if (e.MoveState.HasValue && e.MoveState == MoveState.StopMove) + elementViewModel.CurrentSourceIdx = 0; + break; + case InputDevice.MouseButton: + if (!e.MouseButton.HasValue) + throw new Exception("e.button should not be null for MouseButton events"); + if (!e.State.HasValue) + throw new Exception("e.State should not be null for button events"); + if (e.MouseButton.Value == MouseButtons.Middle && type == ElementType.MouseWheel) + { + elementViewModel.CurrentSourceIdx = (int)e.State.Value; + } + break; + case InputDevice.MouseMove: + if (e.MoveState == MoveState.StopMove) + { + //MouseMove Stopped + if ( !USE_CENTER_MOUSEMOVE || e.PreventMouseCentering) + { + elementViewModel.OffsetX = 0; + elementViewModel.OffsetY = 0; + //ElementTreeViewModel.ElementViewModels[responderIdx].RotateAngle = 0; + } + break; + } + if (e.ValueVector == null || e.ValueVector.Length != 4) + throw new Exception("e.ValueVector should not be null and have length of 4 for MouseMove events"); + double moveX = (double)e.ValueVector[0]; + double moveY = (double)e.ValueVector[1]; + double mouseX = (double)e.ValueVector[2]; + double mouseY = (double)e.ValueVector[3]; + double avgMouseMoveX = 0; + double avgMouseMoveY = 0; + double relativeMouseX = mouseX / RawInputParser.ScreenCenterX - 1; + double relativeMouseY = mouseY / RawInputParser.ScreenCenterY - 1; + _mouseMoveHistory.Enqueue((moveX, moveY)); + while (_mouseMoveHistory.Count > 12) + { + _mouseMoveHistory.Dequeue(); + } + var historyList = _mouseMoveHistory.ToList(); + if (historyList.Count > 2) + { + var xList = historyList.Select(item => item.X).OrderBy(x => x).ToList(); + var yList = historyList.Select(item => item.Y).OrderBy(y => y).ToList(); + xList.RemoveAt(0); + xList.RemoveAt(xList.Count - 1); + yList.RemoveAt(0); + yList.RemoveAt(yList.Count - 1); + avgMouseMoveX = xList.Average(); + avgMouseMoveY = yList.Average(); + } + else + { + avgMouseMoveX = historyList.Average(item => item.X); + avgMouseMoveY = historyList.Average(item => item.Y); + } + MouseMoveType mouseMoveType = elementViewModel.SelectedMouseMoveType; + if (mouseMoveType == MouseMoveType.Arrow) + { + double angleRadius = USE_CENTER_MOUSEMOVE ? Math.Atan2(relativeMouseY, relativeMouseX) : Math.Atan2(avgMouseMoveY, avgMouseMoveX); + double angle = angleRadius * 180 / Math.PI + 90; + elementViewModel.RotateAngle = angle; + break; + } + else if (mouseMoveType == MouseMoveType.Dot) + { + double radius = (double)elementViewModel.MouseRadius; + double offsetX = USE_CENTER_MOUSEMOVE ? + Math.Clamp(relativeMouseX / (MOUSE_SENSE / 20.0), -1, 1) * radius : + Math.Clamp((avgMouseMoveX / MOUSE_SENSE), -1, 1) * radius; + double offsetY = USE_CENTER_MOUSEMOVE ? + Math.Clamp(relativeMouseY / (MOUSE_SENSE / 20.0), -1, 1) * radius : + Math.Clamp((avgMouseMoveY / MOUSE_SENSE), -1, 1) * radius; + elementViewModel.OffsetX = (int)(offsetX * elementViewModel.Scale); + elementViewModel.OffsetY = (int)(offsetY * elementViewModel.Scale); + } + + break; + case InputDevice.XInputButton: + case InputDevice.SDLButton: + if (e.State == null) throw new Exception("e.State should not be null for button events"); + if (!e.GamepadUserIdx.HasValue) + throw new Exception("e.GamepadUserIdx should not be null for GamepadButton events"); + if (e.GamepadUserIdx.Value != elementViewModel.GamepadUserIndex) break; + switch (type) + { + case ElementType.AnalogStick: + if (!e.Flag.HasValue) + throw new Exception("e.flag should not be null for AnalogStick events"); + if (!e.State.HasValue) + throw new Exception("e.State should not be null for AnalogStick events"); + if ((e.Flag.Value == FIPGamepadButtonflags.LS && elementViewModel.SelectedSide == Side.Left)|| + (e.Flag.Value == FIPGamepadButtonflags.RS && elementViewModel.SelectedSide == Side.Right)) + elementViewModel.CurrentSourceIdx = (int)e.State.Value; + break; + case ElementType.DPad: + if (e.Flags.HasValue) + { + int shift = DPadShift(e.Flags.Value); + elementViewModel.CurrentSourceIdx = shift; + } + break; + } + break; + case InputDevice.XInputStick: + case InputDevice.SDLStick: + if (!e.GamepadUserIdx.HasValue) + throw new Exception("e.GamepadUserIdx should not be null for GamepadButton events"); + if (e.GamepadUserIdx.Value != elementViewModel.GamepadUserIndex) break; + if (e.ValueVector == null || e.ValueVector.Length != 2) + throw new Exception("e.ValueVector should not be null and have length of 2 for AnalogStick events"); + int vX = e.ValueVector[0]; + int vY = e.ValueVector[1]; + switch (type) + { + case ElementType.AnalogStick: + if ((e.Side == Side.Left && elementViewModel.SelectedSide == Side.Left)|| + (e.Side == Side.Right && elementViewModel.SelectedSide == Side.Right)) + { + int radius = elementViewModel.Radius; + int shiftX = (int)(((double)vX / (65535.0 / 2)) * (double)radius); + int shiftY = -(int)(((double)vY / (65535.0 / 2)) * (double)radius); + elementViewModel.OffsetX = shiftX; + elementViewModel.OffsetY = shiftY; + } + break; + } + break; + case InputDevice.XInputTrigger: + case InputDevice.SDLTrigger: + if (!e.GamepadUserIdx.HasValue) + throw new Exception("e.GamepadUserIdx should not be null for GamepadButton events"); + if (e.GamepadUserIdx.Value != elementViewModel.GamepadUserIndex) break; + if (!e.Value.HasValue) + throw new Exception("e.Value should not be null for GamepadTrigger events"); + int value = e.Value.Value; + switch (type) + { + case ElementType.GamepadTrigger: + if ((e.Side == Side.Left && elementViewModel.SelectedSide == Side.Left) || + (e.Side == Side.Right && elementViewModel.SelectedSide == Side.Right)) + { + int frameRange = TRIGGER_CACHE_RANGE; //0-9, total 10 frames + int threshold = (int)(0.1 * 255); + bool triggerMode = elementViewModel.IsTriggerMode; + int triggerFrame = (value >= threshold) ? frameRange : 0; + int direction = (int)elementViewModel.SelectedDirection; + int index = triggerMode? triggerFrame:Math.Min((int)((value / 255.0) * frameRange), frameRange); + //Debug.WriteLine($"value:{value}, index:{index}"); + elementViewModel.CurrentSourceIdx = (frameRange + 1) * (direction - 1) + index; + } + break; + } + break; + + case InputDevice.SDLGuide: + if (!e.GamepadUserIdx.HasValue) + throw new Exception("e.GamepadUserIdx should not be null for GamepadButton events"); + if (e.GamepadUserIdx.Value != elementViewModel.GamepadUserIndex) break; + if (!e.State.HasValue) + throw new Exception("e.State should not be null for GamepadGuide events"); + if (e.State.Value == ButtonState.Pressed) + elementViewModel.CurrentSourceIdx = 4; + else + elementViewModel.CurrentSourceIdx = (int)elementViewModel.GamepadUserIndex; + break; + + default: + throw new Exception("Unknown Input Device"); + } + } + + //单独处理 Button 类型事件 + switch (e.Device) + { + case InputDevice.MouseButton: + if (!e.MouseButton.HasValue) + throw new Exception("e.button should not be null for MouseButton events"); + if (!e.State.HasValue) + throw new Exception("e.State should not be null for MouseButton events"); + if (_mouseDictionary.ContainsKey(e.MouseButton.Value)) + { + for (int i = 0; i < _mouseDictionary[e.MouseButton.Value].Count; i++) + { + int idx = _mouseDictionary[e.MouseButton.Value][i]; + var elementViewModel = ElementViewModels[idx]; + SetButtonState(elementViewModel, e.State.Value); + } + } + break; + case InputDevice.MouseWheel: + case InputDevice.MouseMove: + case InputDevice.XInputTrigger: + case InputDevice.XInputStick: + case InputDevice.SDLStick: + case InputDevice.SDLTrigger: + case InputDevice.SDLGuide: + break; + case InputDevice.XInputButton: + case InputDevice.SDLButton: + if (!e.GamepadUserIdx.HasValue) + throw new Exception("e.GamepadUserIdx should not be null for GamepadButton events"); + if (!e.Flag.HasValue) + throw new Exception("e.flag should not be null for GamepadButton events"); + if (!e.State.HasValue) + throw new Exception("e.State should not be null for GamepadButton events"); + if (_gamepadButtonDictionary.ContainsKey(e.Flag.Value)) + { + for (int i = 0; i < _gamepadButtonDictionary[e.Flag.Value].Count; i++) + { + int idx = _gamepadButtonDictionary[e.Flag.Value][i]; + var elementViewModel = ElementViewModels[idx]; + if (e.GamepadUserIdx.Value != elementViewModel.GamepadUserIndex) break; + SetButtonState(elementViewModel, e.State.Value); + } + } + break; + case InputDevice.Keyboard: + if (!e.State.HasValue) + throw new Exception("e.State should not be null for Keyboard events"); + if (!e.KeyBoardMappingType.HasValue) + throw new Exception("e.KeyBoardMappingType should not be null for Keyboard events"); + switch (e.KeyBoardMappingType.Value) + { + case KeyBoardMappingType.Windows: + if (!e.Key.HasValue) + throw new Exception("e.Key should not be null for Keyboard events"); + if (_keyboardDictionary.ContainsKey(e.Key.Value)) + { + for (int i = 0; i < _keyboardDictionary[e.Key.Value].Count; i++) + { + int idx = _keyboardDictionary[e.Key.Value][i]; + var elementViewModel = ElementViewModels[idx]; + SetButtonState(elementViewModel, e.State.Value); + } + } + break; + case KeyBoardMappingType.BIOS: + if (!e.RawKey.HasValue) + throw new Exception("e.RawKey should not be null for Keyboard events"); + if (_rawKeyboardDictionary.ContainsKey(e.RawKey.Value)) + { + for (int i = 0; i < _rawKeyboardDictionary[e.RawKey.Value].Count; i++) + { + int idx = _rawKeyboardDictionary[e.RawKey.Value][i]; + var elementViewModel = ElementViewModels[idx]; + SetButtonState(elementViewModel, e.State.Value); + } + } + break; + default: + throw new Exception("Unknown KeyBoardMappingType"); + } + + break; + default: + throw new Exception("Unknown Input Device"); + } + } + + private void SetButtonState(ElementViewModel elementViewModel, ButtonState state) + { + if (!elementViewModel.UseStopwatch) + { + elementViewModel.CurrentSourceIdx = (int)state; + } + else if (state == ButtonState.Pressed) + { + elementViewModel.CurrentSourceIdx = 1; + _buttonRuning.Add(elementViewModel); + elementViewModel.Stopwatch.Restart(); + } + } + } +} diff --git a/FancyInput/Models/InputMacro.cs b/FancyInput/Models/InputMacro.cs new file mode 100644 index 0000000..bc1aa61 --- /dev/null +++ b/FancyInput/Models/InputMacro.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace FancyInput.Models +{ + public static class InputMacro + { + public static void MouseMoveAnimation(Vector2 from, Vector2 to, int steps, int duration) + { + Vector2 step = (to - from) / steps; + int stepDuration = Math.Max(1, duration / steps); + Task.Run(() => + { + InputGenerator.SendMouseMove((int)from.X, (int)from.Y); + for (int i = 1; i < steps + 1; i++) + { + Thread.Sleep(stepDuration); + Vector2 currentPosition = from + step * i; + InputGenerator.SendMouseMove((int)currentPosition.X, (int)currentPosition.Y); + } + }); + } + + public static void MouseWheelAnimation(bool isUp, int steps, int duration) + { + int stepDuration = Math.Max(1, duration / steps); + Task.Run(() => + { + for (int i = 0; i < steps; i++) + { + Thread.Sleep(stepDuration); + InputGenerator.SendMouseWheel(isUp? 120 : -120); + } + }); + } + + public static void GamepadTriggerAnimation(Side side,int from, int to, int steps, int duration) + { + int step = (to - from) / steps; + int stepDuration = Math.Max(1, duration / steps); + Task.Run(() => + { + InputGenerator.SendXInpuTrigger(from, side); + for (int i = 1; i < steps + 1; i++) + { + Thread.Sleep(stepDuration); + int currentValue = from + step * i; + InputGenerator.SendXInpuTrigger(currentValue, side); + } + }); + } + + public static void GamepadstickAnimation(Side side, Vector2 from, Vector2 to, int steps, int duration) + { + Vector2 step = (to - from) / steps; + int stepDuration = Math.Max(1, duration / steps); + Task.Run(() => + { + InputGenerator.SendXInputStick((int)from.X, (int)from.Y, side); + for (int i = 1; i < steps + 1; i++) + { + Thread.Sleep(stepDuration); + Vector2 currentPosition = from + step * i; + InputGenerator.SendXInputStick((int)currentPosition.X, (int)currentPosition.Y, side); + } + }); + } + + public static void GamepadButtonPress(FIPGamepadButtonflags button, int Duration) + { + Task.Run(() => + { + InputGenerator.SendXInputButton(button,ButtonState.Pressed); + Thread.Sleep(Duration); + InputGenerator.SendXInputButton(button, ButtonState.Released); + }); + } + + public static void KeyPress(FipRawKeys key, int Duration) + { + Task.Run(() => + { + InputGenerator.SendKey(key,ButtonState.Pressed); + Thread.Sleep(Duration); + InputGenerator.SendKey(key, ButtonState.Released); + }); + } + } + + +} diff --git a/FancyInput/Models/InputParser.cs b/FancyInput/Models/InputParser.cs new file mode 100644 index 0000000..8413031 --- /dev/null +++ b/FancyInput/Models/InputParser.cs @@ -0,0 +1,803 @@ +using SharpDX.XInput; +using System.Diagnostics; +using System.Windows.Forms; +using System.Windows.Threading; +using SDL3; +using Window = System.Windows.Window; + +namespace FancyInput.Models +{ + + + public class InputArgs : EventArgs + { + public InputDevice Device { get; set; } + public string Tag { get; set; } = ""; + public ButtonState? State { get; set; } + public MoveState? MoveState { get; set; } + + public int? Value { get; set; } + public int[]? ValueVector { get; set; } + public bool PreventMouseCentering { get; set; } = false; + public FipKeys? Key { get; set; } + public FipRawKeys? RawKey { get; set; } + public KeyBoardMappingType? KeyBoardMappingType { get; set; } + public Side? Side { get; set; } + public Message? Message { get; set; } + + // 保留兼容字段 + public FIPGamepadButtonflags? Flag { get; set; } + public FIPGamepadButtonflags? Flags { get; set; } + public MouseButtons? MouseButton { get; set; } + public GamepadUserIdx? GamepadUserIdx { get; set; } + + public override string ToString() + { + var sb = new System.Text.StringBuilder(); + sb.Append($"Device: {Device}"); + if (Flags.HasValue) sb.Append($", Flags: {Flags}"); + if (State.HasValue) sb.Append($", State: {State}"); + if (MoveState.HasValue) sb.Append($", MoveState: {MoveState}"); + if (Value.HasValue) sb.Append($", Value: {Value}"); + if (ValueVector != null) sb.Append($", ValueVector: [{string.Join(", ", ValueVector)}]"); + if (Key.HasValue) sb.Append($", Key: {Key}"); + if (RawKey.HasValue) sb.Append($", RawKey: {RawKey}"); + if (KeyBoardMappingType.HasValue) sb.Append($", KeyBoardMappingType: {KeyBoardMappingType}"); + if (Side.HasValue) sb.Append($", Side: {Side}"); + if (MouseButton.HasValue) sb.Append($", MouseButton: {MouseButton}"); + if (GamepadUserIdx.HasValue) sb.Append($", GamepadUserIdx: {GamepadUserIdx}"); + if (Message.HasValue) sb.Append($", Message: {Message}"); + return sb.ToString(); + } + } + + public sealed class InputParser : IDisposable + { + private Window? _hookWindow; + private RawInputParser? _globalHook; + + public bool NumPadAsArrow { get; set; } = false; + public KeyBoardMappingType KeyBoardMappingType { get; set; } = KeyBoardMappingType.BIOS; + + private readonly DispatcherTimer _timer; + private readonly Stopwatch _stopwatch; + private readonly Stopwatch _mouseMoveStopwatch; + + public event EventHandler? GetInput; + + + private int _mousePosX = 0; + private int _mousePosY = 0; + private int _lastMouseX = 0; + private int _lastMouseY = 0; + + private bool _preventMouseCentering = false; + public bool PreventMouseCentering + { + get => _preventMouseCentering; + set + { + _preventMouseCentering = value; + if (_globalHook == null) return; + switch (value) + { + case true: + _globalHook.MouseMove -= GlobalHook_MouseMove; + _globalHook.RawMouseMove -= GlobalHook_RawMouseMove; + _globalHook.RawMouseMove += GlobalHook_RawMouseMove; + break; + case false: + _globalHook.MouseMove -= GlobalHook_MouseMove; + _globalHook.RawMouseMove -= GlobalHook_RawMouseMove; + _globalHook.MouseMove += GlobalHook_MouseMove; + break; + } + } + } + + private bool _isWheeling = false; + private bool _isMoving = false; + private bool _isDisposed = false; + public bool IsDisposed => _isDisposed; + + private double _timerInterval = 16.67; + public double TimerInterval + { + get => _timerInterval; + set + { + if (value < 5) value = 5; + if (value > 1000) value = 1000; + _timerInterval = value; + if (_isDisposed) return; + _timer.Interval = TimeSpan.FromMilliseconds(_timerInterval); + } + } + + public GamepadBackend Backend { get; private set; } + + // ==================== XInput State ==================== + private readonly List _xControllers = new(); + private readonly List _xLastLT = new(); + private readonly List _xLastRT = new(); + private readonly List _xLastLX = new(); + private readonly List _xLastLY = new(); + private readonly List _xLastRX = new(); + private readonly List _xLastRY = new(); + private readonly List _xLastButtons = new(); + + // ==================== SDL State ==================== + private readonly Dictionary _sdlGamepads = new(); + private readonly Dictionary _sdlLastState = new(); + private bool _sdlInitialized = false; + + private const int XINPUT_STICK_DELTA_THRESHOLD = 20; + private const int SDL_STICK_DELTA_THRESHOLD = 2000; + private const int SDL_TRIGGER_DELTA_THRESHOLD = 5; + + private sealed class SdlPadState + { + public readonly bool[] Buttons = new bool[(int)SDL.GamepadButton.Count]; + public int LeftX; + public int LeftY; + public int RightX; + public int RightY; + public int LeftTrigger; + public int RightTrigger; + public FIPGamepadButtonflags XFlags = FIPGamepadButtonflags.None; + } + + public InputParser(GamepadBackend backend = GamepadBackend.SDL) + { + Backend = backend; + + InitKeyboardAndMouse(); + + // Polling gamepad state at Send priority reduces input latency under heavy render load (e.g., animated GIF overlays). + _timer = new DispatcherTimer(DispatcherPriority.Send) { Interval = TimeSpan.FromMilliseconds(_timerInterval) }; + _timer.Tick += Timer_Tick; + _timer.Start(); + + _stopwatch = new Stopwatch(); + _mouseMoveStopwatch = new Stopwatch(); + + if (Backend == GamepadBackend.XInput) + { + InitXInput(); + } + } + + public void Dispose() + { + if (_isDisposed) return; + + StopTicker(); + UnhookKeyboardAndMouse(); + ShutdownGamepadBackend(); + + _hookWindow?.Close(); + _isDisposed = true; + GC.SuppressFinalize(this); + } + + public void SetGamepadBackend(GamepadBackend backend) + { + if (_isDisposed) return; + if (Backend == backend) return; + + ShutdownGamepadBackend(); + Backend = backend; + + if (Backend == GamepadBackend.XInput) + { + InitXInput(); + } + } + + public void StartTicker() + { + if (_isDisposed) return; + _timer.Tick -= Timer_Tick; + _timer.Tick += Timer_Tick; + _timer.Start(); + } + + public void StopTicker() + { + if (_isDisposed) return; + _timer.Tick -= Timer_Tick; + _timer.Stop(); + } + + private void Timer_Tick(object? sender, EventArgs e) + { + if (_isDisposed) return; + + // wheel stop + if (_isWheeling && _stopwatch.ElapsedMilliseconds > 200) + { + _isWheeling = false; + _stopwatch.Stop(); + //_stopwatch.Reset(); + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.MouseWheel, + Tag = "MouseWheelStopped", + MoveState = MoveState.StopMove, + }); + } + + // mouse move stop + if (_isMoving && _mouseMoveStopwatch.ElapsedMilliseconds > 100) + { + _isMoving = false; + _mouseMoveStopwatch.Stop(); + //_stopwatch.Reset(); + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.MouseMove, + Tag = "MouseMoveStopped", + MoveState = MoveState.StopMove, + PreventMouseCentering = _preventMouseCentering + }); + if (_globalHook!=null) + { + _globalHook.MouseX = RawInputParser.ScreenCenterX; + _globalHook.MouseY = RawInputParser.ScreenCenterY; + } + } + + if (Backend == GamepadBackend.XInput) + { + PollXInput(); + } + else + { + PollSdlInput(); + } + } + + // ==================== Keyboard / Mouse ==================== + private void InitKeyboardAndMouse() + { + _hookWindow = new Window + { + Visibility = System.Windows.Visibility.Hidden, + Width = 0, + Height = 0 + }; + _hookWindow.Show(); + + _mousePosX = Cursor.Position.X; + _mousePosY = Cursor.Position.Y; + _lastMouseX = _mousePosX; + _lastMouseY = _mousePosY; + + IntPtr handle = new System.Windows.Interop.WindowInteropHelper(_hookWindow).Handle; + _globalHook = new RawInputParser(handle); + + _globalHook.KeyDown += GlobalHook_KeyDown; + _globalHook.KeyUp += GlobalHook_KeyUp; + _globalHook.MouseDown += GlobalHook_MouseDown; + _globalHook.MouseUp += GlobalHook_MouseUp; + _globalHook.MouseWheel += GlobalHook_MouseWheel; + PreventMouseCentering = false; + } + + + private void UnhookKeyboardAndMouse() + { + if (_globalHook == null) return; + + _globalHook.KeyDown -= GlobalHook_KeyDown; + _globalHook.KeyUp -= GlobalHook_KeyUp; + _globalHook.MouseDown -= GlobalHook_MouseDown; + _globalHook.MouseUp -= GlobalHook_MouseUp; + _globalHook.MouseWheel -= GlobalHook_MouseWheel; + _globalHook.MouseMove -= GlobalHook_MouseMove; + _globalHook.RawMouseMove -= GlobalHook_RawMouseMove; + _globalHook.Dispose(); + _globalHook = null; + } + + private void GlobalHook_KeyDown(object? sender, RawInputKeyEventArgs e) + { + if (_isDisposed) return; + + FipRawKeys rawKey = e.RawKey; + if ( KeyBoardMappingType == KeyBoardMappingType.Windows && !e.IsNumLockOn && + RawInputParser.NumLockAffectedFipRawKeys.Contains(rawKey) && !NumPadAsArrow ) return; + + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.Keyboard, + Tag = e.Key.ToString(), + State = ButtonState.Pressed, + Key = e.Key, + RawKey = rawKey, + KeyBoardMappingType = KeyBoardMappingType + }); + } + + private void GlobalHook_KeyUp(object? sender, RawInputKeyEventArgs e) + { + if (_isDisposed) return; + + FipRawKeys rawKey = e.RawKey; + if (!e.IsNumLockOn && + RawInputParser.NumLockAffectedFipRawKeys.Contains(rawKey) && + !NumPadAsArrow && + KeyBoardMappingType == KeyBoardMappingType.Windows) return; + + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.Keyboard, + Tag = e.Key.ToString(), + State = 0, + Key = e.Key, + RawKey = rawKey, + KeyBoardMappingType = KeyBoardMappingType + }); + } + + private void GlobalHook_MouseDown(object? sender, MouseEventArgs e) + { + if (_isDisposed) return; + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.MouseButton, + Tag = e.Button.ToString(), + State = ButtonState.Pressed, + MouseButton = e.Button + }); + } + + private void GlobalHook_MouseUp(object? sender, MouseEventArgs e) + { + if (_isDisposed) return; + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.MouseButton, + Tag = e.Button.ToString(), + State = 0, + MouseButton = e.Button + }); + } + + private void GlobalHook_MouseWheel(object? sender, MouseEventArgs e) + { + if (_isDisposed) return; + MoveState moveState = _isWheeling ? MoveState.Moving : MoveState.StartMove; + _stopwatch.Restart(); + _isWheeling = true; + + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.MouseWheel, + Tag = "MouseWheel", + Value = e.Delta, + MoveState = moveState + }); + } + + private void GlobalHook_MouseMove(object? sender, MouseEventArgs e) + { + if (_isDisposed || _globalHook == null) return; + _mousePosX = e.X; + _mousePosY = e.Y; + int deltaX = _mousePosX - _lastMouseX; + int deltaY = _mousePosY - _lastMouseY; + if (deltaX != 0 || deltaY != 0) + { + MoveState moveState = _isMoving ? MoveState.Moving : MoveState.StartMove; + _isMoving = true; + _mouseMoveStopwatch.Restart(); + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.MouseMove, + Tag = "MouseMove", + MoveState = moveState, + ValueVector = new[] { deltaX, deltaY, e.X, e.Y } + }); + } + + _lastMouseX = _mousePosX; + _lastMouseY = _mousePosY; + + } + + + private void GlobalHook_RawMouseMove(object? sender, RawInputMouseMoveEventArgs e) + { + if (_isDisposed || _globalHook == null) return; + _mousePosX = e.X; + _mousePosY = e.Y; + if (e.DeltaX != 0 || e.DeltaY != 0) + { + MoveState moveState = _isMoving ? MoveState.Moving : MoveState.StartMove; + _isMoving = true; + _mouseMoveStopwatch.Restart(); + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.MouseMove, + Tag = "MouseMove", + MoveState = moveState, + ValueVector = new[] { e.DeltaX, e.DeltaY, e.X, e.Y } + }); + } + _lastMouseX = _mousePosX; + _lastMouseY = _mousePosY; + } + + // ==================== XInput Backend ==================== + private void InitXInput() + { + _xControllers.Clear(); + _xLastLT.Clear(); + _xLastRT.Clear(); + _xLastLX.Clear(); + _xLastLY.Clear(); + _xLastRX.Clear(); + _xLastRY.Clear(); + _xLastButtons.Clear(); + + for (int i = 0; i < 4; i++) + { + _xControllers.Add(new Controller((UserIndex)i)); + _xLastLT.Add(0); + _xLastRT.Add(0); + _xLastLX.Add(0); + _xLastLY.Add(0); + _xLastRX.Add(0); + _xLastRY.Add(0); + _xLastButtons.Add(FIPGamepadButtonflags.None); + } + } + + private void PollXInput() + { + for (int i = 0; i < _xControllers.Count; i++) + { + Controller controller = _xControllers[i]; + if (!controller.IsConnected) continue; + + GamepadUserIdx userIdx = (GamepadUserIdx)i; + var state = controller.GetState(); + var buttons = state.Gamepad.Buttons; + FIPGamepadButtonflags btns = (FIPGamepadButtonflags)buttons; + if (btns != _xLastButtons[i]) + { + foreach (FIPGamepadButtonflags flag in Enum.GetValues(typeof(FIPGamepadButtonflags))) + { + if (flag == FIPGamepadButtonflags.None) continue; + + ButtonState oldPressed = (_xLastButtons[i] & flag) == flag ? ButtonState.Pressed : ButtonState.Released; + ButtonState nowPressed = (btns & flag) == flag ? ButtonState.Pressed : ButtonState.Released; + //FIPGamepadButtonflags + if (oldPressed != nowPressed) + { + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.XInputButton, + GamepadUserIdx = userIdx, + Tag = flag.ToString(), + State = nowPressed, + Flag = flag, + Flags = btns + }); + } + } + + _xLastButtons[i] = btns; + } + + byte lt = state.Gamepad.LeftTrigger; + byte rt = state.Gamepad.RightTrigger; + + if (lt != _xLastLT[i]) + { + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.XInputTrigger, + GamepadUserIdx = userIdx, + Tag = "LeftTrigger", + Side = Side.Left, + Value = lt + }); + _xLastLT[i] = lt; + } + + if (rt != _xLastRT[i]) + { + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.XInputTrigger, + GamepadUserIdx = userIdx, + Tag = "RightTrigger", + Side = Side.Right, + Value = rt + }); + _xLastRT[i] = rt; + } + + short lx = state.Gamepad.LeftThumbX; + short ly = state.Gamepad.LeftThumbY; + short rx = state.Gamepad.RightThumbX; + short ry = state.Gamepad.RightThumbY; + + if (Math.Abs(lx - _xLastLX[i]) > XINPUT_STICK_DELTA_THRESHOLD || + Math.Abs(ly - _xLastLY[i]) > XINPUT_STICK_DELTA_THRESHOLD) + { + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.XInputStick, + GamepadUserIdx = userIdx, + Tag = "LeftThumb", + Side = Side.Left, + ValueVector = [lx, ly] + }); + + _xLastLX[i] = lx; + _xLastLY[i] = ly; + } + + if (Math.Abs(rx - _xLastRX[i]) > XINPUT_STICK_DELTA_THRESHOLD || + Math.Abs(ry - _xLastRY[i]) > XINPUT_STICK_DELTA_THRESHOLD) + { + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.XInputStick, + GamepadUserIdx = userIdx, + Tag = "RightThumb", + Side = Side.Right, + ValueVector = [rx, ry] + }); + + _xLastRX[i] = rx; + _xLastRY[i] = ry; + } + } + } + + // ==================== SDL Backend ==================== + private bool EnsureSdl3() + { + if (_sdlInitialized) return true; + + if (!SDL.Init(SDL.InitFlags.Gamepad)) + return false; + + _sdlInitialized = true; + return true; + } + + private void PollSdlInput() + { + if (!EnsureSdl3()) return; + + SDL.UpdateGamepads(); + var ids = SDL.GetGamepads(out int count); + if (ids == null || count <= 0) return; + + HashSet activeIds = new(); + foreach (uint id in ids) activeIds.Add(id); + + foreach (uint id in ids) + { + if (!_sdlGamepads.TryGetValue(id, out IntPtr pad) || pad == IntPtr.Zero) + { + pad = SDL.OpenGamepad(id); + if (pad == IntPtr.Zero) continue; + _sdlGamepads[id] = pad; + } + + if (!_sdlLastState.TryGetValue(id, out SdlPadState? last)) + { + last = new SdlPadState(); + _sdlLastState[id] = last; + } + + int playerIndex = SDL.GetGamepadPlayerIndex(pad); + GamepadUserIdx? userIdx = (playerIndex >= 0 && playerIndex <= 3) + ? (GamepadUserIdx)playerIndex + : (GamepadUserIdx?)null; + + // buttons + for (int bi = 0; bi < (int)SDL.GamepadButton.Count; bi++) + { + SDL.GamepadButton btn = (SDL.GamepadButton)bi; + bool nowPressed = SDL.GetGamepadButton(pad, btn); + bool oldPressed = last.Buttons[bi]; + if (nowPressed == oldPressed) continue; + + last.Buttons[bi] = nowPressed; + + if (TryMapSdlButtonToSDipButton(btn, out FIPGamepadButtonflags mappedFlag)) + { + if (nowPressed) last.XFlags |= mappedFlag; + else last.XFlags &= ~mappedFlag; + } + + if (btn == SDL.GamepadButton.Guide) + { + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.SDLGuide, + GamepadUserIdx = userIdx, + Tag = "Guide", + State = nowPressed ? ButtonState.Pressed : ButtonState.Released + }); + } + else + { + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.SDLButton, + GamepadUserIdx = userIdx, + Tag = btn.ToString(), + State = nowPressed ? ButtonState.Pressed : ButtonState.Released, + Flag = TryMapSdlButtonToSDipButton(btn, out var flag) ? flag : null, + Flags = last.XFlags + }); + } + } + + const int SDL_AXIS_MAX = 32767; + const int SDL_AXIS_MIN = -32768; + const int SDL_TRIGGER_MAX = 32767; + const int SDL_TRIGGER_MIN = 0; + + // left stick + int lx_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.LeftX); + int ly_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.LeftY); + int lx = Math.Clamp(lx_raw, SDL_AXIS_MIN, SDL_AXIS_MAX); + int ly = -Math.Clamp(ly_raw, SDL_AXIS_MIN, SDL_AXIS_MAX); // 反转Y轴 + + if (Math.Abs(lx - last.LeftX) > SDL_STICK_DELTA_THRESHOLD || + Math.Abs(ly - last.LeftY) > SDL_STICK_DELTA_THRESHOLD) + { + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.SDLStick, + GamepadUserIdx = userIdx, + Tag = "LeftThumb", + Side = Side.Left, + ValueVector = [lx, ly] + }); + last.LeftX = lx; + last.LeftY = ly; + } + + // right stick + int rx_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.RightX); + int ry_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.RightY); + int rx = Math.Clamp(rx_raw, SDL_AXIS_MIN, SDL_AXIS_MAX); + int ry = -Math.Clamp(ry_raw, SDL_AXIS_MIN, SDL_AXIS_MAX); // 反转Y轴 + + if (Math.Abs(rx - last.RightX) > SDL_STICK_DELTA_THRESHOLD || + Math.Abs(ry - last.RightY) > SDL_STICK_DELTA_THRESHOLD) + { + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.SDLStick, + GamepadUserIdx = userIdx, + Tag = "RightThumb", + Side = Side.Right, + ValueVector = [rx, ry] + }); + last.RightX = rx; + last.RightY = ry; + } + + // triggers + int lt_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.LeftTrigger); + int rt_raw = SDL.GetGamepadAxis(pad, SDL.GamepadAxis.RightTrigger); + byte lt = (byte)(Math.Clamp(lt_raw, SDL_TRIGGER_MIN, SDL_TRIGGER_MAX) * 255 / SDL_TRIGGER_MAX); + byte rt = (byte)(Math.Clamp(rt_raw, SDL_TRIGGER_MIN, SDL_TRIGGER_MAX) * 255 / SDL_TRIGGER_MAX); + + if (Math.Abs(lt - last.LeftTrigger) > SDL_TRIGGER_DELTA_THRESHOLD) + { + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.SDLTrigger, + GamepadUserIdx = userIdx, + Tag = "LeftTrigger", + Side = Side.Left, + Value = lt + }); + last.LeftTrigger = lt; + } + + if (Math.Abs(rt - last.RightTrigger) > SDL_TRIGGER_DELTA_THRESHOLD) + { + GetInput?.Invoke(this, new InputArgs + { + Device = InputDevice.SDLTrigger, + GamepadUserIdx = userIdx, + Tag = "RightTrigger", + Side = Side.Right, + Value = rt + }); + last.RightTrigger = rt; + } + + } + + // remove disconnected + List toRemove = new(); + foreach (var kv in _sdlGamepads) + { + if (!activeIds.Contains(kv.Key)) + { + SDL.CloseGamepad(kv.Value); + toRemove.Add(kv.Key); + } + } + + foreach (uint id in toRemove) + { + _sdlGamepads.Remove(id); + _sdlLastState.Remove(id); + } + } + + private static bool TryMapSdlButtonToSDipButton(SDL.GamepadButton btn, out FIPGamepadButtonflags flag) + { + flag = FIPGamepadButtonflags.None; + switch (btn) + { + case SDL.GamepadButton.South: flag = FIPGamepadButtonflags.A; return true; + case SDL.GamepadButton.East: flag = FIPGamepadButtonflags.B; return true; + case SDL.GamepadButton.West: flag = FIPGamepadButtonflags.X; return true; + case SDL.GamepadButton.North: flag = FIPGamepadButtonflags.Y; return true; + case SDL.GamepadButton.Back: flag = FIPGamepadButtonflags.Select; return true; + case SDL.GamepadButton.Start: flag = FIPGamepadButtonflags.Start; return true; + case SDL.GamepadButton.LeftShoulder: flag = FIPGamepadButtonflags.LB; return true; + case SDL.GamepadButton.RightShoulder: flag = FIPGamepadButtonflags.RB; return true; + case SDL.GamepadButton.LeftStick: flag = FIPGamepadButtonflags.LS; return true; + case SDL.GamepadButton.RightStick: flag = FIPGamepadButtonflags.RS; return true; + case SDL.GamepadButton.DPadUp: flag = FIPGamepadButtonflags.Up; return true; + case SDL.GamepadButton.DPadDown: flag = FIPGamepadButtonflags.Down; return true; + case SDL.GamepadButton.DPadLeft: flag = FIPGamepadButtonflags.Left; return true; + case SDL.GamepadButton.DPadRight: flag = FIPGamepadButtonflags.Right; return true; + default: return false; + } + } + + private void ShutdownSdl3() + { + foreach (var kv in _sdlGamepads) + { + SDL.CloseGamepad(kv.Value); + } + + _sdlGamepads.Clear(); + _sdlLastState.Clear(); + + if (_sdlInitialized) + { + SDL.Quit(); + _sdlInitialized = false; + } + } + + private void ShutdownGamepadBackend() + { + if (Backend == GamepadBackend.SDL) + { + ShutdownSdl3(); + } + else + { + _xControllers.Clear(); + _xLastLT.Clear(); + _xLastRT.Clear(); + _xLastLX.Clear(); + _xLastLY.Clear(); + _xLastRX.Clear(); + _xLastRY.Clear(); + _xLastButtons.Clear(); + } + } + } +} \ No newline at end of file diff --git a/FancyInput/Models/MacroActionPlayer.cs b/FancyInput/Models/MacroActionPlayer.cs new file mode 100644 index 0000000..c606d61 --- /dev/null +++ b/FancyInput/Models/MacroActionPlayer.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using FancyInput.ViewModels; +namespace FancyInput.Models +{ + public enum CommandBackEnd + { + Cmd, + PowerShell + } + + public static class MacroActionPlayer + { + public static async Task Run(this MacroAction action, + Action stdoutCallback, + Action stderrCallback, + string? workingDirectory = null, + CancellationToken cancellationToken = default, + CommandBackEnd backEnd = CommandBackEnd.Cmd) + { + switch (action) + { + case MacroActionCommand cmdAction: + var (exitCode, _) = await RunCommandAction(cmdAction, stdoutCallback, stderrCallback, workingDirectory, cancellationToken, backEnd); + return exitCode; + case MacroActionKeyboardButton keyboardAction: + return await RunKeyboardButtonAction(keyboardAction, stderrCallback, cancellationToken); + case MacroActionMouseButton mouseButtonAction: + return await RunMouseButtonAction(mouseButtonAction, stderrCallback, cancellationToken); + case MacroActionMouseMove mouseMoveAction: + return await RunMouseMoveAction(mouseMoveAction, stderrCallback, cancellationToken); + case MacroActionMouseWheel mouseWheelAction: + return await RunMouseWheelAction(mouseWheelAction, stderrCallback, cancellationToken); + case MacroActionDelay delayAction: + return await RunDelayAction(delayAction, stderrCallback, cancellationToken); + default: + stderrCallback?.Invoke($"未知的动作类型: {action.GetType().Name}"); + return -1; + } + } + public static async Task<(int ExitCode, System.Diagnostics.Process? Process)> RunCommandAction( + MacroActionCommand action, + Action stdoutCallback, + Action stderrCallback, + string? workingDirectory = null, + CancellationToken cancellationToken = default, + CommandBackEnd backEnd = CommandBackEnd.Cmd) + { + string command = action.Command; + bool waitUntilExit = action.WaitUntilExit; + System.Diagnostics.Process? process = null; + bool shouldDispose = false; + try + { + if (action.PreWait > 0) + await Task.Delay(action.PreWait, cancellationToken); + process = new System.Diagnostics.Process(); + if (backEnd == CommandBackEnd.PowerShell) + { + process.StartInfo.FileName = "powershell.exe"; + process.StartInfo.Arguments = $"-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"{command}\""; + } + else + { + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = $"/C {command}"; + } + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + if (!string.IsNullOrEmpty(workingDirectory)) + { + process.StartInfo.WorkingDirectory = workingDirectory; + } + process.OutputDataReceived += (sender, e) => + { + if (e.Data != null) stdoutCallback?.Invoke(e.Data); + }; + process.ErrorDataReceived += (sender, e) => + { + if (e.Data != null) stderrCallback?.Invoke(e.Data); + }; + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + int exitCode = 0; + if (waitUntilExit) + { + shouldDispose = true; + await process.WaitForExitAsync(cancellationToken); + exitCode = process.ExitCode; + } + if (action.PostWait > 0) + await Task.Delay(action.PostWait, cancellationToken); + return (exitCode, shouldDispose ? null : process); + } + catch (OperationCanceledException) + { + if (process != null && !process.HasExited) + { + try { process.Kill(); } catch { } + } + shouldDispose = true; + return (-2, null); + } + catch (Exception ex) + { + stderrCallback?.Invoke($"执行命令出错: {ex.Message}"); + shouldDispose = true; + return (-1, null); + } + finally + { + if (shouldDispose) + process?.Dispose(); + } + } + + public static async Task RunKeyboardButtonAction( + MacroActionKeyboardButton action, + Action stderrCallback, + CancellationToken cancellationToken = default) + { + FipRawKeys key = action.Key; + bool downSent = false; + bool upSent = false; + bool down = action.Down; + bool up = action.Up; + int duration = action.Duration; + int repeat = action.RepeatCount>1&&down&&up ? action.RepeatCount : 1; // 如果没有 Down 或 Up,则不重复 + try + { + for (int i = 0; i < repeat; i++) + { + downSent = false; + upSent = false; + if (action.PreWait > 0) + await Task.Delay(action.PreWait, cancellationToken); + if (down) + { + InputGenerator.SendKey(key, ButtonState.Pressed); + downSent = true; + } + await Task.Delay(duration, cancellationToken); + if (up) + { + InputGenerator.SendKey(key, ButtonState.Released); + upSent = true; + } + if (action.PostWait > 0) + await Task.Delay(action.PostWait, cancellationToken); + } + + return 0; // Success + } + catch (OperationCanceledException) + { + // Down 已发送、Up 尚未发送、且原本计划了 Up → 补发 Release + if (downSent && !upSent && action.Up) + { + try { InputGenerator.SendKey(action.Key, ButtonState.Released); } catch { } + } + return -2; + } + catch (Exception ex) + { + stderrCallback?.Invoke($"执行键盘动作出错: {ex.Message}"); + return -1; + } + } + + public static async Task RunMouseButtonAction( + MacroActionMouseButton action, + Action stderrCallback, + CancellationToken cancellationToken = default) + { + MouseButtons button = action.Button; + bool down = action.Down; + bool up = action.Up; + int duration = action.Duration; + bool downSent = false; + bool upSent = false; + int repeat = action.RepeatCount > 1 && action.Down && action.Up ? action.RepeatCount : 1; // 如果没有 Down 或 Up,则不重复 + try + { + for ( int i = 0; i 0) + await Task.Delay(action.PreWait, cancellationToken); + if (down) + { + InputGenerator.SendMouseButton(button, ButtonState.Pressed); + downSent = true; + } + await Task.Delay(duration, cancellationToken); + if (up) + { + InputGenerator.SendMouseButton(button, ButtonState.Released); + upSent = true; + } + if (action.PostWait > 0) + await Task.Delay(action.PostWait, cancellationToken); + } + return 0; // Success + } + catch (OperationCanceledException) + { + // Down 已发送、Up 尚未发送、且原本计划了 Up → 补发 Release + if (downSent && !upSent && action.Up) + { + try { InputGenerator.SendMouseButton(action.Button, ButtonState.Released); } catch { } + } + return -2; + } + catch (Exception ex) + { + stderrCallback?.Invoke($"执行鼠标动作出错: {ex.Message}"); + return -1; + } + } + + public static async Task RunMouseMoveAction( + MacroActionMouseMove action, + Action stderrCallback, + CancellationToken cancellationToken = default) + { + try + { + if (action.PreWait > 0) + await Task.Delay(action.PreWait, cancellationToken); + int FromX = action.FromX; + int FromY = action.FromY; + int ToX = action.ToX; + int ToY = action.ToY; + int steps = Math.Max(1, action.Steps); + int duration = Math.Max(0, action.Duration); + int stepDelay = (int)Math.Max(1, (double)duration / steps); + MousMoveMode mouseMoveMode = action.MoveMode; + if (steps == 1 && mouseMoveMode == MousMoveMode.Absolute) + InputGenerator.SendMouseMove(ToX, ToY, absolute:true); + else if (steps == 1 && mouseMoveMode == MousMoveMode.Relative) + InputGenerator.SendMouseMove(ToX - FromX, ToY - FromY, absolute: false); + else if (mouseMoveMode == MousMoveMode.Absolute) + { + InputGenerator.SendMouseMove(FromX, FromY, absolute: true); + for (int i = 1; i < steps+1; i++) + { + double ratio = (double)i / steps; + int x = (int)(FromX * (1 - ratio) + ToX * ratio); + int y = (int)(FromY * (1 - ratio) + ToY * ratio); + await Task.Delay(stepDelay, cancellationToken); + InputGenerator.SendMouseMove(x, y, absolute: true); + } + } + else if (mouseMoveMode == MousMoveMode.Relative) + { + int Dx = ToX - FromX; + int Dy = ToY - FromY; + int dx = (int)(Dx / (double)steps); + int dy = (int)(Dy / (double)steps); + for (int i = 1; i < steps + 1; i++) + { + await Task.Delay(stepDelay, cancellationToken); + // 最后一步补偿整数截断的累积误差 + if (i == steps) + InputGenerator.SendMouseMove(Dx - dx * (steps - 1), Dy - dy * (steps - 1), absolute: false); + else + InputGenerator.SendMouseMove(dx, dy, absolute: false); + } + } + + if (action.PostWait > 0) + await Task.Delay(action.PostWait, cancellationToken); + return 0; // Success + } + catch (OperationCanceledException) + { + return -2; + } + catch (Exception ex) + { + stderrCallback?.Invoke($"执行鼠标移动动作出错: {ex.Message}"); + return -1; + } + } + + public static async Task RunMouseWheelAction( + MacroActionMouseWheel action, + Action stderrCallback, + CancellationToken cancellationToken = default) + { + try + { + if (action.PreWait > 0) + await Task.Delay(action.PreWait, cancellationToken); + MouseWheelDirection direction = action.WheelDirection; + int delta = direction == MouseWheelDirection.Up ? 120 : -120; + int steps = action.Steps; + int duration = Math.Max(0, action.Duration); + int stepDelay = (int)Math.Max(1, (double)duration / steps); + if (steps == 1) + { + InputGenerator.SendMouseWheel(delta); + } + else + { + for (int i = 0; i < steps; i++) + { + await Task.Delay(stepDelay, cancellationToken); + InputGenerator.SendMouseWheel(delta); + } + } + if (action.PostWait > 0) + await Task.Delay(action.PostWait, cancellationToken); + return 0; + } + catch (OperationCanceledException) + { + return -2; + } + catch (Exception ex) + { + stderrCallback?.Invoke($"执行鼠标滚轮动作出错: {ex.Message}"); + return -1; + } + } + + public static async Task RunDelayAction( + MacroActionDelay action, + Action stderrCallback, + CancellationToken cancellationToken = default) + { + try + { + if (action.PreWait > 0) + await Task.Delay(action.PreWait, cancellationToken); + int duration = Math.Max(0, action.Duration); + await Task.Delay(duration, cancellationToken); + if (action.PostWait > 0) + await Task.Delay(action.PostWait, cancellationToken); + return 0; // Success + } + catch (OperationCanceledException) + { + return -2; + } + catch (Exception ex) + { + stderrCallback?.Invoke($"执行延迟动作出错: {ex.Message}"); + return -1; + } + } + } +} diff --git a/FancyInput/Models/MacroEnums.cs b/FancyInput/Models/MacroEnums.cs new file mode 100644 index 0000000..d4c0245 --- /dev/null +++ b/FancyInput/Models/MacroEnums.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FancyInput.Models +{ + public enum TriggerMode + { + [Description("按下时触发一次")] + Once, + [Description("按住时触发并循环")] + Hold, + [Description("按下时触发并循环,再次按下将其关闭")] + Toggle + } + + public enum TriggerTiming + { + [Description("按下时")] + Press, + [Description("抬起时")] + Release + } +} diff --git a/FancyInput/Models/OverlayParser.cs b/FancyInput/Models/OverlayParser.cs new file mode 100644 index 0000000..44e3c4f --- /dev/null +++ b/FancyInput/Models/OverlayParser.cs @@ -0,0 +1,96 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + + +namespace FancyInput.Models +{ + public class JsonIntOrStringConverter : JsonConverter + { + public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Number => reader.GetInt32(), + JsonTokenType.String => int.TryParse(reader.GetString(), out var v) ? v : throw new JsonException(), + JsonTokenType.Null => null, + _ => throw new JsonException() + }; + } + + public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) + { + if (value.HasValue) + writer.WriteNumberValue(value.Value); + else + writer.WriteNullValue(); + } + } + + public class OverlayElement + { + public ElementType type { get; set; } + public string id { get; set; } = ""; + public int[] pos { get; set; } = Array.Empty(); + public int[] mapping { get; set; } = Array.Empty(); + [JsonConverter(typeof(JsonIntOrStringConverter))] + public int? z_level { get; set; } + public int? code { get; set; } + public bool? trigger_mode { get; set; } + public Side? side { get; set; } + public Direction? direction { get; set; } + public int? stick_radius { get; set; } + public MouseMoveType? mouse_type { get; set; } + public int? mouse_radius { get; set; } + + public override string ToString() + { + return $"Type: {type}, ID: {id}, code:{code}"; + } + + public string ToJSON() + { + var options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true + }; + return JsonSerializer.Serialize(this, options); + } + } + + public class OverlayRoot + { + public int default_width { get; set; } + public int default_height { get; set; } + public int? space_h { get; set; } + public int? space_v { get; set; } + public int? flags { get; set; } + public int overlay_width { get; set; } + public int overlay_height { get; set; } + public List elements { get; set; } = new(); + + public string ToJSON() + { + var options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true + }; + return JsonSerializer.Serialize(this, options); + } + } + + internal class OverlayParser + { + public static OverlayRoot Parse(string path) + { + var json = File.ReadAllText(path); + var result = JsonSerializer.Deserialize(json); + if (result != null) + return result; + else + throw new Exception("Failed to parse overlay JSON."); + } + } +} diff --git a/FancyInput/Models/RawInputParser.cs b/FancyInput/Models/RawInputParser.cs new file mode 100644 index 0000000..e81b767 --- /dev/null +++ b/FancyInput/Models/RawInputParser.cs @@ -0,0 +1,653 @@ +using System; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Windows.Forms; +using System.Windows.Interop; +using System.Windows.Threading; +using Gma.System.MouseKeyHook; + +using Application = System.Windows.Application; +using Timer = System.Threading.Timer; +namespace FancyInput.Models +{ + public class RawInputKeyEventArgs : EventArgs + { + public ushort VKeyCode { get; } + public ushort ScanCode { get; } + public FipKeys Key { get; private set; } = FipKeys.None; + public FipRawKeys RawKey { get; private set; } = FipRawKeys.None; + public bool IsE0 { get; } + public bool IsKeyDown { get; } + + // 新增修饰键状态 + public bool IsCtrlDown { get; } + public bool IsShiftDown { get; } + public bool IsAltDown { get; } + public bool IsWinDown { get; } + public bool IsFnDown { get; } // Fn键无法直接检测,通常为false + + // 新增锁定键状态 + public bool IsNumLockOn { get; } + public bool IsCapsLockOn { get; } + public bool IsScrollLockOn { get; } + + public RawInputKeyEventArgs( + ushort vkey, ushort scancode, bool isE0, bool isKeyDown, + bool isCtrlDown, bool isShiftDown, bool isAltDown, bool isWinDown, bool isFnDown = false) + { + VKeyCode = vkey; + ScanCode = scancode; + IsE0 = isE0; + IsKeyDown = isKeyDown; + IsCtrlDown = isCtrlDown; + IsShiftDown = isShiftDown; + IsAltDown = isAltDown; + IsWinDown = isWinDown; + IsFnDown = isFnDown; + Key = RawInputParser.MapRawInputToCustomKeys(vkey, scancode, isE0); + RawKey = RawInputParser.MapRawInputToCustomRawKeys(vkey, scancode, isE0); + + // 检测Lock状态 + IsNumLockOn = Control.IsKeyLocked(Keys.NumLock); + IsCapsLockOn = Control.IsKeyLocked(Keys.CapsLock); + IsScrollLockOn = Control.IsKeyLocked(Keys.Scroll); + } + } + + public class RawInputMouseMoveEventArgs : EventArgs + { + public int DeltaX { get; } + public int DeltaY { get; } + public int X { get; } + public int Y { get; } + public RawInputMouseMoveEventArgs(int deltaX, int deltaY, int x, int y) + { + DeltaX = deltaX; + DeltaY = deltaY; + X = x; + Y = y; + } + } + + public class RawInputParser : IDisposable + { + public static readonly int ScreenH = Screen.PrimaryScreen!.Bounds.Height; + public static readonly int ScreenW = Screen.PrimaryScreen!.Bounds.Width; + public static readonly int ScreenCenterX = ScreenW / 2; + public static readonly int ScreenCenterY = ScreenH / 2; + + + private static IntPtr _hwnd; + private HwndSource? _source; + private IKeyboardMouseEvents? _globalHook; + private IntPtr _rawBuffer = IntPtr.Zero; + private int _rawBufferSize = 0; + + private int _mouseDeltaX = 0; + private int _mouseDeltaY = 0; + private int _pendingMouseDeltaX = 0; + private int _pendingMouseDeltaY = 0; + private int _mouseDispatchQueued = 0; + public int MouseX; + public int MouseY; + private Timer? _mouseMoveTimer; + private int _isDisposedFlag = 0; + + public event EventHandler? KeyDown; + public event EventHandler? KeyUp; + public event MouseEventHandler? MouseDown; + public event MouseEventHandler? MouseUp; + public event MouseEventHandler? MouseWheel; + public event MouseEventHandler? MouseMove; + + public event EventHandler? RawMouseMove; + + public static readonly HashSet NumLockAffectedFipRawKeys = new() + { + FipRawKeys.NumPad0, + FipRawKeys.NumPad1, + FipRawKeys.NumPad2, + FipRawKeys.NumPad3, + FipRawKeys.NumPad4, + FipRawKeys.NumPad5, + FipRawKeys.NumPad6, + FipRawKeys.NumPad7, + FipRawKeys.NumPad8, + FipRawKeys.NumPad9, + }; + + public RawInputParser(IntPtr hwnd, int moseMoveTickMiniSecond = 16) + { + _hwnd = hwnd; + _rawBuffer = Marshal.AllocHGlobal(RAWINPUT_INITIAL_BUFFER_SIZE); + _rawBufferSize = RAWINPUT_INITIAL_BUFFER_SIZE; + _source = HwndSource.FromHwnd(_hwnd); + _source.AddHook(WndProc); + RegisterRawInpuKeyboard(); + RegisterRawInputMouse(); + + // 鼠标事件使用GlobalMouseKeyHook + _globalHook = Hook.GlobalEvents(); + _globalHook.MouseDownExt += (s, e) => MouseDown?.Invoke(s, e); + _globalHook.MouseUpExt += (s, e) => MouseUp?.Invoke(s, e); + _globalHook.MouseWheel += (s, e) => MouseWheel?.Invoke(s, e); + _globalHook.MouseMove += (s, e) => MouseMove?.Invoke(s, e); + + int intervalMs = Math.Max(1, moseMoveTickMiniSecond); + _mouseMoveTimer = new Timer(MouseMoveTimer_Tick, null, intervalMs, intervalMs); + + MouseX = Cursor.Position.X; + MouseY = Cursor.Position.Y; + } + + private void MouseMoveTimer_Tick(object? state) + { + if (Volatile.Read(ref _isDisposedFlag) != 0) + return; + + int deltaX = Interlocked.Exchange(ref _mouseDeltaX, 0); + int deltaY = Interlocked.Exchange(ref _mouseDeltaY, 0); + if (deltaX == 0 && deltaY == 0) return; + + int targetX = MouseX + deltaX; + int targetY = MouseY + deltaY; + targetX = Math.Max(0, Math.Min(ScreenW - 1, targetX)); + targetY = Math.Max(0, Math.Min(ScreenH - 1, targetY)); + MouseX = targetX; + MouseY = targetY; + + // Coalesce timer ticks into at most one queued UI callback to avoid dispatcher backlog. + Interlocked.Add(ref _pendingMouseDeltaX, deltaX); + Interlocked.Add(ref _pendingMouseDeltaY, deltaY); + QueueRawMouseDispatch(); + } + + private void QueueRawMouseDispatch() + { + if (Interlocked.CompareExchange(ref _mouseDispatchQueued, 1, 0) != 0) + return; + + var dispatcher = Application.Current?.Dispatcher; + if (dispatcher == null) + { + Interlocked.Exchange(ref _mouseDispatchQueued, 0); + return; + } + + dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(FlushPendingRawMouseMove)); + } + + private void FlushPendingRawMouseMove() + { + if (Volatile.Read(ref _isDisposedFlag) != 0) + { + Interlocked.Exchange(ref _mouseDispatchQueued, 0); + return; + } + + int deltaX = Interlocked.Exchange(ref _pendingMouseDeltaX, 0); + int deltaY = Interlocked.Exchange(ref _pendingMouseDeltaY, 0); + if (deltaX != 0 || deltaY != 0) + { + var args = new RawInputMouseMoveEventArgs(deltaX, deltaY, MouseX, MouseY); + RawMouseMove?.Invoke(this, args); + } + + Interlocked.Exchange(ref _mouseDispatchQueued, 0); + + // Handle race: new deltas may arrive between exchange and reset. + if (Volatile.Read(ref _pendingMouseDeltaX) != 0 || Volatile.Read(ref _pendingMouseDeltaY) != 0) + { + QueueRawMouseDispatch(); + } + } + + public static void RegisterRawInpuKeyboard() + { + RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1]; + + rid[0].usUsagePage = 0x01; + rid[0].usUsage = 0x06; // Keyboard + rid[0].dwFlags = RIDEV_INPUTSINK; + rid[0].hwndTarget = _hwnd; + + bool success = RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE))); + if (!success) + { + int error = Marshal.GetLastWin32Error(); + AppMessageBox.Show($"Failed to register raw input devices. Error code: {error}"); + } + } + + public static void RegisterRawInputMouse() + { + RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1]; + rid[0].usUsagePage = 0x01; + rid[0].usUsage = 0x02; // Mouse + rid[0].dwFlags = RIDEV_INPUTSINK; + rid[0].hwndTarget = _hwnd; + + bool success = RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE))); + if (!success) + { + int error = Marshal.GetLastWin32Error(); + AppMessageBox.Show($"Failed to register raw input mouse. Error code: {error}"); + } + } + + public static void UnregisterRawInputKeyboard() + { + RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1]; + rid[0].usUsagePage = 0x01; + rid[0].usUsage = 0x06; // Keyboard + rid[0].dwFlags = 0x00000001; // RIDEV_REMOVE + rid[0].hwndTarget = IntPtr.Zero; // 必须为0 + + bool success = RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE))); + if (!success) + { + int error = Marshal.GetLastWin32Error(); + AppMessageBox.Show($"Failed to unregister raw input keyboard. Error code: {error}"); + } + } + + public static void UnregisterRawInputMouse() + { + RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1]; + rid[0].usUsagePage = 0x01; + rid[0].usUsage = 0x02; // Mouse + rid[0].dwFlags = 0x00000001; // RIDEV_REMOVE + rid[0].hwndTarget = IntPtr.Zero; // 必须为0 + + bool success = RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE))); + if (!success) + { + int error = Marshal.GetLastWin32Error(); + AppMessageBox.Show($"Failed to unregister raw input mouse. Error code: {error}"); + } + } + + private readonly HashSet _pressedKeys = new(); + + private void HandleKeyEvent(RAWKEYBOARD kb) + { + bool isKeyDown = kb.Message == WM_KEYDOWN || kb.Message == WM_SYSKEYDOWN; + bool isKeyUp = kb.Message == WM_KEYUP || kb.Message == WM_SYSKEYUP; + bool isE0 = (kb.Flags & E0_FLAG) != 0; + ushort key = kb.VKey; + + // 检查修饰键 + bool ctrl = IsKeyDown(Keys.LControlKey) || IsKeyDown(Keys.RControlKey); + bool shift = IsKeyDown(Keys.LShiftKey) || IsKeyDown(Keys.RShiftKey); + bool alt = IsKeyDown(Keys.LMenu) || IsKeyDown(Keys.RMenu); + bool win = IsKeyDown(Keys.LWin) || IsKeyDown(Keys.RWin); + + var args = new RawInputKeyEventArgs( + kb.VKey, kb.MakeCode, isE0, isKeyDown, + ctrl, shift, alt, win, false); + + if (isKeyDown) + { + if (_pressedKeys.Add(key)) // 只在第一次按下时触发 + { + OnKeyDown(args); + } + } + else if (isKeyUp) + { + _pressedKeys.Remove(key); + OnKeyUp(args); + } + + } + + private void InvokeOnDispatcher(Action action) + { + var dispatcher = Application.Current?.Dispatcher; + if (dispatcher == null || dispatcher.CheckAccess()) + { + action(); + return; + } + + dispatcher.BeginInvoke(DispatcherPriority.Input, action); + } + + private void OnKeyDown(RawInputKeyEventArgs args) => + InvokeOnDispatcher(() => KeyDown?.Invoke(this, args)); + + private void OnKeyUp(RawInputKeyEventArgs args) => + InvokeOnDispatcher(() => KeyUp?.Invoke(this, args)); + + + int _lastRawMouseX = 0; + int _lastRawMouseY = 0; + int _currentRawMouseX = 0; + int _currentRawMouseY = 0; + private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + const int WM_INPUT = 0x00FF; + if (msg == WM_INPUT) + { + if (KeyDown == null && KeyUp == null && MouseDown == null && MouseUp == null && MouseWheel == null && MouseMove == null && RawMouseMove == null) + return IntPtr.Zero; + uint dwSize = (uint)_rawBufferSize; + uint result = GetRawInputData(lParam, RID_INPUT, _rawBuffer, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))); ; + if (result == uint.MaxValue) + return IntPtr.Zero; + + // 如果缓冲区不够大,扩容并重试(极少发生) + if (result > dwSize) + { + Marshal.FreeHGlobal(_rawBuffer); + _rawBuffer = Marshal.AllocHGlobal((int)result); + _rawBufferSize = (int)result; + dwSize = result; + if (GetRawInputData(lParam, RID_INPUT, _rawBuffer, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))) != dwSize) + return IntPtr.Zero; + } + + RAWINPUTHEADER header = Marshal.PtrToStructure(_rawBuffer); + IntPtr dataPtr = _rawBuffer + Marshal.SizeOf(); + if (header.dwType == RIM_TYPEMOUSE) + { + RAWMOUSE mouse = Marshal.PtrToStructure(dataPtr); + + // 绝对坐标模式(如某些游戏或远程桌面)需要特殊处理 + if ((mouse.usFlags & RI_MOUSE_MOVE_ABSOLUTE) == RI_MOUSE_MOVE_ABSOLUTE) + { + // 归一化到 0~65535 区间 + int absX = Math.Clamp(mouse.lLastX, 0, 65535); + int absY = Math.Clamp(mouse.lLastY, 0, 65535); + + // 转换为像素坐标 + int pixelX = (int)((absX / 65535.0) * (ScreenW - 1)); + int pixelY = (int)((absY / 65535.0) * (ScreenH - 1)); + + _currentRawMouseX = pixelX; + _currentRawMouseY = pixelY; + Interlocked.Add(ref _mouseDeltaX, _currentRawMouseX - _lastRawMouseX); + Interlocked.Add(ref _mouseDeltaY, _currentRawMouseY - _lastRawMouseY); + _lastRawMouseX = _currentRawMouseX; + _lastRawMouseY = _currentRawMouseY; + return IntPtr.Zero; + } + + + Interlocked.Add(ref _mouseDeltaX, mouse.lLastX); + Interlocked.Add(ref _mouseDeltaY, mouse.lLastY); + return IntPtr.Zero; + } + if (header.dwType == RIM_TYPEKEYBOARD) + { + if (KeyDown == null && KeyUp == null) + return IntPtr.Zero; + RAWKEYBOARD kb = Marshal.PtrToStructure(dataPtr); + HandleKeyEvent(kb); + return IntPtr.Zero; + } + + } + return IntPtr.Zero; + } + + + public static FipKeys MapRawInputToCustomKeys(ushort vkey, ushort scancode, bool isE0) + { + // Pause/Break (E1) + if (vkey == 0x13) // VK_PAUSE + return FipKeys.Pause; + + // PrintScreen (有些驱动不设置 isE0) + if (vkey == 0x2C) // VK_SNAPSHOT + return FipKeys.PrintScreen; + + // Enter + if (vkey == 0x0D && scancode == 0x1C) + return isE0 ? FipKeys.NumpadEnter : FipKeys.Enter; + + // Ctrl + if (vkey == 0x11 && scancode == 0x1D) + return isE0 ? FipKeys.RControlKey : FipKeys.LControlKey; + + // Alt + if (vkey == 0x12 && scancode == 0x38) + return isE0 ? FipKeys.RMenu : FipKeys.LMenu; + + // Shift + if (vkey == 0x10) + { + if (scancode == 0x2A) return FipKeys.LShiftKey; + if (scancode == 0x36) return FipKeys.RShiftKey; + } + + // Win / Apps (避免 VK_UNKNOWN) + if (vkey == 0x5B) return FipKeys.LWin; + if (vkey == 0x5C) return FipKeys.RWin; + if (vkey == 0x5D) return FipKeys.Apps; + + // 其它键直接映射 (VK) + if (Enum.IsDefined(typeof(FipKeys), vkey)) + return (FipKeys)vkey; + + // 兜底:使用 E0 扫描码识别方向/编辑区/扩展键 + if (isE0) + { + switch (scancode) + { + case 0x48: return FipKeys.Up; + case 0x4B: return FipKeys.Left; + case 0x4D: return FipKeys.Right; + case 0x50: return FipKeys.Down; + case 0x47: return FipKeys.Home; + case 0x4F: return FipKeys.End; + case 0x49: return FipKeys.PageUp; + case 0x51: return FipKeys.PageDown; + case 0x52: return FipKeys.Insert; + case 0x53: return FipKeys.Delete; + case 0x35: return FipKeys.Divide; + case 0x5B: return FipKeys.LWin; + case 0x5C: return FipKeys.RWin; + case 0x5D: return FipKeys.Apps; + } + } + + return FipKeys.None; + } + + public static FipRawKeys MapRawInputToCustomRawKeys(ushort vkey, ushort scancode, bool isE0) + { + uint code = isE0 ? (0xE000u | scancode) : scancode; + + // Pause/Break (E1) + if (vkey == 0x13) // VK_PAUSE + return FipRawKeys.Pause; + + // PrintScreen (只看 vkey 更稳) + if (vkey == 0x2C) // VK_SNAPSHOT + return FipRawKeys.PrintScreen; + + // 右Ctrl / 左Ctrl + if (vkey == 0x11 && scancode == 0x1D) + return isE0 ? FipRawKeys.RControlKey : FipRawKeys.LControlKey; + + // 右Alt / 左Alt + if (vkey == 0x12 && scancode == 0x38) + return isE0 ? FipRawKeys.RMenu : FipRawKeys.LMenu; + + // 小键盘Enter / 主键盘Enter + if (vkey == 0x0D && scancode == 0x1C) + return isE0 ? FipRawKeys.NumpadEnter : FipRawKeys.Enter; + + // Win键 (若 vkey 可用则直接返回) + if (vkey == 0x5B) return FipRawKeys.LWin; + if (vkey == 0x5C) return FipRawKeys.RWin; + if (vkey == 0x5D) return FipRawKeys.Apps; + + // E0 前缀 / 普通扫描码兜底 + if (Enum.IsDefined(typeof(FipRawKeys), code)) + return (FipRawKeys)code; + + return FipRawKeys.None; + } + + public static Vector2 GetCursorPosition() + { + GetCursorPos(out POINT point); + return new Vector2(point.X, point.Y); + } + + private const uint RIDEV_INPUTSINK = 0x00000100; + private const int RAWINPUT_INITIAL_BUFFER_SIZE = 48; + + private const int RID_INPUT = 0x10000003; + private const int RIM_TYPEKEYBOARD = 1; + private const int RIM_TYPEMOUSE = 0; + private const int WM_KEYDOWN = 0x0100; + private const int WM_SYSKEYDOWN = 0x0104; + private const int WM_KEYUP = 0x0101; + private const int WM_SYSKEYUP = 0x0105; + + private const int E0_FLAG = 0x02; + + private const ushort RI_MOUSE_LEFT_BUTTON_DOWN = 0x0001; + private const ushort RI_MOUSE_LEFT_BUTTON_UP = 0x0002; + private const ushort RI_MOUSE_RIGHT_BUTTON_DOWN = 0x0004; + private const ushort RI_MOUSE_RIGHT_BUTTON_UP = 0x0008; + private const ushort RI_MOUSE_MIDDLE_BUTTON_DOWN = 0x0010; + private const ushort RI_MOUSE_MIDDLE_BUTTON_UP = 0x0020; + private const ushort RI_MOUSE_XBUTTON1_DOWN = 0x0040; + private const ushort RI_MOUSE_XBUTTON1_UP = 0x0080; + private const ushort RI_MOUSE_XBUTTON2_DOWN = 0x0100; + private const ushort RI_MOUSE_XBUTTON2_UP = 0x0200; + private const ushort RI_MOUSE_WHEEL = 0x0400; + private const ushort RI_MOUSE_MOVE_ABSOLUTE = 0x0001; + + [StructLayout(LayoutKind.Sequential)] + struct RAWINPUTDEVICE + { + public ushort usUsagePage; + public ushort usUsage; + public uint dwFlags; + public IntPtr hwndTarget; + } + [StructLayout(LayoutKind.Sequential)] + struct RAWINPUTHEADER + { + public uint dwType; + public uint dwSize; + public IntPtr hDevice; + public IntPtr wParam; + } + + [StructLayout(LayoutKind.Sequential)] + struct RAWKEYBOARD + { + public ushort MakeCode; + public ushort Flags; + public ushort Reserved; + public ushort VKey; + public uint Message; + public uint ExtraInformation; + } + + [StructLayout(LayoutKind.Explicit)] + struct RAWMOUSE + { + [FieldOffset(0)] + public ushort usFlags; + [FieldOffset(2)] + public ushort reserved; // 填充对齐 + [FieldOffset(4)] + public ushort usButtonFlags; + [FieldOffset(6)] + public ushort usButtonData; + [FieldOffset(8)] + public uint ulRawButtons; + [FieldOffset(12)] + public int lLastX; + [FieldOffset(16)] + public int lLastY; + [FieldOffset(20)] + public uint ulExtraInformation; + } + + [StructLayout(LayoutKind.Sequential)] + struct POINT + { + public int X; + public int Y; + } + + [DllImport("user32.dll")] + static extern bool GetCursorPos(out POINT lpPoint); + + [DllImport("user32.dll")] + static extern short GetKeyState(int nVirtKey); + + [DllImport("User32.dll", SetLastError = true)] + static extern bool RegisterRawInputDevices( + [In] RAWINPUTDEVICE[] pRawInputDevices, + uint uiNumDevices, + uint cbSize); + + [DllImport("User32.dll", SetLastError = true)] + static extern uint GetRawInputData( + IntPtr hRawInput, + uint uiCommand, + IntPtr pData, + ref uint pcbSize, + uint cbSizeHeader); + + private static bool IsKeyDown(Keys key) + { + return (GetKeyState((int)key) & 0x8000) != 0; + } + + public void Dispose() + { + if (Interlocked.Exchange(ref _isDisposedFlag, 1) != 0) + return; + + _mouseMoveTimer?.Change(Timeout.Infinite, Timeout.Infinite); + _mouseMoveTimer?.Dispose(); + _mouseMoveTimer = null; + + if (_source != null) + { + _source.RemoveHook(WndProc); + _source = null; + } + if (_rawBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_rawBuffer); + _rawBuffer = IntPtr.Zero; + _rawBufferSize = 0; + } + //Device.MouseInput -= OnMouseInput; + _globalHook?.Dispose(); + _globalHook = null; + GC.SuppressFinalize(this); + } + + private const int WM_ENTERSIZEMOVE = 0x0231; + private const int WM_EXITSIZEMOVE = 0x0232; + public static IntPtr WndDragProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + switch (msg) + { + case WM_ENTERSIZEMOVE: + // 开始移动或缩放 + RawInputParser.UnregisterRawInputMouse(); + break; + case WM_EXITSIZEMOVE: + // 结束移动或缩放 + RawInputParser.RegisterRawInputMouse(); + break; + } + return IntPtr.Zero; + } + } +} diff --git a/FancyInput/Models/SimpleProgressConverter.cs b/FancyInput/Models/SimpleProgressConverter.cs new file mode 100644 index 0000000..b60e369 --- /dev/null +++ b/FancyInput/Models/SimpleProgressConverter.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace FancyInput.Models +{ + /// + /// 通用的Slider进度转换器 + /// 根据Slider的Minimum、Maximum和Value计算进度宽度 + /// + public class SliderProgressConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + try + { + // values[0] = Value + // values[1] = Minimum + // values[2] = Maximum + // values[3] = TrackBackground.ActualWidth + + if (values.Length >= 4 && + values[0] is double value && + values[1] is double minimum && + values[2] is double maximum && + values[3] is double trackWidth) + { + // 防止除以0 + if (Math.Abs(maximum - minimum) < 0.0001) + return 0.0; + + // 计算百分比 + double percentage = (value - minimum) / (maximum - minimum); + + // 返回进度条宽度 + return Math.Max(0, Math.Min(trackWidth, percentage * trackWidth)); + } + + return 0.0; + } + catch + { + return 0.0; + } + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/FancyInput/Models/VirtualControllerManager.cs b/FancyInput/Models/VirtualControllerManager.cs new file mode 100644 index 0000000..f2857e3 --- /dev/null +++ b/FancyInput/Models/VirtualControllerManager.cs @@ -0,0 +1,311 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Diagnostics; +using System.IO; + +namespace FancyInput.Models +{ + /// + /// Lightweight runtime wrapper for Nefarius.ViGEm.Client to manage a single virtual Xbox360 controller. + /// Uses reflection so the project can compile even if the DLL is not present at build time. + /// + public sealed class VirtualControllerManager : IDisposable + { + private static readonly Lazy _instance = new(() => new VirtualControllerManager()); + public static VirtualControllerManager Instance => _instance.Value; + + private object? _client; + private object? _controller; + private MethodInfo? _connectMethod; + private MethodInfo? _disconnectMethod; + private MethodInfo? _setButtonStateMethod; + private MethodInfo? _setAxisValueMethod; + private MethodInfo? _setSliderValueMethod; + private MethodInfo? _submitReportMethod; + private PropertyInfo? _userIndexProperty; + private Type? _xboxButtonEnumType; + private Type? _xboxAxisEnumType; + private Type? _xboxSliderEnumType; + private bool _initialized = false; + + public bool IsAvailable => _initialized; + public int Index { get; private set; } = -1; + + private VirtualControllerManager() { } + + public bool PlugIn() => Initialize(); + + public void Unplug() => Disconnect(); + + public bool Initialize() + { + if (_initialized) return true; + + try + { + // find/load the ViGEm client type + var clientType = ResolveViGEmClientType(); + + if (clientType == null) return false; + + _client = Activator.CreateInstance(clientType); + + var createMethod = FindMethod(clientType, "CreateXbox360Controller", 0); + if (createMethod == null) return false; + + _controller = createMethod.Invoke(_client, null); + if (_controller == null) return false; + + var controllerType = _controller.GetType(); + // choose parameterless Connect/Disconnect to avoid AmbiguousMatchException + _connectMethod = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(m => m.Name == "Connect" && m.GetParameters().Length == 0); + _disconnectMethod = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(m => m.Name == "Disconnect" && m.GetParameters().Length == 0); + + _xboxButtonEnumType = controllerType.Assembly.GetType("Nefarius.ViGEm.Client.Targets.Xbox360.Xbox360Button"); + _xboxAxisEnumType = controllerType.Assembly.GetType("Nefarius.ViGEm.Client.Targets.Xbox360.Xbox360Axis"); + _xboxSliderEnumType = controllerType.Assembly.GetType("Nefarius.ViGEm.Client.Targets.Xbox360.Xbox360Slider"); + + _setButtonStateMethod = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(m => m.Name == "SetButtonState" && m.GetParameters().Length == 2 && + _xboxButtonEnumType != null && m.GetParameters()[0].ParameterType == _xboxButtonEnumType && + m.GetParameters()[1].ParameterType == typeof(bool)); + + _setAxisValueMethod = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(m => m.Name == "SetAxisValue" && m.GetParameters().Length == 2 && + _xboxAxisEnumType != null && m.GetParameters()[0].ParameterType == _xboxAxisEnumType && + m.GetParameters()[1].ParameterType == typeof(short)); + + _setSliderValueMethod = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(m => m.Name == "SetSliderValue" && m.GetParameters().Length == 2 && + _xboxSliderEnumType != null && m.GetParameters()[0].ParameterType == _xboxSliderEnumType && + m.GetParameters()[1].ParameterType == typeof(byte)); + + _submitReportMethod = FindMethod(controllerType, "SubmitReport", 0); + _userIndexProperty = controllerType.GetProperty("UserIndex", BindingFlags.Public | BindingFlags.Instance); + + if (_setButtonStateMethod == null || _setAxisValueMethod == null || _setSliderValueMethod == null || _submitReportMethod == null) + { + Debug.WriteLine("VirtualControllerManager.Initialize failed: required Xbox360 methods not found on controller type."); + return false; + } + + // connect + _connectMethod?.Invoke(_controller, null); + UpdateControllerIndex(); + + _initialized = true; + return true; + } + catch (Exception ex) + { + Debug.WriteLine("VirtualControllerManager.Initialize failed: " + ex); + _initialized = false; + return false; + } + } + + public void Disconnect() + { + if (!_initialized) return; + try + { + _disconnectMethod?.Invoke(_controller, null); + } + catch { } + _initialized = false; + Index = -1; + } + + public void SetButton(FIPGamepadButtonflags flag, bool pressed) + { + EnsureInitialized(); + if (!_initialized) throw new NotSupportedException("Virtual controller not available."); + + var mappings = new[] + { + (FIPGamepadButtonflags.Up, "Up"), + (FIPGamepadButtonflags.Down, "Down"), + (FIPGamepadButtonflags.Left, "Left"), + (FIPGamepadButtonflags.Right, "Right"), + (FIPGamepadButtonflags.Start, "Start"), + (FIPGamepadButtonflags.Select, "Back"), + (FIPGamepadButtonflags.LS, "LeftThumb"), + (FIPGamepadButtonflags.RS, "RightThumb"), + (FIPGamepadButtonflags.LB, "LeftShoulder"), + (FIPGamepadButtonflags.RB, "RightShoulder"), + (FIPGamepadButtonflags.A, "A"), + (FIPGamepadButtonflags.B, "B"), + (FIPGamepadButtonflags.X, "X"), + (FIPGamepadButtonflags.Y, "Y"), + }; + + foreach (var (f, name) in mappings) + { + if (!flag.HasFlag(f)) continue; + var enumVal = ParseEnum(_xboxButtonEnumType, name); + _setButtonStateMethod!.Invoke(_controller, new[] { enumVal, (object)pressed }); + } + + _submitReportMethod!.Invoke(_controller, null); + } + + public void SetTrigger(int triggerValue, Side side) + { + EnsureInitialized(); + if (!_initialized) throw new NotSupportedException("Virtual controller not available."); + byte v = (byte)Math.Clamp(triggerValue, 0, 255); + + string sliderName = side == Side.Left ? "LeftTrigger" : "RightTrigger"; + object slider = ParseEnum(_xboxSliderEnumType, sliderName); + _setSliderValueMethod!.Invoke(_controller, new object[] { slider, v }); + _submitReportMethod!.Invoke(_controller, null); + } + + public void SetStick(int x, int y, Side side) + { + EnsureInitialized(); + if (!_initialized) throw new NotSupportedException("Virtual controller not available."); + short sx = (short)Math.Clamp(x, short.MinValue, short.MaxValue); + short sy = (short)Math.Clamp(y, short.MinValue, short.MaxValue); + if (side == Side.Left) + { + object axisX = ParseEnum(_xboxAxisEnumType, "LeftThumbX"); + object axisY = ParseEnum(_xboxAxisEnumType, "LeftThumbY"); + _setAxisValueMethod!.Invoke(_controller, new object[] { axisX, sx }); + _setAxisValueMethod!.Invoke(_controller, new object[] { axisY, sy }); + } + else + { + object axisX = ParseEnum(_xboxAxisEnumType, "RightThumbX"); + object axisY = ParseEnum(_xboxAxisEnumType, "RightThumbY"); + _setAxisValueMethod!.Invoke(_controller, new object[] { axisX, sx }); + _setAxisValueMethod!.Invoke(_controller, new object[] { axisY, sy }); + } + + _submitReportMethod!.Invoke(_controller, null); + } + + private void EnsureInitialized() + { + if (_initialized) return; + Initialize(); + } + + private void UpdateControllerIndex() + { + try + { + if (_controller == null || _userIndexProperty == null) + { + Index = -1; + return; + } + + object? raw = _userIndexProperty.GetValue(_controller); + if (raw is int i) + { + Index = i; + return; + } + + Index = -1; + } + catch + { + Index = -1; + } + } + + private static Type? ResolveViGEmClientType() + { + const string clientTypeName = "Nefarius.ViGEm.Client.ViGEmClient"; + + // 1) already loaded assemblies + var loaded = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => a.GetTypesSafe()) + .FirstOrDefault(t => t.FullName == clientTypeName); + if (loaded != null) return loaded; + + // 2) try load by assembly name (works when DLL is in probing path) + try + { + var asm = Assembly.Load("Nefarius.ViGEm.Client"); + var t = asm.GetType(clientTypeName); + if (t != null) return t; + } + catch (Exception ex) + { + Debug.WriteLine("ResolveViGEmClientType Assembly.Load failed: " + ex.Message); + } + + // 3) try load from app base directory + try + { + string dllPath = Path.Combine(AppContext.BaseDirectory, "Nefarius.ViGEm.Client.dll"); + if (File.Exists(dllPath)) + { + var asm = Assembly.LoadFrom(dllPath); + var t = asm.GetType(clientTypeName); + if (t != null) return t; + } + } + catch (Exception ex) + { + Debug.WriteLine("ResolveViGEmClientType Assembly.LoadFrom failed: " + ex.Message); + } + + return null; + } + + private static object ParseEnum(Type? enumType, string name) + { + if (enumType == null) + throw new NotSupportedException("Virtual controller enum type is unavailable."); + + // ViGEm's Xbox360Button/Axis/Slider in this version are class types with static fields, + // not CLR enums. Resolve by static field first. + var field = enumType + .GetFields(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.OrdinalIgnoreCase)); + + if (field != null) + { + var value = field.GetValue(null); + if (value != null) return value; + } + + if (enumType.IsEnum) + { + return Enum.Parse(enumType, name, ignoreCase: true); + } + + throw new NotSupportedException($"Type '{enumType.FullName}' does not expose a usable member named '{name}'."); + } + + public void Dispose() + { + Disconnect(); + _client = null; + _controller = null; + } + + private static MethodInfo? FindMethod(Type? type, string name, int parameterCount) + { + if (type == null) return null; + return type.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(m => m.Name == name && m.GetParameters().Length == parameterCount); + } + + } + + internal static class ReflectionHelpers + { + public static System.Collections.Generic.IEnumerable GetTypesSafe(this Assembly a) + { + try { return a.GetTypes(); } catch { return System.Array.Empty(); } + } + } +} diff --git a/FancyInput/Models/XImage.cs b/FancyInput/Models/XImage.cs new file mode 100644 index 0000000..5b35bc9 --- /dev/null +++ b/FancyInput/Models/XImage.cs @@ -0,0 +1,662 @@ +using System.Drawing; +using System.IO; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Color = System.Windows.Media.Color; + + +namespace FancyInput.Models +{ + public enum XImageType + { + PNG, + GIF, + Unknown + } + + public class XImage + { + public XImageType ImageType { get; set; } + private BitmapSource _bitmapSource; + public BitmapSource BitmapSource => _bitmapSource; + public int Width => _bitmapSource.PixelWidth; + public int Height => _bitmapSource.PixelHeight; + public byte[]? GifRawBytes { get; private set; } + public XImage(string path) + { + ImageType = XImage.GetImageType(path); + switch (ImageType) + { + case XImageType.GIF: + GifRawBytes = File.ReadAllBytes(path); + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.StreamSource = new MemoryStream(GifRawBytes); + bitmap.EndInit(); + _bitmapSource = bitmap; + break; + case XImageType.PNG: + { + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad); + _bitmapSource = decoder.Frames[0]; + _bitmapSource = _bitmapSource.Format == PixelFormats.Bgra32 ? + _bitmapSource : new FormatConvertedBitmap(_bitmapSource, PixelFormats.Bgra32, null, 0); + } + break; + default: + throw new NotSupportedException($"Unsupported image format: {Path.GetExtension(path)}"); + } + } + public XImage(BitmapSource bitmapSource, XImageType imageType=XImageType.PNG, byte[]? gifRawBytes = null) + { + _bitmapSource = bitmapSource; + ImageType = imageType; + if (imageType == XImageType.GIF && gifRawBytes != null) + GifRawBytes = gifRawBytes.ToArray(); + } + + public static XImageType GetImageType(string path) + { + var ext = Path.GetExtension(path).ToLower(); + return ext switch + { + ".gif" => XImageType.GIF, + ".png" or ".jpg" or ".jpeg" or ".bmp" => XImageType.PNG, + _ => XImageType.Unknown + }; + } + public static List<(BitmapSource Frame, int DelayMs)> LoadGifFramesWithDelay(string path) + { + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad); + var result = new List<(BitmapSource, int)>(); + foreach (var frame in decoder.Frames) + { + var metadata = frame.Metadata as BitmapMetadata; + int delay = 100; // 默认100ms + if (metadata != null && metadata.ContainsQuery("/grctlext/Delay")) + { + delay = (ushort)metadata.GetQuery("/grctlext/Delay") * 10; + } + result.Add((frame, delay)); + } + return result; + } + + private static BitmapSource EnsureBgra32(BitmapSource source) + { + return source.Format == PixelFormats.Bgra32 + ? source + : new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0); + } + + private BitmapSource GetBitmapForPixelOperations() + { + if (ImageType == XImageType.GIF && GifRawBytes != null && GifRawBytes.Length > 0) + { + using var stream = new MemoryStream(GifRawBytes, false); + var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad); + if (decoder.Frames.Count == 0) + throw new InvalidOperationException("GIF has no decodable frames."); + return EnsureBgra32(decoder.Frames[0]); + } + + return EnsureBgra32(_bitmapSource); + } + + public XImage Cut(int left, int right, int top, int bottom) + { + if (left < 0 || right < 0 || top < 0 || bottom < 0) + throw new ArgumentException("Cut values must be non-negative"); + if (left + right >= Width || top + bottom >= Height) + throw new ArgumentException("Cut values are too large"); + int newW = Width - left - right; + int newH = Height - top - bottom; + return Crop(left, top, newW, newH); + } + + public XImage Crop(int x, int y, int w, int h) + { + if (w <= 0 || h <= 0) + throw new ArgumentException("Crop width and height must be positive"); + + var source = GetBitmapForPixelOperations(); + var format = PixelFormats.Bgra32; + const int bytesPerPixel = 4; + + int srcStride = source.PixelWidth * bytesPerPixel; + byte[] srcPixels = new byte[source.PixelHeight * srcStride]; + source.CopyPixels(srcPixels, srcStride, 0); + + // Fixed-size output canvas initialized to transparent pixels. + int dstStride = w * bytesPerPixel; + byte[] dstPixels = new byte[h * dstStride]; + + int srcStartX = Math.Max(0, x); + int srcStartY = Math.Max(0, y); + int srcEndX = Math.Min(source.PixelWidth, x + w); + int srcEndY = Math.Min(source.PixelHeight, y + h); + + int copyW = srcEndX - srcStartX; + int copyH = srcEndY - srcStartY; + + if (copyW > 0 && copyH > 0) + { + int dstStartX = srcStartX - x; + int dstStartY = srcStartY - y; + + for (int row = 0; row < copyH; row++) + { + int srcOffset = (srcStartY + row) * srcStride + srcStartX * bytesPerPixel; + int dstOffset = (dstStartY + row) * dstStride + dstStartX * bytesPerPixel; + Buffer.BlockCopy(srcPixels, srcOffset, dstPixels, dstOffset, copyW * bytesPerPixel); + } + } + + var bmp = BitmapSource.Create(w, h, source.DpiX, source.DpiY, format, null, dstPixels, dstStride); + return new XImage(bmp, XImageType.PNG); + } + + public static XImage Empty(int H,int W) + { + var format = System.Windows.Media.PixelFormats.Bgra32; + var stride = W * (format.BitsPerPixel / 8); + byte[] pixels = new byte[H * stride]; + var bmp = BitmapSource.Create(W, H, 96, 96, format, null, pixels, stride); + return new XImage(bmp,XImageType.PNG); + } + + public static XImage FromMask(bool[] mask,int H,int W,Color color) + { + if (mask.Length != H * W) + throw new ArgumentException("Mask length must be equal to H * W"); + var format = System.Windows.Media.PixelFormats.Bgra32; + var stride = W * (format.BitsPerPixel / 8); + byte[] pixels = new byte[H * stride]; + for (int i = 0; i < mask.Length; i++) + { + if (!mask[i]) + { + pixels[i * 4] = 0; + pixels[i * 4 + 1] = 0; + pixels[i * 4 + 2] = 0; + pixels[i * 4 + 3] = 0; + } + else + { + pixels[i * 4] = color.B; + pixels[i * 4 + 1] = color.G; + pixels[i * 4 + 2] = color.R; + pixels[i * 4 + 3] = color.A; + } + } + var bmp = BitmapSource.Create(W, H, 96, 96, format, null, pixels, stride); + return new XImage(bmp, XImageType.PNG); + } + + public XImage Copy() => new XImage(_bitmapSource.Clone(),ImageType,GifRawBytes); + + public (byte[] R, byte[] G, byte[] B, byte[] A) ExtractChannels() + { + var source = GetBitmapForPixelOperations(); + int w = source.PixelWidth; + int h = source.PixelHeight; + var format = source.Format; + var stride = w * (format.BitsPerPixel / 8); + byte[] pixels = new byte[h * stride]; + source.CopyPixels(new System.Windows.Int32Rect(0, 0, w, h), pixels, stride, 0); + byte[] r = new byte[w * h]; + byte[] g = new byte[w * h]; + byte[] b = new byte[w * h]; + byte[] a = new byte[w * h]; + for (int i = 0, j = 0; i < pixels.Length; i += 4, j++) + { + b[j] = pixels[i]; + g[j] = pixels[i + 1]; + r[j] = pixels[i + 2]; + a[j] = pixels[i + 3]; + } + return (r, g, b, a); + } + + public static XImage MergeChannels(byte[] r, byte[] g, byte[] b, byte[] a,int h,int w) + { + var format = System.Windows.Media.PixelFormats.Bgra32; + var stride = w * (format.BitsPerPixel / 8); + byte[] pixels = new byte[h * stride]; + for (int i = 0, j = 0; j < r.Length; i += 4, j++) + { + pixels[i] = b[j]; + pixels[i + 1] = g[j]; + pixels[i + 2] = r[j]; + pixels[i + 3] = a[j]; + } + var bmp = BitmapSource.Create(w, h, 96, 96, format, null, pixels, stride); + return new XImage(bmp, XImageType.PNG); + } + + public XImage ReSize(int H, int W) + { + if (H == Height && W == Width) + return this; + if (H <= 0 || W <= 0) + throw new ArgumentException("Width and Height must be positive integers"); + var source = GetBitmapForPixelOperations(); + //var scaleX = (double)W / source.PixelWidth; + //var scaleY = (double)H / source.PixelHeight; + //var transform = new System.Windows.Media.ScaleTransform(scaleX, scaleY); + //var dv = new System.Windows.Media.DrawingVisual(); + //using (var dc = dv.RenderOpen()) + //{ + // dc.PushTransform(transform); + // dc.DrawImage(source, new System.Windows.Rect(0, 0, source.PixelWidth, source.PixelHeight)); + // dc.Pop(); + //} + //var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap( + // W, H, source.DpiX, source.DpiY, System.Windows.Media.PixelFormats.Default); + //rtb.Render(dv); + //return new XImage(rtb, XImageType.PNG); + + // 直接把原图拉伸到目标像素区域 + var dv = new System.Windows.Media.DrawingVisual(); + using (var dc = dv.RenderOpen()) + { + dc.DrawImage(source, new System.Windows.Rect(0, 0, W, H)); + } + // RenderTargetBitmap 只能用 Pbgra32 + var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap( + W, H, 96, 96, System.Windows.Media.PixelFormats.Pbgra32); + rtb.Render(dv); + + // 转换为 Bgra32 + var bgra32 = new FormatConvertedBitmap(rtb, PixelFormats.Bgra32, null, 0); + + return new XImage(bgra32, XImageType.PNG); + } + + public XImage Scale(double scale) + { + if (scale <= 0) + throw new ArgumentException("Scale must be a positive number"); + int newW = (int)(Width * scale); + int newH = (int)(Height * scale); + var resized = ReSize(newH, newW); + return resized; + } + + public XImage EraseCircle(double centerX, double centerY, double radius) + { + if (double.IsNaN(centerX) || double.IsInfinity(centerX) || + double.IsNaN(centerY) || double.IsInfinity(centerY) || + double.IsNaN(radius) || double.IsInfinity(radius)) + { + throw new ArgumentException("Center and radius must be finite numbers."); + } + + if (radius <= 0) + { + return Copy(); + } + + var source = GetBitmapForPixelOperations(); + const int bytesPerPixel = 4; + int width = source.PixelWidth; + int height = source.PixelHeight; + int stride = width * bytesPerPixel; + + byte[] pixels = new byte[height * stride]; + source.CopyPixels(pixels, stride, 0); + + int minX = Math.Max(0, (int)Math.Floor(centerX - radius)); + int maxX = Math.Min(width - 1, (int)Math.Ceiling(centerX + radius)); + int minY = Math.Max(0, (int)Math.Floor(centerY - radius)); + int maxY = Math.Min(height - 1, (int)Math.Ceiling(centerY + radius)); + double radiusSquared = radius * radius; + + for (int y = minY; y <= maxY; y++) + { + for (int x = minX; x <= maxX; x++) + { + double dx = x - centerX; + double dy = y - centerY; + if (dx * dx + dy * dy <= radiusSquared) + { + int offset = y * stride + x * bytesPerPixel; + pixels[offset] = 0; + pixels[offset + 1] = 0; + pixels[offset + 2] = 0; + pixels[offset + 3] = 0; + } + } + } + + var bitmap = BitmapSource.Create( + width, + height, + source.DpiX, + source.DpiY, + PixelFormats.Bgra32, + null, + pixels, + stride); + + return new XImage(bitmap, XImageType.PNG); + } + + public static XImage Merge(XImage image1, XImage image2, Direction direction, double percentage) + { + var source1 = image1.GetBitmapForPixelOperations(); + var source2 = image2.GetBitmapForPixelOperations(); + + if (source1.PixelWidth != source2.PixelWidth || source1.PixelHeight != source2.PixelHeight) + throw new ArgumentException("Images must have same dimensions"); + + int w = source1.PixelWidth; + int h = source1.PixelHeight; + var format = source1.Format; + var dpiX = source1.DpiX; + var dpiY = source1.DpiY; + int bytesPerPixel = format.BitsPerPixel / 8; + int stride = w * bytesPerPixel; + + // 创建结果像素数组 + byte[] resultPixels = new byte[h * stride]; + + // 根据方向进行处理 + switch (direction) + { + case Direction.Up: // 上下合并:image1在上,image2在下 + { + int splitPos = (int)(h * (1 - percentage)); + splitPos = Math.Max(0, Math.Min(splitPos, h)); + if (splitPos > 0) + { + source1.CopyPixels( + new System.Windows.Int32Rect(0, 0, w, splitPos), + resultPixels, stride, 0); + } + if (splitPos < h) + { + int startY = Math.Min(splitPos, h - 1); + int height = h - startY; + int offset = startY * stride; + source2.CopyPixels( + new System.Windows.Int32Rect(0, startY, w, height), + resultPixels, stride, offset); + } + break; + } + + case Direction.Down: // 下上合并:image2在上,image1在下 + { + int splitPos = (int)(h * percentage); + splitPos = Math.Max(0, Math.Min(splitPos, h)); + + if (splitPos > 0) + { + source2.CopyPixels( + new System.Windows.Int32Rect(0, 0, w, splitPos), + resultPixels, stride, 0); + } + if (splitPos < h) + { + // 复制image1的下半部分 + int startY = Math.Min(splitPos, h - 1); + int height = h - startY; + int offset = startY * stride; + + source1.CopyPixels( + new System.Windows.Int32Rect(0, startY, w, height), + resultPixels, stride, offset); + } + break; + } + + + case Direction.Left: // 左右合并:image1在左,image2在右 + { + int splitPos = (int)(w * (1 - percentage)); + splitPos = Math.Max(0, Math.Min(splitPos, w)); + byte[] rowBuffer = new byte[w * bytesPerPixel]; + + for (int y = 0; y < h; y++) + { + int destOffset = y * stride; + if (splitPos > 0) + { + source1.CopyPixels( + new System.Windows.Int32Rect(0, y, splitPos, 1), + rowBuffer, splitPos * bytesPerPixel, 0); + Buffer.BlockCopy(rowBuffer, 0, resultPixels, destOffset, splitPos * bytesPerPixel); + } + if (splitPos < w) + { + int rightWidth = w - splitPos; + source2.CopyPixels( + new System.Windows.Int32Rect(splitPos, y, rightWidth, 1), + rowBuffer, rightWidth * bytesPerPixel, 0); + Buffer.BlockCopy(rowBuffer, 0, resultPixels, destOffset + splitPos * bytesPerPixel, + rightWidth * bytesPerPixel); + } + } + break; + } + + case Direction.Right: // 右左合并:image2在左,image1在右 + { + int splitPos = (int)(w * percentage); + splitPos = Math.Max(0, Math.Min(splitPos, w)); + byte[] rowBuffer = new byte[w * bytesPerPixel]; + + for (int y = 0; y < h; y++) + { + int destOffset = y * stride; + if (splitPos > 0) + { + source2.CopyPixels( + new System.Windows.Int32Rect(0, y, splitPos, 1), + rowBuffer, splitPos * bytesPerPixel, 0); + Buffer.BlockCopy(rowBuffer, 0, resultPixels, destOffset, splitPos * bytesPerPixel); + } + if (splitPos < w) + { + int rightWidth = w - splitPos; + source1.CopyPixels( + new System.Windows.Int32Rect(splitPos, y, rightWidth, 1), + rowBuffer, rightWidth * bytesPerPixel, 0); + Buffer.BlockCopy(rowBuffer, 0, resultPixels, destOffset + splitPos * bytesPerPixel, + rightWidth * bytesPerPixel); + } + } + break; + } + default: + throw new ArgumentException("Direction must be 1-4"); + } + + // 创建结果位图 + var resultBitmap = BitmapSource.Create( + w, h, dpiX, dpiY, format, null, resultPixels, stride); + + return new XImage(resultBitmap, XImageType.PNG); + } + + public void Save(string path) + { + var ext = Path.GetExtension(path).ToLower(); + BitmapEncoder encoder; + switch (ext) + { + case ".png": + encoder = new PngBitmapEncoder(); + break; + case ".jpg": + case ".jpeg": + encoder = new JpegBitmapEncoder(); + break; + case ".bmp": + encoder = new BmpBitmapEncoder(); + break; + case ".gif": + if (ImageType == XImageType.GIF) + { + // 只保存当前帧,非动画 + encoder = new GifBitmapEncoder(); + } + else + { + encoder = new GifBitmapEncoder(); + } + break; + default: + throw new NotSupportedException($"Unsupported image format: {ext}"); + } + encoder.Frames.Add(BitmapFrame.Create(_bitmapSource)); + using var stream = new FileStream(path, FileMode.Create, FileAccess.Write); + encoder.Save(stream); + + if (ext == ".gif" && ImageType == XImageType.GIF) + { + // 提示:只保存当前帧,非动画 + System.Diagnostics.Debug.WriteLine("Warning: Saving GIF only saves the current frame, not the full animation."); + } + } + + public record PlacedImage(int Index, XImage Image, int X, int Y); + + public static (XImage atlas, List placements) PackImages(List images) + { + if (images == null || images.Count == 0) + throw new ArgumentException("images 不能为空"); + + // 记录原始索引 + var indexed = images.Select((img, idx) => (img, idx, source: img.GetBitmapForPixelOperations())).ToList(); + + // 按高度降序排列,但保留原始索引 + var sorted = indexed.OrderByDescending(x => x.source.PixelHeight).ToList(); + + int maxRowWidth = sorted.Sum(x => x.source.PixelWidth); + int atlasWidth = 0, atlasHeight = 0; + int curX = 0, curY = 0, rowHeight = 0; + List placements = new(); + + foreach (var (img, idx, source) in sorted) + { + if (curX + source.PixelWidth > maxRowWidth && curX > 0) + { + atlasWidth = Math.Max(atlasWidth, curX); + curY += rowHeight; + curX = 0; + rowHeight = 0; + } + placements.Add(new PlacedImage(idx, img, curX, curY)); + curX += source.PixelWidth; + rowHeight = Math.Max(rowHeight, source.PixelHeight); + } + atlasWidth = Math.Max(atlasWidth, curX); + atlasHeight = curY + rowHeight; + + var wb = new System.Windows.Media.Imaging.WriteableBitmap( + atlasWidth, atlasHeight, 96, 96, System.Windows.Media.PixelFormats.Bgra32, null); + + foreach (var p in placements) + { + var src = p.Image.GetBitmapForPixelOperations(); + var rect = new System.Windows.Int32Rect(0, 0, src.PixelWidth, src.PixelHeight); + int stride = src.PixelWidth * (src.Format.BitsPerPixel / 8); + byte[] pixels = new byte[src.PixelHeight * stride]; + src.CopyPixels(rect, pixels, stride, 0); + + wb.Lock(); + try + { + IntPtr destPtr = wb.BackBuffer + p.Y * wb.BackBufferStride + p.X * 4; + for (int row = 0; row < src.PixelHeight; row++) + { + System.Runtime.InteropServices.Marshal.Copy( + pixels, row * stride, + destPtr + row * wb.BackBufferStride, + stride); + } + wb.AddDirtyRect(new System.Windows.Int32Rect(p.X, p.Y, src.PixelWidth, src.PixelHeight)); + } + finally + { + wb.Unlock(); + } + } + + // placements 按原始索引排序,方便查找 + var placementsByIndex = placements.OrderBy(p => p.Index).ToList(); + + return (new XImage(wb, XImageType.PNG), placementsByIndex); + } + + public static XImage ConcatImages(List images, int spacing, int direction) + { + if (images == null || images.Count == 0) + throw new ArgumentException("images 不能为空"); + + var sources = images.Select(img => img.GetBitmapForPixelOperations()).ToList(); + int w = sources[0].PixelWidth; + int h = sources[0].PixelHeight; + foreach (var src in sources) + if (src.PixelWidth != w || src.PixelHeight != h) + throw new ArgumentException("所有图片必须尺寸一致"); + + int count = images.Count; + int outW, outH; + if (direction == 0) // 向下 + { + outW = w; + outH = h * count + spacing * (count - 1); + } + else if (direction == 1) // 向右 + { + outW = w * count + spacing * (count - 1); + outH = h; + } + else + { + throw new ArgumentException("direction 只能为0(向下)或1(向右)"); + } + + var wb = new System.Windows.Media.Imaging.WriteableBitmap( + outW, outH, 96, 96, System.Windows.Media.PixelFormats.Bgra32, null); + + for (int i = 0; i < count; i++) + { + int x = direction == 1 ? i * (w + spacing) : 0; + int y = direction == 0 ? i * (h + spacing) : 0; + + var src = sources[i]; + var rect = new System.Windows.Int32Rect(0, 0, w, h); + int stride = w * (src.Format.BitsPerPixel / 8); + byte[] pixels = new byte[h * stride]; + src.CopyPixels(rect, pixels, stride, 0); + + wb.Lock(); + try + { + IntPtr destPtr = wb.BackBuffer + y * wb.BackBufferStride + x * 4; + for (int row = 0; row < h; row++) + { + System.Runtime.InteropServices.Marshal.Copy( + pixels, row * stride, + destPtr + row * wb.BackBufferStride, + stride); + } + wb.AddDirtyRect(new System.Windows.Int32Rect(x, y, w, h)); + } + finally + { + wb.Unlock(); + } + } + + return new XImage(wb, XImageType.PNG); + } + } +} diff --git a/FancyInput/Resources/Icons/Icon.ico b/FancyInput/Resources/Icons/Icon.ico new file mode 100644 index 0000000..6341163 Binary files /dev/null and b/FancyInput/Resources/Icons/Icon.ico differ diff --git a/FancyInput/Resources/Icons/Icon.png b/FancyInput/Resources/Icons/Icon.png new file mode 100644 index 0000000..08184ce Binary files /dev/null and b/FancyInput/Resources/Icons/Icon.png differ diff --git a/FancyInput/Resources/QRCodes/bilibili.jpg b/FancyInput/Resources/QRCodes/bilibili.jpg new file mode 100644 index 0000000..b9fa1bb Binary files /dev/null and b/FancyInput/Resources/QRCodes/bilibili.jpg differ diff --git a/FancyInput/Resources/QRCodes/wechat.jpg b/FancyInput/Resources/QRCodes/wechat.jpg new file mode 100644 index 0000000..1f07716 Binary files /dev/null and b/FancyInput/Resources/QRCodes/wechat.jpg differ diff --git a/FancyInput/Resources/QRCodes/xlworkspace.jpg b/FancyInput/Resources/QRCodes/xlworkspace.jpg new file mode 100644 index 0000000..dac3941 Binary files /dev/null and b/FancyInput/Resources/QRCodes/xlworkspace.jpg differ diff --git a/FancyInput/Resources/Styles/BorderStyles.xaml b/FancyInput/Resources/Styles/BorderStyles.xaml new file mode 100644 index 0000000..dc9cc7d --- /dev/null +++ b/FancyInput/Resources/Styles/BorderStyles.xaml @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/FancyInput/Resources/Styles/ButtonStyles.xaml b/FancyInput/Resources/Styles/ButtonStyles.xaml new file mode 100644 index 0000000..f1eb513 --- /dev/null +++ b/FancyInput/Resources/Styles/ButtonStyles.xaml @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FancyInput/Resources/Styles/CheckBoxStyles.xaml b/FancyInput/Resources/Styles/CheckBoxStyles.xaml new file mode 100644 index 0000000..691f94b --- /dev/null +++ b/FancyInput/Resources/Styles/CheckBoxStyles.xaml @@ -0,0 +1,224 @@ + + + + + + + \ No newline at end of file diff --git a/FancyInput/Resources/Styles/ComboBoxStyles.xaml b/FancyInput/Resources/Styles/ComboBoxStyles.xaml new file mode 100644 index 0000000..1853b04 --- /dev/null +++ b/FancyInput/Resources/Styles/ComboBoxStyles.xaml @@ -0,0 +1,133 @@ + + + + + + + + \ No newline at end of file diff --git a/FancyInput/Resources/Styles/ControlStyles.xaml b/FancyInput/Resources/Styles/ControlStyles.xaml new file mode 100644 index 0000000..2678aed --- /dev/null +++ b/FancyInput/Resources/Styles/ControlStyles.xaml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/FancyInput/Resources/Styles/LabelStyles.xaml b/FancyInput/Resources/Styles/LabelStyles.xaml new file mode 100644 index 0000000..fdec4a0 --- /dev/null +++ b/FancyInput/Resources/Styles/LabelStyles.xaml @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/FancyInput/Resources/Styles/MenuStyles.xaml b/FancyInput/Resources/Styles/MenuStyles.xaml new file mode 100644 index 0000000..7698427 --- /dev/null +++ b/FancyInput/Resources/Styles/MenuStyles.xaml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FancyInput/Resources/Styles/ScrollViewerStyles.xaml b/FancyInput/Resources/Styles/ScrollViewerStyles.xaml new file mode 100644 index 0000000..3855074 --- /dev/null +++ b/FancyInput/Resources/Styles/ScrollViewerStyles.xaml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FancyInput/Resources/Styles/SliderStyles.xaml b/FancyInput/Resources/Styles/SliderStyles.xaml new file mode 100644 index 0000000..39c2641 --- /dev/null +++ b/FancyInput/Resources/Styles/SliderStyles.xaml @@ -0,0 +1,266 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/FancyInput/Resources/Styles/TextBoxStyles.xaml b/FancyInput/Resources/Styles/TextBoxStyles.xaml new file mode 100644 index 0000000..57cd14a --- /dev/null +++ b/FancyInput/Resources/Styles/TextBoxStyles.xaml @@ -0,0 +1,66 @@ + + + + + + \ No newline at end of file diff --git a/FancyInput/ViewModels/ElementSegmentViewModel.cs b/FancyInput/ViewModels/ElementSegmentViewModel.cs new file mode 100644 index 0000000..acd8a9c --- /dev/null +++ b/FancyInput/ViewModels/ElementSegmentViewModel.cs @@ -0,0 +1,308 @@ +using FancyInput.Common; +using FancyInput.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace FancyInput.ViewModels +{ + public class ElementSegmentViewModel: ViewModelBase + { + private const double DefaultCanvasSize = 100.0; + + private double _canvasWidthForEditor = DefaultCanvasSize; + private double _canvasHeightForEditor = DefaultCanvasSize; + + private string _name = string.Empty; + public string Name + { + get => _name; + set=> SetProperty(ref _name, value); + } + + private XImage? _image; + public XImage? Image + { + get => _image; + set + { + SetProperty(ref _image, value); + OnPropertyChanged(nameof(ImageSource)); + OnPropertyChanged(nameof(Width)); + OnPropertyChanged(nameof(Height)); + if (!(_image!=null && value !=null && _image.Width == value.Width && _image.Height == value.Height)) + RecalculateEditorRanges(); + } + } + + public ImageSource? ImageSource => Image?.BitmapSource; + public double Width => Image != null ? Image.Width : 0; + public double Height => Image != null ? Image.Height : 0; + private bool _isVisible = true; + public bool IsVisible + { + get => _isVisible; + set + { + SetProperty(ref _isVisible, value); + OnPropertyChanged(nameof(SegmentVisibility)); + OnPropertyChanged(nameof(VisibilityIcon)); + OnPropertyChanged(nameof(VisibilityToolTip)); + OnPropertyChanged(nameof(VisibilityButtonBrush)); + } + } + public Visibility SegmentVisibility => IsVisible ? Visibility.Visible : Visibility.Collapsed; + public Geometry VisibilityIcon => _isVisible ? PathDataGeometry.EyeOn : PathDataGeometry.EyeOff; + public string VisibilityToolTip => _isVisible ? "隐藏" : "显示"; + public Brush VisibilityButtonBrush => _isVisible ?new SolidColorBrush(Colors.White): new SolidColorBrush( Color.FromArgb(0xFF, 0xF4, 0x8C, 0x8C)); + + private bool _isLocked = false; + public bool IsLocked + { + get => _isLocked; + set + { + SetProperty(ref _isLocked, value); + OnPropertyChanged(nameof(LockIcon)); + OnPropertyChanged(nameof(LockToolTip)); + OnPropertyChanged(nameof(LockButtonBrush)); + OnPropertyChanged(nameof(IsHitTestVisible)); + } + } + public bool IsHitTestVisible => !IsLocked; + public Geometry LockIcon => _isLocked? PathDataGeometry.LockOn : PathDataGeometry.LockOff; + public string LockToolTip => _isLocked ? "解锁" : "锁定"; + public Brush LockButtonBrush => _isLocked ? new SolidColorBrush( Color.FromArgb(0xFF, 0xF4, 0x8C, 0x8C)) : new SolidColorBrush(Colors.White); + + private bool _isSelected = false; + public bool IsSelected + { + get => _isSelected; + set + { + SetProperty(ref _isSelected, value); + OnPropertyChanged(nameof(SelectColor1)); + OnPropertyChanged(nameof(SelectColor2)); + } + } + + public Brush SelectColor1 => new SolidColorBrush(Colors.Gray); + public Brush SelectColor2 => new SolidColorBrush(_isSelected ? Color.FromRgb(0xFF,0xF2,0xFF) : Colors.White); + + private double _localScaleMin = 0.2; + public double LocalScaleMin + { + get => _localScaleMin; + private set => SetProperty(ref _localScaleMin, value); + } + + private double _localScaleMax = 4.0; + public double LocalScaleMax + { + get => _localScaleMax; + private set => SetProperty(ref _localScaleMax, value); + } + + private double _offsetXMin = -100.0; + public double OffsetXMin + { + get => _offsetXMin; + private set => SetProperty(ref _offsetXMin, value); + } + + private double _offsetXMax = 100.0; + public double OffsetXMax + { + get => _offsetXMax; + private set => SetProperty(ref _offsetXMax, value); + } + + private double _offsetYMin = -100.0; + public double OffsetYMin + { + get => _offsetYMin; + private set => SetProperty(ref _offsetYMin, value); + } + + private double _offsetYMax = 100.0; + public double OffsetYMax + { + get => _offsetYMax; + private set => SetProperty(ref _offsetYMax, value); + } + + private double _alphaMin = 0.0; + public double AlphaMin + { + get => _alphaMin; + private set => SetProperty(ref _alphaMin, value); + } + + private double _alphaMax = 1.0; + public double AlphaMax + { + get => _alphaMax; + private set => SetProperty(ref _alphaMax, value); + } + + private double _localScaleWheelFactor = 1.03; + public double LocalScaleWheelFactor + { + get => _localScaleWheelFactor; + private set => SetProperty(ref _localScaleWheelFactor, value); + } + + private double _offsetXWheelStep = 1.0; + public double OffsetXWheelStep + { + get => _offsetXWheelStep; + private set => SetProperty(ref _offsetXWheelStep, value); + } + + private double _offsetYWheelStep = 1.0; + public double OffsetYWheelStep + { + get => _offsetYWheelStep; + private set => SetProperty(ref _offsetYWheelStep, value); + } + + private double _alphaWheelStep = 0.02; + public double AlphaWheelStep + { + get => _alphaWheelStep; + private set => SetProperty(ref _alphaWheelStep, value); + } + + private double _alpha = 0.5; + public double Alpha + { + get => _alpha; + set + { + if (IsLocked) + { + return; + } + SetProperty(ref _alpha, Clamp(value, AlphaMin, AlphaMax)); + } + } + + + public event Action? SegmentChanged; + private void OnSegmentChanged() => SegmentChanged?.Invoke(); + private double _localScale = 1.1; + public double LocalScale + { + get => _localScale; + set + { + if (IsLocked) + { + return; + } + + if (SetProperty(ref _localScale, Clamp(value, LocalScaleMin, LocalScaleMax))) + { + OnSegmentChanged(); + } + } + } + + private double offsetX = 0.0; + public double OffsetX + { + get => offsetX; + set + { + if (IsLocked) + { + return; + } + + if (SetProperty(ref offsetX, Clamp(value, OffsetXMin, OffsetXMax))) + { + OnSegmentChanged(); + } + } + } + + private double offsetY = 0.0; + public double OffsetY + { + get => offsetY; + set + { + if (IsLocked) + { + return; + } + + if (SetProperty(ref offsetY, Clamp(value, OffsetYMin, OffsetYMax))) + { + OnSegmentChanged(); + } + } + } + + public void ConfigureEditorRanges(double canvasWidth, double canvasHeight) + { + _canvasWidthForEditor = Math.Max(1.0, canvasWidth); + _canvasHeightForEditor = Math.Max(1.0, canvasHeight); + RecalculateEditorRanges(); + } + + private void RecalculateEditorRanges() + { + double imageWidth = Math.Max(1.0, Width); + double imageHeight = Math.Max(1.0, Height); + double canvasWidth = Math.Max(1.0, _canvasWidthForEditor); + double canvasHeight = Math.Max(1.0, _canvasHeightForEditor); + + double fitScale = Math.Min(canvasWidth / imageWidth, canvasHeight / imageHeight); + if (double.IsNaN(fitScale) || double.IsInfinity(fitScale) || fitScale <= 0) + { + fitScale = 1.0; + } + + LocalScaleMin = Clamp(fitScale * 0.25, 0.05, 1.0); + LocalScaleMax = Clamp(fitScale * 8.0, 1.5, 20.0); + if (LocalScaleMax < LocalScaleMin * 1.5) + { + LocalScaleMax = LocalScaleMin * 1.5; + } + + double offsetXSpan = Math.Max(canvasWidth, imageWidth) * 1.2; + double offsetYSpan = Math.Max(canvasHeight, imageHeight) * 1.2; + OffsetXMin = -offsetXSpan; + OffsetXMax = offsetXSpan; + OffsetYMin = -offsetYSpan; + OffsetYMax = offsetYSpan; + + AlphaMin = 0.0; + AlphaMax = 1.0; + + LocalScaleWheelFactor = fitScale < 0.75 ? 1.035 : 1.03; + OffsetXWheelStep = Math.Max(1.0, offsetXSpan / 200.0); + OffsetYWheelStep = Math.Max(1.0, offsetYSpan / 200.0); + AlphaWheelStep = 0.02; + + LocalScale = LocalScale; + OffsetX = OffsetX; + OffsetY = OffsetY; + Alpha = Alpha; + } + + private static double Clamp(double value, double min, double max) + { + if (value < min) return min; + if (value > max) return max; + return value; + } + + } +} diff --git a/FancyInput/ViewModels/ElementTreeViewModel.cs b/FancyInput/ViewModels/ElementTreeViewModel.cs new file mode 100644 index 0000000..f8ce854 --- /dev/null +++ b/FancyInput/ViewModels/ElementTreeViewModel.cs @@ -0,0 +1,756 @@ +using FancyInput.Models; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +//using System.Windows.Forms; +using System.Windows.Media; +using System.IO; + +using SaveFileDialog = System.Windows.Forms.SaveFileDialog; +using DialogResult = System.Windows.Forms.DialogResult; +using System.Diagnostics; +using System.Collections.Specialized; +namespace FancyInput.ViewModels +{ + public class ElementTreeViewModel : INotifyPropertyChanged,IDisposable + { + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + protected virtual bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") + { + if (Equals(field, value)) return false; + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return true; + } + + public string Name { get; set; } = string.Empty; + public LoadMode LoadMode { get; set; } = LoadMode.PngJson; + public string PngFilePath { get; set; } = string.Empty; + public string JsonFilePath { get; set; } = string.Empty; + public string ProjectFilePath { get; set; } = string.Empty; + + private GamepadUserIdx _gamepadUserIndex = GamepadUserIdx.One; + public GamepadUserIdx GamepadUserIndex + { + get => _gamepadUserIndex; + set + { + for (int i = 0; i < ElementViewModels.Count; i++) + { + var elementViewModel = ElementViewModels[i]; + switch (elementViewModel.ElementType) + { + case ElementType.GamepadButton: + case ElementType.GamepadTrigger: + case ElementType.AnalogStick: + case ElementType.DPad: + case ElementType.GamepadPlayerId: + elementViewModel.GamepadUserIndex = value; + break; + case ElementType.Texture: + case ElementType.KeyboardButton: + case ElementType.MouseButton: + case ElementType.MouseWheel: + case ElementType.MouseMovement: + break; + default: + break; + } + } + } + } + public bool IsGamepad + { + get + { + for (int i = 0; i < ElementViewModels.Count; i++) + { + switch (ElementViewModels[i].ElementType) + { + case ElementType.GamepadButton: + case ElementType.GamepadTrigger: + case ElementType.AnalogStick: + case ElementType.DPad: + case ElementType.GamepadPlayerId: + return true; + case ElementType.Texture: + case ElementType.KeyboardButton: + case ElementType.MouseButton: + case ElementType.MouseWheel: + case ElementType.MouseMovement: + break; + default: + break; + } + } + return false; + } + } + + //private InputParser? _inputParser; + public InputParser? InputParser { get; set; } = null; + private InputHandler? _inputHandler; + public InputHandler InputHandler => _inputHandler ??= new InputHandler(this); + + private bool _isTestEnabled = false; + public bool IsTestEnabled + { + get => _isTestEnabled; + set + { + SetProperty(ref _isTestEnabled, value); + if (InputParser != null) + { + if (value) + { + InputParser.GetInput -= InputParser_GetInput; + InputParser.GetInput += InputParser_GetInput; + } + else + { + InputParser.GetInput -= InputParser_GetInput; + } + } + } + } + public void InputParser_GetInput(object? sender, InputArgs e) + { + InputHandler.Handle(e); + } + + public void Dispose() + { + if (InputParser != null) + { + InputParser.GetInput -= InputParser_GetInput; + InputParser = null; + } + if (_inputHandler != null) + { + _inputHandler.Dispose(); + } + foreach (var element in ElementViewModels) + { + element.ParentCollection = null; + } + } + + private Color _imageBackGroundColor = Colors.Black; + public Color ImageBackGroundColor + { + get => _imageBackGroundColor; + set + { + SetProperty(ref _imageBackGroundColor, value); + OnPropertyChanged(nameof(ImageBackground)); + } + } + public SolidColorBrush ImageBackground => new SolidColorBrush(_imageBackGroundColor); + + ObservableCollection _elementViewModels = new(); + public ObservableCollection ElementViewModels + { + get => _elementViewModels; + set => SetProperty(ref _elementViewModels, value); + } + + private ObservableCollection _selectedElementViewModels = new(); + public ObservableCollection SelectedElementViewModels + { + get => _selectedElementViewModels; + set => SetProperty(ref _selectedElementViewModels, value); + } + + + private bool _isSelectedLocked = false; + public bool IsSelectedLocked + { + get => _isSelectedLocked; + set + { + SetProperty(ref _isSelectedLocked, value); + foreach (var element in SelectedElementViewModels.ToList()) + { + element.IsLocked = value; + } + if (value) + { + RemoveLockedSelectedElements(); + } + } + } + private bool _isAllLocked = false; + public bool IsAllLocked + { + get => _isAllLocked; + set + { + SetProperty(ref _isAllLocked, value); + foreach (var element in ElementViewModels) + { + element.IsLocked = value; + } + if (value) + { + RemoveLockedSelectedElements(); + } + } + } + + public void RemoveLockedSelectedElements() + { + for (int i = _selectedElementViewModels.Count - 1; i >= 0; i--) + { + if (_selectedElementViewModels[i].IsLocked) + { + _selectedElementViewModels.RemoveAt(i); + } + } + } + + private bool _isSelectedHidden = false; + public bool IsSelectedHidden + { + get => _isSelectedHidden; + set + { + SetProperty(ref _isSelectedHidden, value); + foreach (var element in SelectedElementViewModels) + { + element.IsVisible = !value; + } + } + } + + public ElementViewModel? SelectedElementViewModel => _selectedElementViewModels.Count==0?null: _selectedElementViewModels[0]; + public int? SelectedIdx => SelectedElementViewModel?.Idx; + public bool HasSelectedElement => SelectedElementViewModel != null; + public Visibility HasSelectedVisibility => HasSelectedElement ? Visibility.Visible : Visibility.Collapsed; + public Visibility HasManySelectedVisibility => _selectedElementViewModels.Count > 1 ? Visibility.Visible : Visibility.Collapsed; + + private Visibility _selectionRectVisibility = Visibility.Collapsed; + public Visibility SelectionRectVisibility + { + get => _selectionRectVisibility; + set => SetProperty(ref _selectionRectVisibility, value); + } + + private double _selectionRectLeft = 0; + public double SelectionRectLeft + { + get => _selectionRectLeft; + set => SetProperty(ref _selectionRectLeft, value); + } + private double _selectionRectTop = 0; + public double SelectionRectTop + { + get => _selectionRectTop; + set => SetProperty(ref _selectionRectTop, value); + } + private double _selectionRectWidth = 0; + public double SelectionRectWidth + { + get => _selectionRectWidth; + set => SetProperty(ref _selectionRectWidth, value); + } + private double _selectionRectHeight = 0; + public double SelectionRectHeight + { + get => _selectionRectHeight; + set => SetProperty(ref _selectionRectHeight, value); + } + + //public const double MinScale = 0.1; + //public const double MaxScale = 10.0; + private double _canvasScale = 1.0; + public double CanvasScale + { + get => _canvasScale; + set => SetProperty(ref _canvasScale, value); + } + + public const double MIN_CANVAS_WIDTH = 800; + public const double MIN_CANVAS_Height = 600; + public const int MIN_TIGHT_CANVAS = 100; + + private const double _defaultCanvasSizeScale = 1.5; + public double MinCanvasWidth => TightCanvasWidth; + public double MinCanvasHeight => TightCanvasHeight; + public double MaxCanvasWidth => TightCanvasWidth * 4; + public double MaxCanvasHeight => TightCanvasHeight * 4; + + private double _canvasWidth; + public double CanvasWidth + { + get => _canvasWidth; + set + { + if (value < MinCanvasWidth) value = MinCanvasWidth; + if (value > MaxCanvasWidth) value = MaxCanvasWidth; + SetProperty(ref _canvasWidth, value); + } + } + + private double _canvasHeight; + public double CanvasHeight + { + get => _canvasHeight; + set + { + if (value < MinCanvasHeight) value = MinCanvasHeight; + if (value > MaxCanvasHeight) value = MaxCanvasHeight; + SetProperty(ref _canvasHeight, value); + } + } + + public double TightCanvasWidth + { + get + { + if (ElementViewModels.Count == 0) return MIN_CANVAS_WIDTH; + double maxX = ElementViewModels.Max(ev => ev.PosX + ev.MaxWidth*ev.Scale); + double maxMouseRadius = ElementViewModels.Max(ev => ev.MouseRadius * ev.Scale); + double maxGamepadRadius = ElementViewModels.Max(ev=>ev.Radius * ev.Scale); + double with = maxX + maxMouseRadius*2 + maxGamepadRadius*2; + return Math.Max(MIN_TIGHT_CANVAS, with); + } + } + + public double TightCanvasHeight + { + get + { + if (_elementViewModels.Count == 0) return MIN_CANVAS_Height; + double maxY = ElementViewModels.Max(ev => ev.PosY + ev.MaxHeight * ev.Scale); + double maxMouseRadius = ElementViewModels.Max(ev => ev.MouseRadius * ev.Scale); + double maxGamepadRadius = ElementViewModels.Max(ev => ev.Radius * ev.Scale); + double height = maxY + maxMouseRadius * 2 + maxGamepadRadius * 2; + return Math.Max(MIN_TIGHT_CANVAS, height); + } + } + + private XImage? _atlas; + public XImage? Atlas + { + get => _atlas; + set => SetProperty(ref _atlas, value); + } + + public event Action? ElementWARNingEvent; + public ElementTreeViewModel() { Initialize();} + + public ElementTreeViewModel(string fipPath) + { + var bytes = File.ReadAllBytes(fipPath); + var dto = System.Text.Json.JsonSerializer.Deserialize(bytes); + if (dto == null) + throw new Exception($"项目文件:{fipPath}解析失败!"); + var loadedVm = ElementTreeViewModel.FromDto(dto); + // 拷贝属性 + this.Name = loadedVm.Name; + this.ImageBackGroundColor = loadedVm.ImageBackGroundColor; + this.ElementViewModels.Clear(); + foreach (var evm in loadedVm.ElementViewModels) + this.ElementViewModels.Add(evm); + Initialize(); + } + + public ElementTreeViewModel(string pngPath, string jsonPath) + { + XImage inputImage = new XImage(pngPath); + OverlayRoot overlayRoot = OverlayParser.Parse(jsonPath); + for (int i = 0; i < overlayRoot.elements.Count; i++) + { + ElementViewModel elementViewModel = ElementViewModel.FromOverlayElement(overlayRoot.elements[i], inputImage); + _elementViewModels.Add(elementViewModel); + } + Initialize(); + } + + public ElementTreeViewModel(OverlayRoot overlayRoot, XImage image) + { + Atlas = image; + for (int i = 0; i < overlayRoot.elements.Count; i++) + { + ElementViewModel elementViewModel = ElementViewModel.FromOverlayElement(overlayRoot.elements[i], image); + _elementViewModels.Add(elementViewModel); + } + Initialize(); + } + + private void Initialize() + { + ReIndexing(); + for (int i = 0; i < _elementViewModels.Count; i++) + { + _elementViewModels[i].ParentCollection = _elementViewModels; + } + foreach (ElementViewModel elementViewModel in _elementViewModels) + { + int? anchorIdx = elementViewModel.OffsetAnchorSavedIdx; + if (anchorIdx != null && anchorIdx>=0 && anchorIdx < _elementViewModels.Count) + { + ElementViewModel? anchor = _elementViewModels[(int)anchorIdx]; + elementViewModel.OffsetAnchorViewModel = anchor; + } + } + _selectedElementViewModels.CollectionChanged -= SelectedElementViewModels_CollectionChanged; + _selectedElementViewModels.CollectionChanged += SelectedElementViewModels_CollectionChanged; + CanvasWidth = TightCanvasWidth * _defaultCanvasSizeScale; + CanvasHeight = TightCanvasHeight * _defaultCanvasSizeScale; + } + + public void RemoveAt(int idx) + { + if (idx < 0 || idx >= _elementViewModels.Count) return; + _elementViewModels[idx].Dispose(); + _elementViewModels.RemoveAt(idx); + ReIndexing(); + } + + public void UpMoveAt(int idx) + { + if (idx <= 0 || idx >= _elementViewModels.Count) return; + var temp = _elementViewModels[idx - 1]; + _elementViewModels[idx - 1] = _elementViewModels[idx]; + _elementViewModels[idx] = temp; + ReIndexing(); + } + + public void DownMoveAt(int idx) + { + if (idx < 0 || idx >= _elementViewModels.Count - 1) return; + var temp = _elementViewModels[idx + 1]; + _elementViewModels[idx + 1] = _elementViewModels[idx]; + _elementViewModels[idx] = temp; + ReIndexing(); + } + + public void TopMoveAt(int idx) + { + if (idx <= 0 || idx >= _elementViewModels.Count) return; + var temp = _elementViewModels[idx]; + _elementViewModels.RemoveAt(idx); + _elementViewModels.Insert(0, temp); + ReIndexing(); + } + public void BottomMoveAt(int idx) + { + if (idx < 0 || idx >= _elementViewModels.Count - 1) return; + var temp = _elementViewModels[idx]; + _elementViewModels.RemoveAt(idx); + _elementViewModels.Add(temp); + ReIndexing(); + } + + public void AddElement(ElementViewModel elementViewModel) + { + elementViewModel.Idx = _elementViewModels.Count; + _elementViewModels.Add(elementViewModel); + elementViewModel.ParentCollection = _elementViewModels; + ReIndexing(); + } + + + public void ReIndexing() + { + + for (int i = 0; i < _elementViewModels.Count; i++) + { + _elementViewModels[i].Idx = i; + } + OnPropertyChanged(nameof(ElementViewModels)); + OnPropertyChanged(nameof(CanvasWidth)); + OnPropertyChanged(nameof(CanvasHeight)); + OnPropertyChanged(nameof(TightCanvasWidth)); + OnPropertyChanged(nameof(TightCanvasHeight)); + if (InputHandler != null) + InputHandler.ResetRespondersAndDictionaries(); + } + + + private void SelectedElementViewModels_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(SelectedElementViewModel)); + OnPropertyChanged(nameof(HasSelectedVisibility)); + OnPropertyChanged(nameof(HasManySelectedVisibility)); + foreach (var element in ElementViewModels) + { + element.IsSelected = SelectedElementViewModels.Contains(element); + } + } + + public bool HasCircleChainIfSelectedBind(ElementViewModel vm) + { + var selectedSnapshot = SelectedElementViewModels.ToList(); + var originalAnchors = selectedSnapshot + .Select(s => s.OffsetAnchorViewModel) + .ToList(); + + try + { + // 临时套用目标绑定 + foreach (var selected in selectedSnapshot) + { + selected.SetPrivateOffsetAnchor(vm); + } + + // 真正的环检测:访问过同一节点即成环 + foreach (var start in ElementViewModels) + { + var visited = new HashSet(); + var cur = start.OffsetAnchorViewModel; + while (cur != null) + { + if (!visited.Add(cur)) + { + return true; + } + cur = cur.OffsetAnchorViewModel; + } + } + + return false; + } + finally + { + // 保证恢复 + for (int i = 0; i < selectedSnapshot.Count; i++) + { + selectedSnapshot[i].SetPrivateOffsetAnchor(originalAnchors[i]); + } + } + } + + public void Export(ExportType exportType) + { + SaveFileDialog saveFileDialog = new SaveFileDialog(); + saveFileDialog.Filter = (exportType == ExportType.PngAndJson) ? "JSON 文件 (*.json)|*.json" : "FancyInput 项目文件 (*.fip)|*.fip"; + + if (saveFileDialog.ShowDialog() == DialogResult.OK) + { + string fileName = saveFileDialog.FileName; + if (exportType == ExportType.ProjectFile) + { + var dto = ToDto(this); + var bytes = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(dto); + File.WriteAllBytes(fileName, bytes); + FancyInput.AppMessageBox.Show($"项目文件已成功保存到:{fileName}", "导出成功", MessageBoxButton.OK, MessageBoxImage.Information); + } + else + { + if (Atlas == null) + GenerateAtlas(); + if (Atlas == null) + { + FancyInput.AppMessageBox.Show("图集生成失败,无法导出JSON文件!"); + return; + } + + OverlayRoot overlayRoot = new OverlayRoot(); + overlayRoot.elements = new List(); + + foreach (var evm in _elementViewModels) + { + overlayRoot.elements.Add(evm.ToOverlayElement()); + } + overlayRoot.overlay_height = (int)TightCanvasHeight; + overlayRoot.overlay_width = (int)TightCanvasWidth; + + var options = new System.Text.Json.JsonSerializerOptions + { + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true + }; + string json = System.Text.Json.JsonSerializer.Serialize(overlayRoot, options); + System.IO.File.WriteAllText(saveFileDialog.FileName, json); + + string pngName = System.IO.Path.ChangeExtension(saveFileDialog.FileName, ".png"); + Atlas.Save(pngName); + FancyInput.AppMessageBox.Show($"PNG纹理图集和JSON文件已成功保存到:\n{pngName}\n{saveFileDialog.FileName}", "导出成功", MessageBoxButton.OK, MessageBoxImage.Information); + } + } + } + + public void GenerateAtlas() + { + List concatedImages = new List(); + for (int i = 0; i < _elementViewModels.Count; i++) + { + _elementViewModels[i].Idx = i; + ElementType elementType = _elementViewModels[i].ElementType; + XImage? concatedImage = null; + List elementImages = new List(); + XImage? xImage0 = _elementViewModels[i][0]; + XImage? xImagelocal = null; + if (xImage0 != null) + { + elementImages.Add(xImage0); + } + else + { + FancyInput.AppMessageBox.Show($"元素{i},Id:{_elementViewModels[i].Id}缺少图片,无法生成图集!"); + return; + } + switch (elementType) + { + case ElementType.Texture: + concatedImage = xImage0.Copy(); + break; + case ElementType.KeyboardButton: + case ElementType.GamepadButton: + case ElementType.MouseButton: + case ElementType.GamepadTrigger: + case ElementType.AnalogStick: + xImagelocal = _elementViewModels[i][1]; + if (xImagelocal != null) + elementImages.Add(xImagelocal); + else + { + elementImages.Add(xImage0.Copy()); + ElementWARNingEvent?.Invoke(_elementViewModels[i], "缺少按下状态图片,已使用默认图片代替!"); + } + concatedImage = XImage.ConcatImages(elementImages, ElementViewModel.SPACE_BETWEEN_IMAGES, 0); + break; + case ElementType.MouseWheel: + for (int j = 1; j < 4; j++) + { + xImagelocal = _elementViewModels[i][j]; + if (xImagelocal != null) + elementImages.Add(xImagelocal); + else + { + elementImages.Add(xImage0.Copy()); + ElementWARNingEvent?.Invoke(_elementViewModels[i], $"缺少状态{j}图片,已使用默认图片代替!"); + } + } + concatedImage = XImage.ConcatImages(elementImages, ElementViewModel.SPACE_BETWEEN_IMAGES, 1); + break; + case ElementType.DPad: + for (int j = 1; j < 9; j++) + { + xImagelocal = _elementViewModels[i][j]; + if (xImagelocal != null) + elementImages.Add(xImagelocal); + else + { + elementImages.Add(xImage0.Copy()); + ElementWARNingEvent?.Invoke(_elementViewModels[i], $"缺少状态{j}图片,已使用默认图片代替!"); + } + } + concatedImage = XImage.ConcatImages(elementImages, ElementViewModel.SPACE_BETWEEN_IMAGES, 1); + break; + case ElementType.GamepadPlayerId: + for (int j = 1; j < 5; j++) + { + xImagelocal = _elementViewModels[i][j]; + if (xImagelocal != null) + elementImages.Add(xImagelocal); + else + { + elementImages.Add(xImage0.Copy()); + ElementWARNingEvent?.Invoke(_elementViewModels[i], $"缺少状态{j}图片,已使用默认图片代替!"); + } + } + concatedImage = XImage.ConcatImages(elementImages, ElementViewModel.SPACE_BETWEEN_IMAGES, 1); + break; + case ElementType.MouseMovement: + concatedImage = xImage0.Copy(); + ElementWARNingEvent?.Invoke(_elementViewModels[i], "该元素类型不支持生成图集!"); + break; + default: + throw new ArgumentOutOfRangeException($"不支持的元素类型:{elementType}"); + } + if (concatedImage == null) + { + FancyInput.AppMessageBox.Show($"元素{i},Id:{_elementViewModels[i].Id}图集生成失败!"); + return; + } + concatedImages.Add(concatedImage); + } + var result = XImage.PackImages(concatedImages); + Atlas = result.atlas; + var placements = result.placements; + for (int i = 0; i < placements.Count; i++) + { + var placedImage = placements[i]; + if (placedImage == null) + { + ElementWARNingEvent?.Invoke(_elementViewModels[i], "图集打包失败,元素位置未更新!"); + continue; + } + XImage image = placedImage.Image; + int Index = placedImage.Index; + int X = placedImage.X; + int Y = placedImage.Y; + if (Index >= 0 && Index < _elementViewModels.Count) + { + _elementViewModels[Index].MappingU = X; + _elementViewModels[Index].MappingV = Y; + } + } + } + + public static ElementTreeViewModelDto ToDto(ElementTreeViewModel vm) + { + var ElementViewModelDTOs = vm.ElementViewModels.Select(ElementViewModel.ToDto).ToList(); + return new ElementTreeViewModelDto + { + Name = vm.Name, + LoadMode = vm.LoadMode, + PngFilePath = vm.PngFilePath, + JsonFilePath = vm.JsonFilePath, + ProjectFilePath = vm.ProjectFilePath, + ImageBackGroundColor = new byte[] { + vm.ImageBackGroundColor.A, + vm.ImageBackGroundColor.R, + vm.ImageBackGroundColor.G, + vm.ImageBackGroundColor.B + }, + Elements = ElementViewModelDTOs + }; + } + + public static ElementTreeViewModel FromDto(ElementTreeViewModelDto dto) + { + var vm = new ElementTreeViewModel + { + Name = dto.Name, + LoadMode = dto.LoadMode, + PngFilePath = dto.PngFilePath, + JsonFilePath = dto.JsonFilePath, + ProjectFilePath = dto.ProjectFilePath, + ImageBackGroundColor = dto.ImageBackGroundColor.Length == 4 + ? Color.FromArgb(dto.ImageBackGroundColor[0], dto.ImageBackGroundColor[1], dto.ImageBackGroundColor[2], dto.ImageBackGroundColor[3]) + : Colors.Black + }; + foreach (var elementDto in dto.Elements) + { + vm.ElementViewModels.Add(ElementViewModel.FromDto(elementDto)); + } + vm.ReIndexing(); + return vm; + } + } + + public class ElementTreeViewModelDto + { + public string Name { get; set; } = string.Empty; + public LoadMode LoadMode { get; set; } = LoadMode.PngJson; + public string PngFilePath { get; set; } = string.Empty; + public string JsonFilePath { get; set; } = string.Empty; + public string ProjectFilePath { get; set; } = string.Empty; + public byte[] ImageBackGroundColor { get; set; } = Array.Empty(); // ARGB + public List Elements { get; set; } = new(); + } +} diff --git a/FancyInput/ViewModels/ElementViewModel.Data.cs b/FancyInput/ViewModels/ElementViewModel.Data.cs new file mode 100644 index 0000000..0e8f2a1 --- /dev/null +++ b/FancyInput/ViewModels/ElementViewModel.Data.cs @@ -0,0 +1,439 @@ +using FancyInput.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Text.Json.Serialization; +using System.Windows.Media.Imaging; +using System.Windows.Media; + +namespace FancyInput.ViewModels +{ + public partial class ElementViewModel + { + public void MakeCache(int frameRange) + { + if (ElementType != ElementType.GamepadTrigger) return; + XImage? startImage = this[0]; + XImage? endImage = this[1]; + if (startImage == null || endImage == null) return; + if (_triggerCacheImages != null) + { + _triggerCacheImages.Clear(); + } + int framesInBatch = frameRange + 1; + _triggerCacheImages = new List(); + for (int directionIdx = 1; directionIdx <= 4; directionIdx++) + { + Direction direction = (Direction)directionIdx; + for (int frame = 0; frame < framesInBatch; frame++) + { + double percent = (double)frame / frameRange; + XImage Merge = XImage.Merge(startImage, endImage, direction, percent); + _triggerCacheImages.Add(Merge); + } + } + } + + public ElementViewModel Copy() + { + ElementViewModel copy = new ElementViewModel() + { + ElementType = this.ElementType, + Id = this.Id, + Idx = this.Idx, + PosX = this.PosX, + PosY = this.PosY, + IsVisible = this.IsVisible, + OffsetAnchorViewModel = this.OffsetAnchorViewModel, + OffsetAnchorSavedIdx = this.OffsetAnchorSavedIdx, + MappingU = this.MappingU, + MappingV = this.MappingV, + MappingW = this.MappingW, + MappingH = this.MappingH, + LogScale = this.LogScale, + Radius = this.Radius, + MouseRadius = this.MouseRadius, + IsTriggerMode = this.IsTriggerMode, + ImageBackGroundColor = this.ImageBackGroundColor, + UseStopwatch = this.UseStopwatch, + StopwatchMilliseconds = this.StopwatchMilliseconds, + SelectedDirection = this.SelectedDirection, + SelectedSide = this.SelectedSide, + SelectedGamepadButton = this.SelectedGamepadButton, + SelectedKeyBoardButton = this.SelectedKeyBoardButton, + SelectedMouseButton = this.SelectedMouseButton, + SelectedMouseMoveType = this.SelectedMouseMoveType, + }; + for (int i = 0; i < MAX_IAMGE_NUM; i++) + { + copy[i] = this[i]?.Copy(); + copy.SegmentsScales[i] = this.SegmentsScales[i]; + copy.SegmentsOffsetX[i] = this.SegmentsOffsetX[i]; + copy.SegmentsOffsetY[i] = this.SegmentsOffsetY[i]; + } + return copy; + } + + public void CopyFrom(ElementViewModel viewModel, bool changeIdx = false) + { + this.ElementType = viewModel.ElementType; + this.Id = viewModel.Id; + this.PosX = viewModel.PosX; + this.PosY = viewModel.PosY; + this.IsVisible = viewModel.IsVisible; + this.OffsetAnchorViewModel = viewModel.OffsetAnchorViewModel; + this.OffsetAnchorSavedIdx = viewModel.OffsetAnchorSavedIdx; + this.MappingU = viewModel.MappingU; + this.MappingV = viewModel.MappingV; + this.MappingW = viewModel.MappingW; + this.MappingH = viewModel.MappingH; + this.LogScale = viewModel.LogScale; + this.Radius = viewModel.Radius; + this.MouseRadius = viewModel.MouseRadius; + this.IsTriggerMode = viewModel.IsTriggerMode; + this.ImageBackGroundColor = viewModel.ImageBackGroundColor; + this.UseStopwatch = viewModel.UseStopwatch; + this.StopwatchMilliseconds = viewModel.StopwatchMilliseconds; + this.SelectedDirection = viewModel.SelectedDirection; + this.SelectedSide = viewModel.SelectedSide; + this.SelectedKeyBoardButton = viewModel.SelectedKeyBoardButton; + this.SelectedGamepadButton = viewModel.SelectedGamepadButton; + this.SelectedMouseButton = viewModel.SelectedMouseButton; + this.SelectedMouseMoveType = viewModel.SelectedMouseMoveType; + for (int i = 0; i < MAX_IAMGE_NUM; i++) + { + this[i] = viewModel[i]?.Copy(); + this.SegmentsScales[i] = viewModel.SegmentsScales[i]; + this.SegmentsOffsetX[i] = viewModel.SegmentsOffsetX[i]; + this.SegmentsOffsetY[i] = viewModel.SegmentsOffsetY[i]; + } + if (changeIdx) + { + this.Idx = viewModel.Idx; + } + + } + + public static ElementViewModel FromOverlayElement(OverlayElement overlayElement, XImage texture) + { + ElementViewModel elementViewModel = new ElementViewModel() + { + ElementType = overlayElement.type, + Id = overlayElement.id, + PosX = overlayElement.pos.Length > 0 ? overlayElement.pos[0] : 0, + PosY = overlayElement.pos.Length > 1 ? overlayElement.pos[1] : 0, + MappingU = overlayElement.mapping.Length > 0 ? overlayElement.mapping[0] : 0, + MappingV = overlayElement.mapping.Length > 1 ? overlayElement.mapping[1] : 0, + MappingW = overlayElement.mapping.Length > 2 ? overlayElement.mapping[2] : texture.Width, + MappingH = overlayElement.mapping.Length > 3 ? overlayElement.mapping[3] : texture.Height, + }; + //Additional properties + switch (overlayElement.type) + { + case ElementType.GamepadButton: + if (overlayElement.code.HasValue) + { + elementViewModel.SelectedGamepadButton = (GamepadCodeType)overlayElement.code.Value; + } + break; + case ElementType.KeyboardButton: + if (overlayElement.code.HasValue) + { + elementViewModel.SelectedKeyBoardButton = (KeyBoardCodeType)overlayElement.code.Value; + } + break; + case ElementType.MouseButton: + if (overlayElement.code.HasValue) + { + elementViewModel.SelectedMouseButton = (MouseCodeType)overlayElement.code.Value; + } + break; + case ElementType.GamepadTrigger: + if (overlayElement.trigger_mode.HasValue) + { + elementViewModel.IsTriggerMode = overlayElement.trigger_mode.Value; + } + if (overlayElement.side.HasValue) + { + elementViewModel.SelectedSide = overlayElement.side.Value; + } + if (overlayElement.direction.HasValue) + { + elementViewModel.SelectedDirection = overlayElement.direction.Value; + } + break; + case ElementType.AnalogStick: + if (overlayElement.side.HasValue) + { + elementViewModel.SelectedSide = overlayElement.side.Value; + } + if (overlayElement.stick_radius.HasValue) + { + elementViewModel.Radius = overlayElement.stick_radius.Value; + } + break; + case ElementType.MouseMovement: + if (overlayElement.mouse_type.HasValue) + { + elementViewModel.SelectedMouseMoveType = overlayElement.mouse_type.Value; + } + if (overlayElement.mouse_radius.HasValue) + { + elementViewModel.MouseRadius = overlayElement.mouse_radius.Value; + } + break; + case ElementType.MouseWheel: + case ElementType.GamepadPlayerId: + case ElementType.Texture: + case ElementType.DPad: + // No additional properties + break; + } + //Croped images from texure + switch (overlayElement.type) + { + case ElementType.MouseButton: + case ElementType.KeyboardButton: + case ElementType.GamepadButton: + case ElementType.GamepadTrigger: + case ElementType.AnalogStick: + elementViewModel[0] = texture.Crop(elementViewModel.MappingU, elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH); + elementViewModel[1] = texture.Crop(elementViewModel.MappingU, elementViewModel.MappingV + elementViewModel.MappingH + SPACE_BETWEEN_IMAGES, + elementViewModel.MappingW, elementViewModel.MappingH); + break; + case ElementType.MouseWheel: + for (int i = 0; i < 4; i++) + { + elementViewModel[i] = texture.Crop(elementViewModel.MappingU + i * (elementViewModel.MappingW + SPACE_BETWEEN_IMAGES), + elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH); + } + break; + case ElementType.DPad: + for (int i = 0; i < MAX_IAMGE_NUM; i++) + { + elementViewModel[i] = texture.Crop(elementViewModel.MappingU + i * (elementViewModel.MappingW + SPACE_BETWEEN_IMAGES), + elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH); + } + break; + case ElementType.Texture: + elementViewModel[0] = texture.Crop(elementViewModel.MappingU, elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH); + break; + case ElementType.GamepadPlayerId: + for (int i = 0; i < 5; i++) + { + elementViewModel[i] = texture.Crop(elementViewModel.MappingU + i * (elementViewModel.MappingW + SPACE_BETWEEN_IMAGES), + elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH); + } + break; + case ElementType.MouseMovement: + elementViewModel[0] = texture.Crop(elementViewModel.MappingU, elementViewModel.MappingV, elementViewModel.MappingW, elementViewModel.MappingH); + break; + } + return elementViewModel; + } + + public OverlayElement ToOverlayElement() + { + OverlayElement overlayElement = new OverlayElement() + { + type = this.ElementType, + id = this.Id, + pos = new int[] { this.PosX, this.PosY }, + mapping = new int[] { this.MappingU, this.MappingV, this.MappingW, this.MappingH }, + }; + //Additional properties + switch (this.ElementType) + { + case ElementType.GamepadButton: + overlayElement.code = (int)this.SelectedGamepadButton; + break; + case ElementType.KeyboardButton: + overlayElement.code = (int)this.SelectedKeyBoardButton; + break; + case ElementType.MouseButton: + overlayElement.code = (int)this.SelectedMouseButton; + break; + case ElementType.GamepadTrigger: + overlayElement.trigger_mode = this.IsTriggerMode; + overlayElement.side = this.SelectedSide; + overlayElement.direction = this.SelectedDirection; + break; + case ElementType.AnalogStick: + overlayElement.side = this.SelectedSide; + overlayElement.stick_radius = this.Radius; + break; + case ElementType.MouseMovement: + overlayElement.mouse_type = this.SelectedMouseMoveType; + overlayElement.mouse_radius = this.MouseRadius; + break; + case ElementType.MouseWheel: + case ElementType.GamepadPlayerId: + case ElementType.Texture: + // No additional properties + break; + } + return overlayElement; + } + + public static ElementViewModelDto ToDto(ElementViewModel vm) + { + var dto = new ElementViewModelDto + { + ElementType = vm.ElementType, + Idx = vm.Idx, + Id = vm.Id, + PosX = vm.PosX, + PosY = vm.PosY, + IsVisible = vm.IsVisible, + OffsetAnchorSavedIdx = vm.OffsetAnchorIdx, + MappingU = vm.MappingU, + MappingV = vm.MappingV, + MappingW = vm.MappingW, + MappingH = vm.MappingH, + LogScale = vm.LogScale, + Radius = vm.Radius, + MouseRadius = vm.MouseRadius, + IsTriggerMode = vm.IsTriggerMode, + ImageBackGroundColor = new byte[] { vm.ImageBackGroundColor.A, vm.ImageBackGroundColor.R, + vm.ImageBackGroundColor.G, vm.ImageBackGroundColor.B }, + UseStopwatch = vm.UseStopwatch, + StopwatchMilliseconds = vm.StopwatchMilliseconds, + SelectedGamepadButton = vm.SelectedGamepadButton, + SelectedKeyBoardButton = vm.SelectedKeyBoardButton, + SelectedMouseButton = vm.SelectedMouseButton, + SelectedSide = vm.SelectedSide, + SelectedDirection = vm.SelectedDirection, + SelectedMouseMoveType = vm.SelectedMouseMoveType, + Images = new List(), + ImagesGifRaw = new List() + }; + + for (int i = 0; i < ElementViewModel.MAX_IAMGE_NUM; i++) + { + var img = vm[i]; + if (img == null) + { + dto.Images.Add(null); + dto.ImagesGifRaw.Add(null); + continue; + } + if (img.ImageType == XImageType.GIF) + { + // 保存GIF原始字节流 + if (img.BitmapSource is BitmapImage bi && img.GifRawBytes != null) + { + dto.ImagesGifRaw.Add(img.GifRawBytes); + } + else + { + dto.ImagesGifRaw.Add(null); + } + dto.Images.Add(null); + } + else + { + using var ms = new MemoryStream(); + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(img.BitmapSource)); + encoder.Save(ms); + dto.Images.Add(ms.ToArray()); + dto.ImagesGifRaw.Add(null); + } + } + return dto; + } + + public static ElementViewModel FromDto(ElementViewModelDto dto) + { + + var vm = new ElementViewModel + { + ElementType = dto.ElementType, + Idx = dto.Idx, + Id = dto.Id, + PosX = dto.PosX, + PosY = dto.PosY, + IsVisible = dto.IsVisible, + OffsetAnchorSavedIdx = dto.OffsetAnchorSavedIdx, + MappingU = dto.MappingU, + MappingV = dto.MappingV, + MappingW = dto.MappingW, + MappingH = dto.MappingH, + LogScale = dto.LogScale, + Radius = dto.Radius, + MouseRadius = dto.MouseRadius, + IsTriggerMode = dto.IsTriggerMode, + ImageBackGroundColor = dto.ImageBackGroundColor.Length == 4 + ? Color.FromArgb(dto.ImageBackGroundColor[0], dto.ImageBackGroundColor[1], dto.ImageBackGroundColor[2], dto.ImageBackGroundColor[3]) + : Colors.Black, + UseStopwatch = dto.UseStopwatch, + StopwatchMilliseconds = dto.StopwatchMilliseconds, + SelectedGamepadButton = dto.SelectedGamepadButton, + SelectedKeyBoardButton = dto.SelectedKeyBoardButton, + SelectedMouseButton = dto.SelectedMouseButton, + SelectedSide = dto.SelectedSide, + SelectedDirection = dto.SelectedDirection, + SelectedMouseMoveType = dto.SelectedMouseMoveType + }; + + for (int i = 0; i < ElementViewModel.MAX_IAMGE_NUM; i++) + { + if (dto.ImagesGifRaw.Count > i && dto.ImagesGifRaw[i] != null && dto.ImagesGifRaw[i]!.Length > 0) + { + var ms = new MemoryStream(dto.ImagesGifRaw[i]!); + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.StreamSource = ms; + bitmap.EndInit(); + vm[i] = new XImage(bitmap, XImageType.GIF, dto.ImagesGifRaw[i]); + } + else if (dto.Images.Count > i && dto.Images[i] != null && dto.Images[i]!.Length > 0) + { + using var ms = new MemoryStream(dto.Images[i]!); + var decoder = new PngBitmapDecoder(ms, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad); + vm[i] = new XImage(decoder.Frames[0], XImageType.PNG); + } + else + { + vm[i] = null; + } + } + return vm; + } + } + + public class ElementViewModelDto + { + public ElementType ElementType { get; set; } + public int Idx { get; set; } + public string Id { get; set; } = string.Empty; + public int PosX { get; set; } + public int PosY { get; set; } + public bool IsVisible { get; set; } = true; + public int? OffsetAnchorSavedIdx { get; set; } = null; + public int MappingU { get; set; } + public int MappingV { get; set; } + public int MappingW { get; set; } + public int MappingH { get; set; } + public double LogScale { get; set; } = 0.0; + public int Radius { get; set; } + public int MouseRadius { get; set; } + public bool IsTriggerMode { get; set; } + public byte[] ImageBackGroundColor { get; set; } = Array.Empty(); // ARGB + public bool UseStopwatch { get; set; } = false; + public int StopwatchMilliseconds { get; set; } = 50; + public GamepadCodeType SelectedGamepadButton { get; set; } + public KeyBoardCodeType SelectedKeyBoardButton { get; set; } + public MouseCodeType SelectedMouseButton { get; set; } + public Side SelectedSide { get; set; } + public Direction SelectedDirection { get; set; } + public MouseMoveType SelectedMouseMoveType { get; set; } + public List Images { get; set; } = new(); // PNG字节流,最多9张 + public List ImagesGifRaw { get; set; } = new(); // GIF原始字节流,最多9张 + } + +} diff --git a/FancyInput/ViewModels/ElementViewModel.Display.cs b/FancyInput/ViewModels/ElementViewModel.Display.cs new file mode 100644 index 0000000..1960a35 --- /dev/null +++ b/FancyInput/ViewModels/ElementViewModel.Display.cs @@ -0,0 +1,721 @@ +using FancyInput.Common; +using FancyInput.Models; +using FancyInput.Views.Controls; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +//using System.Windows.Forms; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +using Color = System.Windows.Media.Color; +using Cursor = System.Windows.Input.Cursor; +using Cursors = System.Windows.Input.Cursors; +namespace FancyInput.ViewModels +{ + public partial class ElementViewModel : INotifyPropertyChanged,IDisposable + { + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + protected virtual bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") + { + if (Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } + public AnimatedImage? AnimatedImageControl { get; set; } + + + private Color _imageBackGroundColor = Colors.Black; + public Color ImageBackGroundColor + { + get => _imageBackGroundColor; + set + { + SetProperty(ref _imageBackGroundColor, value); + OnPropertyChanged(nameof(ImageBackground)); + } + } + public SolidColorBrush ImageBackground => new SolidColorBrush(_imageBackGroundColor); + + public ElementType ElementType { get; set; } + + private GamepadUserIdx _gamepadUserIndex = GamepadUserIdx.One; + public GamepadUserIdx GamepadUserIndex + { + get => _gamepadUserIndex; + set + { + _gamepadUserIndex = value; + if (ElementType == ElementType.GamepadPlayerId) + CurrentSourceIdx = (int)value; + } + } + + private bool _isSelected = false; + public bool IsSelected + { + get => _isSelected; + set + { + SetProperty(ref _isSelected, value); + OnPropertyChanged(nameof(SelectColor1)); + OnPropertyChanged(nameof(SelectColor2)); + } + } + public SolidColorBrush SelectColor1 => new SolidColorBrush(_isSelected ? Color.FromArgb(0xFF, 0x65, 0x1F, 0x65) : Colors.Gray); + public SolidColorBrush SelectColor2 => new SolidColorBrush(_isSelected ? Color.FromArgb(0xAA, 0x65, 0x1F, 0x65) : Color.FromArgb(0x33, 0x3D, 0x3C, 0x3E)); + + public const int SPACE_BETWEEN_IMAGES = 3; + public const int MAX_IAMGE_NUM = 9; + + private string _id = string.Empty; + public string Id + { + get => _id; + set => SetProperty(ref _id, value); + } + + private bool _isVisible = true; + public bool IsVisible + { + get => _isVisible; + set + { + SetProperty(ref _isVisible, value); + OnPropertyChanged(nameof(ElementVisibility)); + OnPropertyChanged(nameof(VisibilityIcon)); + OnPropertyChanged(nameof(VisibilityToolTip)); + OnPropertyChanged(nameof(VisibilityButtonBrush)); + } + } + public Visibility ElementVisibility => IsVisible ? Visibility.Visible : Visibility.Collapsed; + public Geometry VisibilityIcon => _isVisible ? PathDataGeometry.EyeOn : PathDataGeometry.EyeOff; + public string VisibilityToolTip => _isVisible ? "隐藏" : "显示"; + + public Brush VisibilityButtonBrush => _isVisible ?new SolidColorBrush(Colors.White): new SolidColorBrush( Color.FromArgb(0xFF, 0xF4, 0x8C, 0x8C)); + + private bool _isLocked = false; + public bool IsLocked + { + get => _isLocked; + set + { + SetProperty(ref _isLocked, value); + OnPropertyChanged(nameof(LockIcon)); + OnPropertyChanged(nameof(LockToolTip)); + OnPropertyChanged(nameof(LockButtonBrush)); + OnPropertyChanged(nameof(IsHitTestVisible)); + } + } + public bool IsHitTestVisible => !IsLocked; + public Geometry LockIcon => _isLocked? PathDataGeometry.LockOn : PathDataGeometry.LockOff; + public string LockToolTip => _isLocked ? "解锁" : "锁定"; + public Brush LockButtonBrush => _isLocked ? new SolidColorBrush( Color.FromArgb(0xFF, 0xF4, 0x8C, 0x8C)) : new SolidColorBrush(Colors.White); + + + public double Scale => Math.Pow(10, _logScale); + public string ScaleString + { + get => Math.Pow(10, _logScale).ToString("0.##"); + set + { + if (double.TryParse(value, out double scale) && scale > 0) + { + double log = Math.Log10(scale); + if (log < -1) log = -1; + else if (log > 1) log = 1; + _logScale = log; + OnPropertyChanged(nameof(LogScale)); + OnPropertyChanged(nameof(Scale)); + OnPropertyChanged(nameof(ScaleString)); + } + } + } + private double _logScale = 0.0; + public double LogScale + { + get => _logScale; + set + { + SetProperty(ref _logScale, value); + OnPropertyChanged(nameof(Scale)); + OnPropertyChanged(nameof(ScaleString)); + } + } + + + private int _posX = 0; + public int PosX + { + get => _posX; + set + { + SetProperty(ref _posX, value); + OnPropertyChanged(nameof(VisualPosX)); + OnPropertyChanged(nameof(OverlayJSON)); + } + } + + private int _posY = 0; + public int PosY + { + get => _posY; + set + { + SetProperty(ref _posY, value); + OnPropertyChanged(nameof(VisualPosY)); + OnPropertyChanged(nameof(OverlayJSON)); + } + } + + public event Action? OffsetXChanged; + public event Action? OffsetYChanged; + private void OnOffsetXChanged(int offsetX) => OffsetXChanged?.Invoke(offsetX); + private void OnOffsetYChanged(int offsetY) => OffsetYChanged?.Invoke(offsetY); + private void HandleOffsetXChanged(int offsetX) => OffsetX = offsetX; + private void HandleOffsetYChanged(int offsetY) => OffsetY = offsetY; + private int offsetX = 0; + public int OffsetX + { + get => offsetX; + set + { + if(SetProperty(ref offsetX, value)) + { + OnPropertyChanged(nameof(OffsetX)); + OnPropertyChanged(nameof(VisualPosX)); + OnPropertyChanged(nameof(OverlayJSON)); + OnOffsetXChanged(offsetX); + } + } + } + private int offsetY = 0; + public int OffsetY + { + get => offsetY; + set + { + if (SetProperty(ref offsetY, value)) + { + OnPropertyChanged(nameof(OffsetY)); + OnPropertyChanged(nameof(VisualPosY)); + OnPropertyChanged(nameof(OverlayJSON)); + OnOffsetYChanged(offsetY); + } + } + } + public int VisualPosX => _posX + offsetX; + public int VisualPosY => _posY + offsetY; + + public event Action? IdxChanged; + private void OnIdxChanged() => IdxChanged?.Invoke(); + private void HandleOffsetAnchorIdxChanged() => OnPropertyChanged(nameof(OffsetAnchorIdx)); + private int idx = 0; + public int Idx + { + get => idx; + set + { + if (SetProperty(ref idx, value)) + { + OnIdxChanged(); + OnPropertyChanged(nameof(IdxString)); + } + } + } + public string IdxString => idx.ToString(); + public int? OffsetAnchorIdx=> _offsetAnchorViewModel?.Idx; + public int? OffsetAnchorSavedIdx { get; set; } = null; + private ObservableCollection? _parentCollection; + public ObservableCollection? ParentCollection + { + get => _parentCollection; + set => SetProperty(ref _parentCollection, value); + } + private ElementViewModel? _offsetAnchorViewModel; + public void SetPrivateOffsetAnchor(ElementViewModel? anchor)=> _offsetAnchorViewModel = anchor; + public ElementViewModel? OffsetAnchorViewModel + { + get => _offsetAnchorViewModel; + set + { + if (ReferenceEquals(_offsetAnchorViewModel, value)) return; + if (ReferenceEquals(this, value)) return; + if (value !=null && GetOffsetAnchorChainDepth(value) == -1) return; + if (_offsetAnchorViewModel == null && value != null) + { + + value.OffsetXChanged -= HandleOffsetXChanged; + value.OffsetYChanged -= HandleOffsetYChanged; + value.IdxChanged -= HandleOffsetAnchorIdxChanged; + value.OffsetXChanged += HandleOffsetXChanged; + value.OffsetYChanged += HandleOffsetYChanged; + value.IdxChanged += HandleOffsetAnchorIdxChanged; + if (SetProperty(ref _offsetAnchorViewModel, value)) + { + OffsetX = value.OffsetX; + OffsetY = value.OffsetY; + HandleOffsetAnchorIdxChanged(); + } + } + else if (_offsetAnchorViewModel != null && value == null) + { + _offsetAnchorViewModel.OffsetXChanged -= HandleOffsetXChanged; + _offsetAnchorViewModel.OffsetYChanged -= HandleOffsetYChanged; + _offsetAnchorViewModel.IdxChanged -= HandleOffsetAnchorIdxChanged; + if(SetProperty(ref _offsetAnchorViewModel, value)) + { + OffsetX = 0; + OffsetY = 0; + HandleOffsetAnchorIdxChanged(); + } + } + else if (_offsetAnchorViewModel != null && value != null) + { + _offsetAnchorViewModel.OffsetXChanged -= HandleOffsetXChanged; + _offsetAnchorViewModel.OffsetYChanged -= HandleOffsetYChanged; + _offsetAnchorViewModel.IdxChanged -= HandleOffsetAnchorIdxChanged; + value.OffsetXChanged -= HandleOffsetXChanged; + value.OffsetYChanged -= HandleOffsetYChanged; + value.IdxChanged -= HandleOffsetAnchorIdxChanged; + value.OffsetXChanged += HandleOffsetXChanged; + value.OffsetYChanged += HandleOffsetYChanged; + value.IdxChanged += HandleOffsetAnchorIdxChanged; + if(SetProperty(ref _offsetAnchorViewModel, value)) + { + OffsetX = value.OffsetX; + OffsetY = value.OffsetY; + HandleOffsetAnchorIdxChanged(); + } + } + } + } + + public const int MAX_OFFSET_ANCHOR_CHAIN_DEPTH = 100; + public int GetOffsetAnchorChainDepth(ElementViewModel vmAnchor) + { + int depth = 1; + ElementViewModel? vm = vmAnchor; + HashSet visited = new HashSet(); + while (vm != null) + { + // If the candidate chain reaches this node, binding would form a cycle. + if (ReferenceEquals(vm, this)) + { + return -1; + } + + // Existing cycles in the chain should also be treated as invalid. + if (!visited.Add(vm)) + { + return -1; + } + + vm = vm.OffsetAnchorViewModel; + if (vm != null) + { + depth++; + if (depth > MAX_OFFSET_ANCHOR_CHAIN_DEPTH) + { + return -1; + } + } + } + + return depth; + } + + private double _rotateAngle = 0; + public double RotateAngle + { + get => _rotateAngle; + set => SetProperty(ref _rotateAngle, value); + } + + private int _mappingU = 0; + public int MappingU + { + get => _mappingU; + set + { + SetProperty(ref _mappingU, value); + OnPropertyChanged(nameof(OverlayJSON)); + } + } + + private int _mappingV = 0; + public int MappingV + { + get => _mappingV; + set + { + SetProperty(ref _mappingV, value); + OnPropertyChanged(nameof(OverlayJSON)); + } + } + + private int _mappingW = 0; + public int MappingW + { + get => _mappingW; + set + { + SetProperty(ref _mappingW, value); + OnPropertyChanged(nameof(MaxRadius)); + OnPropertyChanged(nameof(OverlayJSON)); + Radius = Math.Min(Radius, MaxRadius); + } + } + + private int _mappingH = 0; + public int MappingH + { + get => _mappingH; + set + { + SetProperty(ref _mappingH, value); + OnPropertyChanged(nameof(MaxRadius)); + OnPropertyChanged(nameof(OverlayJSON)); + Radius = Math.Min(Radius, MaxRadius); + } + } + + public int MaxHeight => _images.Take(ImagesCount[ElementType]).Where(img => img != null).Max(img => img?.Height ?? 0); + public int MaxWidth => _images.Take(ImagesCount[ElementType]).Where(img => img != null).Max(img => img?.Width ?? 0); + private int _radius = 0; + public int Radius + { + get => _radius; + set + { + SetProperty(ref _radius, value); + OnPropertyChanged(nameof(RadiusString)); + OnPropertyChanged(nameof(OverlayJSON)); + } + } + + public string RadiusString => _radius.ToString(); + public int MaxRadius => Math.Max(MappingW, MappingH) * 2; + + private int _mouseRadius = 0; + public int MouseRadius + { + get => _mouseRadius; + set + { + SetProperty(ref _mouseRadius, value); + OnPropertyChanged(nameof(OverlayJSON)); + OnPropertyChanged(nameof(MouseRadiusString)); + } + } + public string MouseRadiusString => _mouseRadius.ToString(); + + + public static readonly Dictionary ImagesCount = new() + { + { ElementType.Texture, 1 }, + { ElementType.MouseMovement, 1 }, + { ElementType.KeyboardButton, 2 }, + { ElementType.GamepadButton, 2 }, + { ElementType.MouseButton, 2 }, + { ElementType.GamepadTrigger, 2 }, + { ElementType.AnalogStick, 2 }, + { ElementType.MouseWheel, 4 }, + { ElementType.GamepadPlayerId, 5 }, + { ElementType.DPad, 9 }, + }; + + + private readonly XImage?[] _images = new XImage?[MAX_IAMGE_NUM]; + public XImage?[] Images => _images; + public XImage? this[int index] + { + get => _images[index]; + set + { + if (_images[index] != value) + { + _images[index] = value; + OnPropertyChanged($"Image{index}Source"); + MappingH = CurrentImageSource?.PixelHeight ?? 1; + MappingW = CurrentImageSource?.PixelWidth ?? 1; + OnPropertyChanged(nameof(MaxHeight)); + OnPropertyChanged(nameof(MaxWidth)); + OnPropertyChanged(nameof(CurrentImageSource)); + } + } + } + public bool IsMainImageLoaded => _images[0] != null; + public bool IsImagesLoaded + { + get + { + for (int i = 0; i < ImagesCount[ElementType]; i++) + { + if (_images[i] == null) + return false; + } + return true; + } + } + + public bool IsAllSameShape + { + get + { + var validImages = _images.Take(ImagesCount[ElementType]).Where(img => img != null).ToList(); + if (validImages.Count == 0) return true; + int width = validImages[0]!.Width; + int height = validImages[0]!.Height; + return validImages.All(img => img!.Width == width && img.Height == height); + } + } + + + + public bool IsGIFExist => _images.Any(img => img != null && img.ImageType == XImageType.GIF); + public void FillMissingImagesWithMainImage() + { + for (int i = 0; i < ImagesCount[ElementType]; i++) + { + this[i] = this[0]; + } + } + public void ResizeAllImagesToMainImage() + { + if (!IsImagesLoaded) return; + int width = this[0]?.Width ?? 2; + int height = this[0]?.Height ?? 2; + for (int i = 1; i < ImagesCount[ElementType]; i++) + { + if (_images[i] != null && _images[i]?.ImageType != XImageType.GIF) + this[i] = this[i]?.ReSize(height, width); + } + } + public void ResizeAllImagesToMaxSize() + { + if (!IsImagesLoaded) return; + int maxWidth = _images.Take(ImagesCount[ElementType]).Max(img => img?.Width ?? 0); + int maxHeight = _images.Take(ImagesCount[ElementType]).Max(img => img?.Height ?? 0); + maxWidth = Math.Max(maxWidth, 2); + maxHeight = Math.Max(maxHeight, 2); + for (int i = 0; i < ImagesCount[ElementType]; i++) + { + if (_images[i] != null && _images[i]?.ImageType != XImageType.GIF) + this[i] = this[i]?.ReSize(maxHeight, maxWidth); + } + } + public void ResizeAllImagesToFirstGIF() + { + var firstGIF = _images.FirstOrDefault(img => img != null && img.ImageType == XImageType.GIF); + if (firstGIF == null) return; + int width = firstGIF.Width; + int height = firstGIF.Height; + for (int i = 0; i < ImagesCount[ElementType]; i++) + { + if (_images[i] != null && _images[i]?.ImageType != XImageType.GIF) + this[i] = this[i]?.ReSize(height, width); + } + } + + public BitmapSource? GetImageSource(int index) + { + if (ElementType == ElementType.GamepadTrigger && _triggerCacheImages != null) + { + return _triggerCacheImages[index].BitmapSource; + } + else + return _images[index]?.BitmapSource; + } + + private int _currentSourceIdx = 0; + public int CurrentSourceIdx + { + get => _currentSourceIdx; + set + { + if (ElementType == ElementType.GamepadTrigger && _triggerCacheImages != null) + { + if (value < 0 || value >= _triggerCacheImages.Count) return; + } + else if (ElementType != ElementType.GamepadTrigger) + { + if (value < 0 || value >= MAX_IAMGE_NUM) return; + } + SetProperty(ref _currentSourceIdx, value); + MappingH = CurrentImageSource?.PixelHeight ?? 1; + MappingW = CurrentImageSource?.PixelWidth ?? 1; + OnPropertyChanged(nameof(CurrentImageSource)); + OnPropertyChanged(nameof(CurrentModifiedImageSource)); + OnPropertyChanged(nameof(CurrentModifiedImageWidth)); + OnPropertyChanged(nameof(CurrentModifiedImageHeight)); + OnPropertyChanged(nameof(ModifiedCanvasWidth)); + OnPropertyChanged(nameof(ModifiedCanvasHeight)); + } + } + + private bool _useStopwatch = false; + public bool UseStopwatch + { + get => _useStopwatch; + set + { + SetProperty(ref _useStopwatch, value); + OnPropertyChanged(nameof(StopwatchVisibility)); + } + } + public Visibility StopwatchVisibility => _useStopwatch ? Visibility.Visible : Visibility.Collapsed; + + private int _stopwatchMilliseconds = 50; + public int StopwatchMilliseconds + { + get => _stopwatchMilliseconds; + set + { + if (value >= 50 && value <= 5000) + SetProperty(ref _stopwatchMilliseconds, value); + } + } + + private Stopwatch? _stopwatch = null; + public Stopwatch Stopwatch => _stopwatch ??= new Stopwatch(); + + private List? _triggerCacheImages = null; + public List? TriggerCacheImages => _triggerCacheImages; + + public BitmapSource? MainImageSource => GetImageSource(0); + public BitmapSource? CurrentImageSource => GetImageSource(_currentSourceIdx); + public BitmapSource? Image0Source => GetImageSource(0); + public BitmapSource? Image1Source => GetImageSource(1); + public BitmapSource? Image2Source => GetImageSource(2); + public BitmapSource? Image3Source => GetImageSource(3); + public BitmapSource? Image4Source => GetImageSource(4); + public BitmapSource? Image5Source => GetImageSource(5); + public BitmapSource? Image6Source => GetImageSource(6); + public BitmapSource? Image7Source => GetImageSource(7); + public BitmapSource? Image8Source => GetImageSource(8); + + + private bool _isTriggerMode = false; + public bool IsTriggerMode + { + get => _isTriggerMode; + set + { + SetProperty(ref _isTriggerMode, value); + OnPropertyChanged(nameof(IsNonTriggerMode)); + OnPropertyChanged(nameof(TriggerCursor)); + } + } + public bool IsNonTriggerMode => !_isTriggerMode; + public Cursor TriggerCursor => _isTriggerMode ? Cursors.No : Cursors.Arrow; + + + public Array GamepadButtonList => Enum.GetValues(typeof(GamepadCodeType)); + private GamepadCodeType _selectedGamepadButton = Models.GamepadCodeType.A; + public GamepadCodeType SelectedGamepadButton + { + get => _selectedGamepadButton; + set + { + SetProperty(ref _selectedGamepadButton, value); + OnPropertyChanged(nameof(OverlayJSON)); + } + } + + public Array KeyBoardButtonList => Enum.GetValues(typeof(KeyBoardCodeType)); + private KeyBoardCodeType _selectedKeyBoardButton = Models.KeyBoardCodeType.None; + public KeyBoardCodeType SelectedKeyBoardButton + { + get => _selectedKeyBoardButton; + set + { + SetProperty(ref _selectedKeyBoardButton, value); + OnPropertyChanged(nameof(OverlayJSON)); + } + } + + public Array SideList => Enum.GetValues(typeof(Side)); + private Side _selectedSide = Models.Side.Left; + public Side SelectedSide + { + get => _selectedSide; + set + { + SetProperty(ref _selectedSide, value); + OnPropertyChanged(nameof(OverlayJSON)); + } + } + + public Array MouseButtonList => Enum.GetValues(typeof(MouseCodeType)); + private MouseCodeType _selectedMouseButton = Models.MouseCodeType.None; + public MouseCodeType SelectedMouseButton + { + get => _selectedMouseButton; + set + { + SetProperty(ref _selectedMouseButton, value); + OnPropertyChanged(nameof(OverlayJSON)); + } + } + + public Array DirectionList => Enum.GetValues(typeof(Direction)); + private Direction _selectedDirection = Models.Direction.Up; + public Direction SelectedDirection + { + get => _selectedDirection; + set + { + SetProperty(ref _selectedDirection, value); + OnPropertyChanged(nameof(OverlayJSON)); + } + } + + public Array MouseMoveList => Enum.GetValues(typeof(MouseMoveType)); + private MouseMoveType _selectedMouseMoveType = MouseMoveType.Dot; + public MouseMoveType SelectedMouseMoveType + { + get => _selectedMouseMoveType; + set + { + SetProperty(ref _selectedMouseMoveType, value); + OnPropertyChanged(nameof(OverlayJSON)); + } + } + + public ElementViewModel() + { GetSegments(); } + + public override string ToString() + { + return ParentCollection==null?"无": $"编号: {Idx}"; + } + + public void Dispose() + { + this.OffsetAnchorViewModel = null; + this.AnimatedImageControl = null; + this.PropertyChanged = null; + this.OffsetXChanged = null; + this.OffsetYChanged = null; + this.IdxChanged = null; + this.ParentCollection = null; + } + + public string OverlayJSON => ToOverlayElement().ToJSON(); + } +} diff --git a/FancyInput/ViewModels/ElementViewModel.Test.cs b/FancyInput/ViewModels/ElementViewModel.Test.cs new file mode 100644 index 0000000..35443ac --- /dev/null +++ b/FancyInput/ViewModels/ElementViewModel.Test.cs @@ -0,0 +1,586 @@ +using FancyInput.Models; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace FancyInput.ViewModels +{ + public partial class ElementViewModel + { + private bool _isDetectingInput = false; + public bool IsDetectingInput + { + get => _isDetectingInput; + set + { + SetProperty(ref _isDetectingInput, value); + OnPropertyChanged(nameof(DetectingString)); + OnPropertyChanged(nameof(DetectingColor)); + } + } + public string DetectingString => _isDetectingInput ? "检测中..." : "检测"; + public SolidColorBrush DetectingColor => _isDetectingInput ? new SolidColorBrush(Colors.Red) : new SolidColorBrush(Color.FromArgb(0xFF, 0x65, 0x1F, 0x65)); + + + private double _canvasWidth = 100; + public double CanvasWidth + { + get => _canvasWidth; + set => SetProperty(ref _canvasWidth, value); + } + + private double _canvasHeight = 100; + public double CanvasHeight + { + get => _canvasHeight; + set => SetProperty(ref _canvasHeight, value); + } + + public void InitCanvasSize() + { + var images = _images.Take(ImagesCount[ElementType]).Where(img => img != null).ToList(); + if (images.Count == 0) + { + CanvasWidth = 100; + CanvasHeight = 100; + } + else + { + int maxWidth = images.Max(img => img!.Width) ; + int maxHeight = images.Max(img => img!.Height); + CanvasWidth = maxWidth; + CanvasHeight = maxHeight; + } + + } + + public double[] SegmentsScales = Enumerable.Repeat(1.0, MAX_IAMGE_NUM).ToArray(); + public double[] SegmentsOffsetX = Enumerable.Repeat(0.0, MAX_IAMGE_NUM).ToArray(); + public double[] SegmentsOffsetY = Enumerable.Repeat(0.0, MAX_IAMGE_NUM).ToArray(); + + public enum AlignRegionMode + { + Union, + Intersection, + } + + + private bool _allowTesting = false; + public bool AllowTesting + { + get => _allowTesting; + set + { + SetProperty(ref _allowTesting, value); + } + } + + //private ObservableCollection _segments = new ObservableCollection(); + public ObservableCollection Segments { get; } = new ObservableCollection(); + public ObservableCollection SelectedSegmentViewModels { get; } = new ObservableCollection(); + public bool HasSelectedSegment => SelectedSegmentViewModels.Count > 0; + public bool HasMultipleSelectedSegments => SelectedSegmentViewModels.Count > 1; + public Visibility HasSelectedSegmentVisibility => HasSelectedSegment ? Visibility.Visible : Visibility.Collapsed; + public Visibility HasMultipleSelectedSegmentsVisibility => HasMultipleSelectedSegments ? Visibility.Visible : Visibility.Collapsed; + public ElementSegmentViewModel? SelectedSegmentViewModel => HasSelectedSegment ? SelectedSegmentViewModels[0] : null; + private ElementSegmentViewModel? _erasePreviewOwnerSegment; + private XImage? _erasePreviewImage; + public bool HasPendingErase => _erasePreviewImage != null; + public ImageSource? SelectedSegmentSource => _erasePreviewImage?.BitmapSource ?? SelectedSegmentViewModel?.ImageSource; + public double SelectedSegmentWidth => _erasePreviewImage?.Width ?? SelectedSegmentViewModel?.Width ?? 0; + public double SelectedSegmentHeight => _erasePreviewImage?.Height ?? SelectedSegmentViewModel?.Height ?? 0; + public double MaxEraseBrushRadius => Math.Max(CanvasWidth, CanvasHeight)/2*0.3; + public double MinEraseBrushRadius => 0.5; + public double _eraseBrushRadius = 2; + public double EraseBrushRadius + { + get => _eraseBrushRadius; + set + { + if (SetProperty(ref _eraseBrushRadius, value)) + { + OnPropertyChanged(nameof(EraseBrushDiameter)); + OnPropertyChanged(nameof(EraseBrushPreviewLeft)); + OnPropertyChanged(nameof(EraseBrushPreviewTop)); + } + } + } + + private double _eraseBrushCenterX = 0; + public double EraseBrushCenterX + { + get => _eraseBrushCenterX; + private set + { + if (SetProperty(ref _eraseBrushCenterX, value)) + { + OnPropertyChanged(nameof(EraseBrushPreviewLeft)); + } + } + } + + private double _eraseBrushCenterY = 0; + public double EraseBrushCenterY + { + get => _eraseBrushCenterY; + private set + { + if (SetProperty(ref _eraseBrushCenterY, value)) + { + OnPropertyChanged(nameof(EraseBrushPreviewTop)); + } + } + } + + private bool _isEraseBrushPreviewVisible = false; + public bool IsEraseBrushPreviewVisible + { + get => _isEraseBrushPreviewVisible; + private set + { + if (SetProperty(ref _isEraseBrushPreviewVisible, value)) + { + OnPropertyChanged(nameof(EraseBrushPreviewVisibility)); + } + } + } + + public double EraseBrushDiameter => EraseBrushRadius * 2; + public double EraseBrushPreviewLeft => EraseBrushCenterX - EraseBrushRadius; + public double EraseBrushPreviewTop => EraseBrushCenterY - EraseBrushRadius; + public Visibility EraseBrushPreviewVisibility => IsEraseBrushPreviewVisible && HasSelectedSegment ? Visibility.Visible : Visibility.Collapsed; + + public void SetEraseBrushPreview(double centerX, double centerY) + { + EraseBrushCenterX = centerX; + EraseBrushCenterY = centerY; + IsEraseBrushPreviewVisible = true; + } + + public void HideEraseBrushPreview() + { + IsEraseBrushPreviewVisible = false; + } + + + private double _selectionRectLeft = 0; + public double SelectionRectLeft + { + get => _selectionRectLeft; + set => SetProperty(ref _selectionRectLeft, value); + } + + private double _selectionRectTop = 0; + public double SelectionRectTop + { + get => _selectionRectTop; + set => SetProperty(ref _selectionRectTop, value); + } + + private double _selectionRectWidth = 0; + public double SelectionRectWidth + { + get => _selectionRectWidth; + set => SetProperty(ref _selectionRectWidth, value); + } + + private double _selectionRectHeight = 0; + public double SelectionRectHeight + { + get => _selectionRectHeight; + set => SetProperty(ref _selectionRectHeight, value); + } + + private Visibility _selectionRectVisibility = Visibility.Collapsed; + public Visibility SelectionRectVisibility + { + get => _selectionRectVisibility; + set => SetProperty(ref _selectionRectVisibility, value); + } + + public void SyncSelectedSegmentState() + { + foreach (var segment in Segments) + { + segment.IsSelected = SelectedSegmentViewModels.Contains(segment); + } + + if (_erasePreviewOwnerSegment != null && _erasePreviewOwnerSegment != SelectedSegmentViewModel) + { + DiscardErasePreview(); + } + + OnPropertyChanged(nameof(HasSelectedSegment)); + OnPropertyChanged(nameof(HasMultipleSelectedSegments)); + OnPropertyChanged(nameof(HasSelectedSegmentVisibility)); + OnPropertyChanged(nameof(HasMultipleSelectedSegmentsVisibility)); + OnPropertyChanged(nameof(HasPendingErase)); + OnPropertyChanged(nameof(EraseBrushPreviewVisibility)); + OnPropertyChanged(nameof(SelectedSegmentViewModel)); + OnPropertyChanged(nameof(SelectedSegmentSource)); + OnPropertyChanged(nameof(SelectedSegmentWidth)); + OnPropertyChanged(nameof(SelectedSegmentHeight)); + } + + public bool TryStartOrContinueErasePreview(ElementSegmentViewModel segment) + { + if (segment.Image == null) + { + return false; + } + + if (_erasePreviewOwnerSegment != segment || _erasePreviewImage == null) + { + _erasePreviewOwnerSegment = segment; + _erasePreviewImage = segment.Image.Copy(); + } + + OnPropertyChanged(nameof(HasPendingErase)); + OnPropertyChanged(nameof(SelectedSegmentSource)); + OnPropertyChanged(nameof(SelectedSegmentWidth)); + OnPropertyChanged(nameof(SelectedSegmentHeight)); + return true; + } + + public bool TryApplyEraseToPreview(double centerX, double centerY, double radius) + { + if (_erasePreviewImage == null) + { + return false; + } + + _erasePreviewImage = _erasePreviewImage.EraseCircle(centerX, centerY, radius); + OnPropertyChanged(nameof(SelectedSegmentSource)); + OnPropertyChanged(nameof(SelectedSegmentWidth)); + OnPropertyChanged(nameof(SelectedSegmentHeight)); + return true; + } + + public bool CommitErasePreview() + { + if (_erasePreviewOwnerSegment == null || _erasePreviewImage == null) + { + return false; + } + + _erasePreviewOwnerSegment.Image = _erasePreviewImage.Copy(); + DiscardErasePreview(); + return true; + } + + public void DiscardErasePreview() + { + if (_erasePreviewOwnerSegment == null && _erasePreviewImage == null) + { + return; + } + + _erasePreviewOwnerSegment = null; + _erasePreviewImage = null; + OnPropertyChanged(nameof(HasPendingErase)); + OnPropertyChanged(nameof(SelectedSegmentSource)); + OnPropertyChanged(nameof(SelectedSegmentWidth)); + OnPropertyChanged(nameof(SelectedSegmentHeight)); + } + + public void RemoveLockedSelectedSegments() + { + for (int i = SelectedSegmentViewModels.Count - 1; i >= 0; i--) + { + if (SelectedSegmentViewModels[i].IsLocked) + { + SelectedSegmentViewModels.RemoveAt(i); + } + } + SyncSelectedSegmentState(); + } + + + public void AttachSegmentChangeHandler() + { + foreach (var segment in Segments) + { + segment.SegmentChanged -= ChangeModifiedVisual; + segment.SegmentChanged += ChangeModifiedVisual; + } + } + public void DetachSegmentChangeHandler() + { + foreach (var segment in Segments) + { + segment.SegmentChanged -= ChangeModifiedVisual; + } + } + + public void ChangeModifiedVisual() + { + SaveSegments(); + ResetModifiedImageSources(); + OnPropertyChanged(nameof(CurrentModifiedImageSource)); + OnPropertyChanged(nameof(CurrentModifiedImageWidth)); + OnPropertyChanged(nameof(CurrentModifiedImageHeight)); + OnPropertyChanged(nameof(ModifiedCanvasWidth)); + OnPropertyChanged(nameof(ModifiedCanvasHeight)); + } + + private static readonly string[] DefaultTwoStateTitles = { "闲置", "按下" }; + private static readonly string[] DPadTitles = { "闲置", "左", "右", "上", "下", "左上", "右上", "左下", "右下" }; + private static readonly string[] MouseWheelTitles = { "闲置", "按下", "向上", "向下" }; + private static readonly string[] GamepadPlayerIdTitles = { "玩家1", "玩家2", "玩家3", "玩家4", "GUIDE" }; + private static readonly string[] SingleTextureTitle = { "贴图" }; + + public void GetSegments() + { + DetachSegmentChangeHandler(); + DiscardErasePreview(); + SelectedSegmentViewModels.Clear(); + Segments.Clear(); + + string[]? titles = ElementType switch + { + ElementType.KeyboardButton => DefaultTwoStateTitles, + ElementType.MouseButton => DefaultTwoStateTitles, + ElementType.GamepadButton => DefaultTwoStateTitles, + ElementType.GamepadTrigger => DefaultTwoStateTitles, + ElementType.AnalogStick => DefaultTwoStateTitles, + ElementType.DPad => DPadTitles, + ElementType.MouseWheel => MouseWheelTitles, + ElementType.GamepadPlayerId => GamepadPlayerIdTitles, + ElementType.MouseMovement => SingleTextureTitle, + _ => null, + }; + + if (titles == null) + { + return; + } + + int count = Math.Min(titles.Length, ImagesCount[ElementType]); + for (int i = 0; i < count; i++) + { + var segment = new ElementSegmentViewModel + { + Name = titles[i], + Image = _images[i], + LocalScale = SegmentsScales[i], + OffsetX = SegmentsOffsetX[i], + OffsetY = SegmentsOffsetY[i] + }; + segment.ConfigureEditorRanges(CanvasWidth, CanvasHeight); + Segments.Add(segment); + } + AttachSegmentChangeHandler(); + } + + public void SaveSegments() + { + for (int i = 0; i < Segments.Count; i++) + { + if (i < ImagesCount[ElementType]) + { + SegmentsScales[i] = Segments[i].LocalScale; + SegmentsOffsetX[i] = Segments[i].OffsetX; + SegmentsOffsetY[i] = Segments[i].OffsetY; + } + } + } + + private List? _modifiedTriggerCacheImages = null; + private XImage?[] _modifiedImages = new XImage?[MAX_IAMGE_NUM]; + public XImage?[] ModifiedImages => _modifiedImages; + public void MakeModifiedCache(int frameRange) + { + if (ElementType != ElementType.GamepadTrigger) return; + XImage? startImage = _modifiedImages[0]; + XImage? endImage = _modifiedImages[1]; + if (startImage == null || endImage == null) return; + if (_modifiedTriggerCacheImages != null) + { + _modifiedTriggerCacheImages.Clear(); + } + int framesInBatch = frameRange + 1; + _modifiedTriggerCacheImages = new List(); + for (int directionIdx = 1; directionIdx <= 4; directionIdx++) + { + Direction direction = (Direction)directionIdx; + for (int frame = 0; frame < framesInBatch; frame++) + { + double percent = (double)frame / frameRange; + XImage Merge = XImage.Merge(startImage, endImage, direction, percent); + _modifiedTriggerCacheImages.Add(Merge); + } + } + } + + public BitmapSource? GetModifiedImageSource(int index) + { + if (ElementType == ElementType.GamepadTrigger && _modifiedTriggerCacheImages != null) + { + return _modifiedTriggerCacheImages[index].BitmapSource; + } + else + return _modifiedImages[index]?.BitmapSource; + } + public BitmapSource? CurrentModifiedImageSource => GetModifiedImageSource(_currentSourceIdx); + public double CurrentModifiedImageWidth => CurrentModifiedImageSource?.PixelWidth ?? 0; + public double CurrentModifiedImageHeight => CurrentModifiedImageSource?.PixelHeight ?? 0; + public double ModifiedCanvasWidth => Math.Max(1, _modifiedImages.Where(img => img != null).Select(img => (double)img!.Width).DefaultIfEmpty(0).Max()); + public double ModifiedCanvasHeight => Math.Max(1, _modifiedImages.Where(img => img != null).Select(img => (double)img!.Height).DefaultIfEmpty(0).Max()); + + private bool TryGetUnionValidRectFromModifiedImages(out int left, out int top, out int right, out int bottom) + { + left = 0; + top = 0; + right = 0; + bottom = 0; + + bool hasAnyValidRect = false; + + foreach (var image in _modifiedImages) + { + if (image == null) + { + continue; + } + + int width = image.Width; + int height = image.Height; + var alpha = image.ExtractChannels().A; + + int minX = width; + int minY = height; + int maxX = -1; + int maxY = -1; + + for (int y = 0; y < height; y++) + { + int rowStart = y * width; + for (int x = 0; x < width; x++) + { + if (alpha[rowStart + x] == 0) + { + continue; + } + + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + } + + if (maxX < minX || maxY < minY) + { + continue; + } + + if (!hasAnyValidRect) + { + left = minX; + top = minY; + right = maxX + 1; + bottom = maxY + 1; + hasAnyValidRect = true; + } + else + { + left = Math.Min(left, minX); + top = Math.Min(top, minY); + right = Math.Max(right, maxX + 1); + bottom = Math.Max(bottom, maxY + 1); + } + } + + return hasAnyValidRect; + } + + public void ResetModifiedImageSources(AlignRegionMode mode = AlignRegionMode.Union) + { + Array.Clear(_modifiedImages, 0, _modifiedImages.Length); + + int imageCount = ImagesCount[ElementType]; + var entries = new List<(int idx, XImage image, int x, int y, int right, int bottom)>(); + + for (int i = 0; i < imageCount; i++) + { + if (Segments[i].Image == null) + { + continue; + } + + double scale = SegmentsScales[i] > 0 ? SegmentsScales[i] : 1.0; + XImage scaled = Math.Abs(scale - 1.0) < 1e-9 ? Segments[i].Image! : Segments[i].Image!.Scale(scale); + int x = (int)Math.Round(SegmentsOffsetX[i]); + int y = (int)Math.Round(SegmentsOffsetY[i]); + entries.Add((i, scaled, x, y, x + scaled.Width, y + scaled.Height)); + } + + if (entries.Count == 0) + { + return; + } + + int regionLeft; + int regionTop; + int regionRight; + int regionBottom; + + if (mode == AlignRegionMode.Intersection) + { + regionLeft = entries.Max(e => e.x); + regionTop = entries.Max(e => e.y); + regionRight = entries.Min(e => e.right); + regionBottom = entries.Min(e => e.bottom); + + // No overlap in intersection mode. + if (regionRight <= regionLeft || regionBottom <= regionTop) + { + return; + } + } + else + { + regionLeft = entries.Min(e => e.x); + regionTop = entries.Min(e => e.y); + regionRight = entries.Max(e => e.right); + regionBottom = entries.Max(e => e.bottom); + } + + int regionWidth = regionRight - regionLeft; + int regionHeight = regionBottom - regionTop; + + foreach (var entry in entries) + { + int cropX = regionLeft - entry.x; + int cropY = regionTop - entry.y; + _modifiedImages[entry.idx] = entry.image.Crop(cropX, cropY, regionWidth, regionHeight); + } + + if (TryGetUnionValidRectFromModifiedImages(out int validLeft, out int validTop, out int validRight, out int validBottom)) + { + int validWidth = validRight - validLeft; + int validHeight = validBottom - validTop; + + for (int i = 0; i < _modifiedImages.Length; i++) + { + if (_modifiedImages[i] == null) + { + continue; + } + _modifiedImages[i] = _modifiedImages[i]!.Crop(validLeft, validTop, validWidth, validHeight); + } + } + + MakeModifiedCache(InputHandler.TRIGGER_CACHE_RANGE); + } + + + } +} diff --git a/FancyInput/ViewModels/ImageEditorViewModel.cs b/FancyInput/ViewModels/ImageEditorViewModel.cs new file mode 100644 index 0000000..7322b41 --- /dev/null +++ b/FancyInput/ViewModels/ImageEditorViewModel.cs @@ -0,0 +1,713 @@ +using FancyInput.Models; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +using Cursor = System.Windows.Input.Cursor; +using Cursors = System.Windows.Input.Cursors; +namespace FancyInput.ViewModels +{ + public enum ImageSourceType + { + Main, + Original + } + public class ImageEditorViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + protected virtual bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") + { + if (Equals(field, value)) return false; + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return true; + } + + public double OriginalCanvasScale { get; set; } = 1.0; + public const double ScaleStep = 0.1; + public const double MinScale = 0.1; + public const double MaxScale = 10.0; + public double ActualMinScale => MinScale * OriginalCanvasScale; + public double ActualMaxScale => MaxScale * OriginalCanvasScale; + public double PowStep => 1 + ScaleStep; + private double _canvasScale = 1.0; + public double CanvasScale + { + get => _canvasScale; + set + { + SetProperty(ref _canvasScale, value); + OnPropertyChanged(nameof(CutLineWidth)); + } + } + public double CutLineWidth => 1.0 / CanvasScale; + + private ImageSourceType _imageSourceType = ImageSourceType.Main; + public ImageSourceType ImageSourceType + { + get => _imageSourceType; + set + { + SetProperty(ref _imageSourceType, value); + if (_imageSourceType == ImageSourceType.Main) + CurrentImage = MainImage; + else + CurrentImage = OriginalImage; + } + } + + private int _originalH, _originalW; + public int OriginalH + { + get => _originalH; + set =>SetProperty(ref _originalH, value); + } + public int OriginalW + { + get => _originalW; + set => SetProperty(ref _originalW, value); + } + public const int MIN_H = 1; + public const int MIN_W = 1; + public int H => Math.Max((int)((_originalH - _topCut - _bottomCut) * _imageScale), MIN_H); + public int W => Math.Max((int)((_originalW - _leftCut - _rightCut) * _imageScale),MIN_W); + public int VirtualH => _originalH - _topCut - _bottomCut; + public int VirtualW => _originalW - _leftCut - _rightCut; + + private double _logImageScale = 0; + public double LogImageScale + { + get => _logImageScale; + set + { + SetProperty(ref _logImageScale, value); + _imageScale = Math.Pow(10, _logImageScale); + OnPropertyChanged(nameof(W)); + OnPropertyChanged(nameof(H)); + OnPropertyChanged(nameof(ImageScaleString)); + } + } + private double _minLogImageScale; + public double MinLogImageScale + { + get => _minLogImageScale; + set => SetProperty(ref _minLogImageScale, value); + } + private double _maxLogImageScale; + public double MaxLogImageScale + { + get => _maxLogImageScale; + set => SetProperty(ref _maxLogImageScale, value); + } + private double _imageScale; + public string ImageScaleString + { + get => $"{_imageScale:F2}"; + set + { + if (double.TryParse(value, out double result)) + { + result = Math.Clamp(result, ActualMinScale, ActualMaxScale); + LogImageScale = Math.Log10(result); + OnPropertyChanged(nameof(ImageScaleString)); + } + } + } + + private Color _cutBorderBackgroundColor = Colors.Transparent; + public Color CutBorderBackgroundColor + { + get => _cutBorderBackgroundColor; + set + { + SetProperty(ref _cutBorderBackgroundColor, value); + OnPropertyChanged(nameof(CutBorderBackgroundBrush)); + } + } + public SolidColorBrush CutBorderBackgroundBrush => new SolidColorBrush(CutBorderBackgroundColor); + + private int _maxHCut; + public int MaxHCut + { + get => _maxHCut; + set=>SetProperty(ref _maxHCut, value); + } + private int _maxWCut; + public int MaxWCut + { + get => _maxWCut; + set => SetProperty(ref _maxWCut, value); + } + private int _leftCut; + public int LeftCut + { + get => _leftCut; + set + { + SetProperty(ref _leftCut, Math.Clamp(value, 0, _maxWCut - _rightCut)); + OnPropertyChanged(nameof(VirtualW)); + OnPropertyChanged(nameof(W)); + } + } + private int _rightCut; + public int RightCut + { + get => _rightCut; + set + { + SetProperty(ref _rightCut, Math.Clamp(value, 0, _maxWCut - _leftCut)); + OnPropertyChanged(nameof(VirtualW)); + OnPropertyChanged(nameof(W)); + } + } + private int _topCut; + public int TopCut + { + get => _topCut; + set + { + SetProperty(ref _topCut, Math.Clamp(value, 0, _maxHCut - _bottomCut)); + OnPropertyChanged(nameof(VirtualH)); + OnPropertyChanged(nameof(H)); + } + } + private int _bottomCut; + public int BottomCut + { + get => _bottomCut; + set + { + SetProperty(ref _bottomCut, Math.Clamp(value, 0, _maxHCut - _topCut)); + OnPropertyChanged(nameof(VirtualH)); + OnPropertyChanged(nameof(H)); + } + } + + private bool _isCutVisible = false; + public bool IsCutVisible + { + get => _isCutVisible; + set + { + SetProperty(ref _isCutVisible, value); + CutVisibility = value? Visibility.Visible:Visibility.Hidden; + OnPropertyChanged(nameof(CutVisibility)); + } + } + public Visibility CutVisibility { get; set; } + private bool _isDetectingInput = false; + public bool IsEditingCutBorder + { + get => _isDetectingInput; + set + { + SetProperty(ref _isDetectingInput, value); + OnPropertyChanged(nameof(DetectingString)); + OnPropertyChanged(nameof(DetectingColor)); + } + } + public string DetectingString => _isDetectingInput ? "绘制中..." : "手动"; + public SolidColorBrush DetectingColor => _isDetectingInput ? new SolidColorBrush(Colors.Red) : new SolidColorBrush(Color.FromArgb(0xFF, 0x65, 0x1F, 0x65)); + + private bool _isProtectTransparent = true; + public bool IsProtectTransparent + { + get => _isProtectTransparent; + set => SetProperty(ref _isProtectTransparent, value); + } + private byte[] _originalRBytes; + private byte[] _originalGBytes; + private byte[] _originalBBytes; + private byte[] _originalABytes; + public XImage OriginalImage => XImage.MergeChannels(_originalRBytes, _originalGBytes, + _originalBBytes, _originalABytes, _originalH, _originalW); + public BitmapSource OriginalImageSource => OriginalImage.BitmapSource; + + private XImage _mainImage; + public XImage MainImage + { + get => _mainImage; + set + { + SetProperty(ref _mainImage, value); + OnPropertyChanged(nameof(MainImageSource)); + if (ImageSourceType == ImageSourceType.Main) + CurrentImage = MainImage; + } + } + public BitmapSource MainImageSource => MainImage.BitmapSource; + + private XImage? _currentImage; + private XImage? CurrentImage + { + get => _currentImage; + set + { + SetProperty(ref _currentImage, value); + OnPropertyChanged(nameof(CurrentImageSource)); + } + } + public BitmapSource? CurrentImageSource => CurrentImage?.BitmapSource; + + private byte[] _rBytes; + private byte[] _gBytes; + private byte[] _bBytes; + private byte[] _aBytes; + + + private double _rShift = 0; + public double RShift + { + get => _rShift; + set + { + SetProperty(ref _rShift, value); + _rBytes = ShiftBytes(_originalRBytes, _rShift, _aBytes); + MainImage = XImage.MergeChannels(_rBytes, _gBytes, _bBytes, _aBytes, _originalH, _originalW); + + } + } + private double _gShift = 0; + public double GShift + { + get => _gShift; + set + { + SetProperty(ref _gShift, value); + _gBytes = ShiftBytes(_originalGBytes, _gShift, _aBytes); + MainImage = XImage.MergeChannels(_rBytes, _gBytes, _bBytes, _aBytes, _originalH, _originalW); + } + } + private double _bShift = 0; + public double BShift + { + get => _bShift; + set + { + SetProperty(ref _bShift, value); + _bBytes = ShiftBytes(_originalBBytes, _bShift, _aBytes); + MainImage = XImage.MergeChannels(_rBytes, _gBytes, _bBytes, _aBytes, _originalH, _originalW); + } + } + private double _aShift = 0; + public double AShift + { + get => _aShift; + set + { + SetProperty(ref _aShift, value); + _aBytes = ShiftBytes(_originalABytes, _aShift, _originalABytes, true); + MainImage = XImage.MergeChannels(_rBytes, _gBytes, _bBytes, _aBytes, _originalH, _originalW); + } + } + + //蒙版 + private bool[] _maskBytes; + public bool[] MaskBytes + { + get => _maskBytes; + set + { + SetProperty(ref _maskBytes, value); + OnPropertyChanged(nameof(MaskBytes)); + OnPropertyChanged(nameof(MaskSource)); + } + } + private bool _useMask = false; + public bool UseMask + { + get => _useMask; + set => SetProperty(ref _useMask, value); + } + private bool _isMaskVisible = false; + public bool IsMaskVisible + { + get => _isMaskVisible; + set + { + SetProperty(ref _isMaskVisible, value); + OnPropertyChanged(nameof(MaskVisibility)); + OnPropertyChanged(nameof(MaskBrushPreviewVisibility)); + OnPropertyChanged(nameof(CanvasCursor)); + } + } + public Visibility MaskVisibility => _isMaskVisible? Visibility.Visible : Visibility.Collapsed; + private Color _maskColor = Color.FromArgb(150,255,0,0); + public Color MaskColor + { + get => _maskColor; + set + { + SetProperty(ref _maskColor, value); + OnPropertyChanged(nameof(MaskColorBrush)); + OnPropertyChanged(nameof(MaskSource)); + } + } + public SolidColorBrush MaskColorBrush => new SolidColorBrush(MaskColor); + public BitmapSource MaskSource => XImage.FromMask(_maskBytes, OriginalH, OriginalW, _maskColor).BitmapSource; + //private bool _allowEditingMask = false; + //public bool AllowEditingMask + //{ + // get => _allowEditingMask; + // set + // { + // SetProperty(ref _allowEditingMask, value); + // OnPropertyChanged(nameof(CanvasCursor)); + // OnPropertyChanged(nameof(IsEditBorderEnabled)); + // } + //} + //public bool IsEditBorderEnabled => !AllowEditingMask; + private bool _useMaskBrush = false; + public bool UseMaskBrush + { + get => _useMaskBrush; + set + { + SetProperty(ref _useMaskBrush, value); + OnPropertyChanged(nameof(CanvasCursor)); + OnPropertyChanged(nameof(MaskBrushPreviewVisibility)); + } + } + + public const double MinMaskBrushRadius = 0.4; + public const double MaxMaskBrushRadius = 60; + public const double MaskBrushLogStep = 0.08; + public double MinLogMaskBrushRadius => Math.Log10(MinMaskBrushRadius); + public double MaxLogMaskBrushRadius => Math.Log10(MaxMaskBrushRadius); + + private double _maskBrushRadius = 1; + public double MaskBrushRadius + { + get => _maskBrushRadius; + set + { + SetProperty(ref _maskBrushRadius, Math.Clamp(value, MinMaskBrushRadius, MaxMaskBrushRadius)); + OnPropertyChanged(nameof(MaskBrushRadiusString)); + OnPropertyChanged(nameof(LogMaskBrushRadius)); + OnPropertyChanged(nameof(MaskBrushPreviewDiameter)); + OnPropertyChanged(nameof(MaskBrushPreviewLeft)); + OnPropertyChanged(nameof(MaskBrushPreviewTop)); + } + } + public double LogMaskBrushRadius + { + get => Math.Log10(MaskBrushRadius); + set => MaskBrushRadius = Math.Pow(10, Math.Clamp(value, MinLogMaskBrushRadius, MaxLogMaskBrushRadius)); + } + + private double _maskBrushCenterX; + public double MaskBrushCenterX + { + get => _maskBrushCenterX; + private set + { + if (SetProperty(ref _maskBrushCenterX, value)) + { + OnPropertyChanged(nameof(MaskBrushPreviewLeft)); + } + } + } + + private double _maskBrushCenterY; + public double MaskBrushCenterY + { + get => _maskBrushCenterY; + private set + { + if (SetProperty(ref _maskBrushCenterY, value)) + { + OnPropertyChanged(nameof(MaskBrushPreviewTop)); + } + } + } + + private bool _isMaskBrushPreviewVisible; + public bool IsMaskBrushPreviewVisible + { + get => _isMaskBrushPreviewVisible; + private set + { + if (SetProperty(ref _isMaskBrushPreviewVisible, value)) + { + OnPropertyChanged(nameof(MaskBrushPreviewVisibility)); + } + } + } + + public double MaskBrushPreviewDiameter => MaskBrushRadius * 2; + public double MaskBrushPreviewLeft => MaskBrushCenterX - MaskBrushRadius; + public double MaskBrushPreviewTop => MaskBrushCenterY - MaskBrushRadius; + public Visibility MaskBrushPreviewVisibility => IsMaskVisible && UseMaskBrush && IsMaskBrushPreviewVisible + ? Visibility.Visible + : Visibility.Collapsed; + + public void SetMaskBrushPreview(double x, double y) + { + MaskBrushCenterX = Math.Clamp(x, 0, Math.Max(0, OriginalW - 1)); + MaskBrushCenterY = Math.Clamp(y, 0, Math.Max(0, OriginalH - 1)); + IsMaskBrushPreviewVisible = true; + } + + public void HideMaskBrushPreview() + { + IsMaskBrushPreviewVisible = false; + } + + public void AdjustMaskBrushRadiusByWheel(int wheelDelta) + { + if (wheelDelta == 0) + { + return; + } + + LogMaskBrushRadius += wheelDelta > 0 ? MaskBrushLogStep : -MaskBrushLogStep; + } + + public string MaskBrushRadiusString => $"{MaskBrushRadius:F1}"; + public Cursor CanvasCursor + { + get + { + if (IsMaskVisible) + { + if (UseMaskBrush) + return Cursors.Pen; + else + return Cursors.Cross; + } + else + return Cursors.Arrow; + + } + } + + + private Color _imageBackGroundColor = Colors.Black; + public Color ImageBackGroundColor + { + get => _imageBackGroundColor; + set + { + SetProperty(ref _imageBackGroundColor, value); + OnPropertyChanged(nameof(ImageBackGround)); + } + } + public SolidColorBrush ImageBackGround => new SolidColorBrush(_imageBackGroundColor); + private Visibility _maskBorderVisibility = Visibility.Collapsed; + public Visibility MaskBorderVisibility + { + get => _maskBorderVisibility; + set => SetProperty(ref _maskBorderVisibility, value); + } + private int _maskBorderX = 0; + private int _maskBorderY = 0; + private int _maskBorderW = 0; + private int _maskBorderH = 0; + public int MaskBorderX + { + get => _maskBorderX; + set => SetProperty(ref _maskBorderX, value); + } + public int MaskBorderY + { + get => _maskBorderY; + set => SetProperty(ref _maskBorderY, value); + } + public int MaskBorderW + { + get => _maskBorderW; + set => SetProperty(ref _maskBorderW, value); + } + public int MaskBorderH + { + get => _maskBorderH; + set => SetProperty(ref _maskBorderH, value); + } + + + public ImageEditorViewModel(XImage xImage) + { + _mainImage = xImage.Copy(); + var bytes = xImage.ExtractChannels(); + _originalRBytes = bytes.R; + _originalGBytes = bytes.G; + _originalBBytes = bytes.B; + _originalABytes = bytes.A; + _rBytes = CopyBytes(_originalRBytes); + _gBytes = CopyBytes(_originalGBytes); + _bBytes = CopyBytes(_originalBBytes); + _aBytes = CopyBytes(_originalABytes); + OriginalH = xImage.Height; + OriginalW = xImage.Width; + MaxHCut = _originalH -1; + MaxWCut = _originalW -1; + LeftCut = 0; + RightCut = 0; + TopCut = 0; + BottomCut = 0; + IsCutVisible = false; + CurrentImage = MainImage; + OriginalCanvasScale = 200.0/Math.Max(OriginalH, OriginalW); + CanvasScale = OriginalCanvasScale; + //计算最小和最大缩放比例 + MinLogImageScale = -1; + MaxLogImageScale = 1; + LogImageScale = 0; + //蒙版 + _maskBytes = CreateMask(_originalABytes, true); + MaskBorderX = 0; + MaskBorderY = 0; + MaskBorderW = OriginalW; + MaskBorderH = OriginalH; + } + + private static byte[] CopyBytes(byte[] bytes) + { + byte[] newBytes = new byte[bytes.Length]; + Array.Copy(bytes, newBytes, bytes.Length); + return newBytes; + } + + private static bool[] CreateMask(byte[] bytes, bool fill) + { + bool[] newBytes = new bool[bytes.Length]; + for (int i = 0; i < bytes.Length; i++) + { + newBytes[i] = fill; + } + return newBytes; + } + + public void FillCroppedMask(int X,int Y,int W,int H,bool fill) + { + if (MaskBytes.Length != OriginalH * OriginalW) + throw new ArgumentException("Fatal Error:蒙版尺寸不正确!"); + X = Math.Clamp(X, 0, OriginalW-1); + Y = Math.Clamp(Y, 0, OriginalH-1); + W = Math.Clamp(W, 1, OriginalW - X); + H = Math.Clamp(H, 1, OriginalH - Y); + for (int i = Y; i < Y + H; i++) + { + int iStride = i * OriginalW; + for (int j = X; j < X + W; j++) + { + MaskBytes[iStride + j] = fill; + } + } + MaskBytes = MaskBytes; + } + + public void AddMask()=> FillCroppedMask(MaskBorderX, MaskBorderY, MaskBorderW, MaskBorderH, true); + public void RemoveMask()=> FillCroppedMask(MaskBorderX, MaskBorderY, MaskBorderW, MaskBorderH, false); + + public void FillBrushMask(int X, int Y, double CircleRadius, bool fill) + { + if (MaskBytes.Length != OriginalH * OriginalW) + throw new ArgumentException("Fatal Error:蒙版尺寸不正确!"); + X = Math.Clamp(X, 0, OriginalW - 1); + Y = Math.Clamp(Y, 0, OriginalH - 1); + int left = (int)Math.Clamp(X - CircleRadius, 0, OriginalW - 1); + int right = (int)Math.Clamp(X + CircleRadius, 0, OriginalW - 1); + int top = (int)Math.Clamp(Y - CircleRadius, 0, OriginalH - 1); + int bottom = (int)Math.Clamp(Y + CircleRadius, 0, OriginalH - 1); + double radiusSquared = CircleRadius * CircleRadius; + for (int i = top; i <= bottom; i++) + { + int iStride = i * OriginalW; + for (int j = left; j <= right; j++) + { + double dx = j - X; + double dy = i - Y; + if (dx * dx + dy * dy <= radiusSquared) + { + MaskBytes[iStride + j] = fill; + } + } + } + MaskBytes = MaskBytes; + } + + private byte[] ShiftBytes(byte[] bytes, double shift, byte[] aBytes, bool isABytes = false) + { + byte[] newBytes = new byte[bytes.Length]; + if (IsProtectTransparent) + { + for (int i = 0; i < bytes.Length; i++) + { + if (UseMask) + { + if (MaskBytes[i]) + { + if (aBytes[i] == 0) + newBytes[i] = bytes[i]; // keep transparent pixels unchanged + else + { + int newValue = (int)bytes[i] + (int)shift; + if (newValue >= 255) newValue = 255; + if (newValue <= 0) newValue = isABytes ? 1 : 0; // prevent alpha from being 0 + newBytes[i] = (byte)newValue; + } + } + else + { + int newValue = (int)bytes[i]; + newBytes[i] = (byte)newValue; + } + } + else + { + if (aBytes[i] == 0) + newBytes[i] = bytes[i]; // keep transparent pixels unchanged + else + { + int newValue = (int)bytes[i] + (int)shift; + if (newValue >= 255) newValue = 255; + if (newValue <= 0) newValue = isABytes ? 1 : 0; // prevent alpha from being 0 + newBytes[i] = (byte)newValue; + } + } + } + } + else + { + for (int i = 0; i < bytes.Length; i++) + { + if (UseMask) + { + if (MaskBytes[i]) + { + int newValue = (int)bytes[i] + (int)shift; + if (newValue >= 255) newValue = 255; + if (newValue <= 0) newValue = 0; + newBytes[i] = (byte)newValue; + } + else + { + int newValue = (int)bytes[i]; + newBytes[i] = (byte)newValue; + } + } + else + { + int newValue = (int)bytes[i] + (int)shift; + if (newValue >= 255) newValue = 255; + if (newValue <= 0) newValue = 0; + newBytes[i] = (byte)newValue; + } + } + } + return newBytes; + } + } +} diff --git a/FancyInput/ViewModels/MacroActionViewModel.cs b/FancyInput/ViewModels/MacroActionViewModel.cs new file mode 100644 index 0000000..14eb46b --- /dev/null +++ b/FancyInput/ViewModels/MacroActionViewModel.cs @@ -0,0 +1,740 @@ +using FancyInput.Common; +using FancyInput.Models; +using System.Text.Json; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Media; +namespace FancyInput.ViewModels; + +using FancyInput.Views; +using System.Windows.Input; + +public enum MacroActionType +{ + None, + Command, + Keyboard, + MouseButton, + MouseMove, + MouseWheel, + Delay, +} + +public class MacroAction : ViewModelBase +{ + private string _name = "动作"; + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + private Geometry? _icon = null; + public Geometry? Icon + { + get => _icon; + set => SetProperty(ref _icon, value); + } + private int _iconMargin = 1; + public int IconMargin + { + get => _iconMargin; + set => SetProperty(ref _iconMargin, value); + } + private int _iconShiftX = 0; + public int IconShiftX + { + get => _iconShiftX; + set => SetProperty(ref _iconShiftX, value); + } + private int _iconShiftY = 0; + public int IconShiftY + { + get => _iconShiftY; + set => SetProperty(ref _iconShiftY, value); + } + private bool _isIconFilpped = false; + public bool IsIconFilpped + { + get => _isIconFilpped; + set => SetProperty(ref _isIconFilpped, value); + } + + + private bool _isSelected = false; + public bool IsSelected + { + get => _isSelected; + set + { + SetProperty(ref _isSelected, value); + OnPropertyChanged(nameof(Background)); + OnPropertyChanged(nameof(IsSelectedVisibility)); + } + } + public Visibility IsSelectedVisibility => IsSelected ? Visibility.Visible : Visibility.Collapsed; + public SolidColorBrush Background => IsSelected ? new SolidColorBrush(Color.FromArgb(0x11, 0x00, 0x00, 0x00)) + : new SolidColorBrush(Colors.Transparent); + + private MacroActionType _actionType = MacroActionType.None; + public MacroActionType ActionType + { + get => _actionType; + set => SetProperty(ref _actionType, value); + } + + private int _preWait = 0; + public int PreWait + { + get => _preWait; + set + { + int wait_time_ms = Math.Min(Math.Max(0, value), 60000); + SetProperty(ref _preWait, wait_time_ms); + OnPropertyChanged(nameof(PreWaitVisibility)); + } + } + + protected int _duration = 0; + public virtual int Duration + { + get => _duration; + set + { + int duration_ms = Math.Min(Math.Max(0, value), 60000); + SetProperty(ref _duration, duration_ms); + OnPropertyChanged(nameof(DurationVisibility)); + } + } + + private int _postWait = 0; + public int PostWait + { + get => _postWait; + set + { + int wait_time_ms = Math.Min(Math.Max(0, value), 60000); + SetProperty(ref _postWait, wait_time_ms); + OnPropertyChanged(nameof(PostWaitVisibility)); + } + } + private int _repeatCount = 1; + public int RepeatCount + { + get => _repeatCount; + set + { + int count = Math.Min(Math.Max(1, value), 1000); + SetProperty(ref _repeatCount, count); + OnPropertyChanged(nameof(RepeatCountVisibility)); + } + } + public int TotalDuration => (PreWait + Duration + PostWait) * RepeatCount; + public int StartTime { get; set; } + public int EndTime => StartTime + Duration; + + public Visibility PreWaitVisibility => _preWait > 0 ? Visibility.Visible : Visibility.Collapsed; + public Visibility DurationVisibility => _duration > 0 ? Visibility.Visible : Visibility.Collapsed; + public Visibility PostWaitVisibility => _postWait > 0 ? Visibility.Visible : Visibility.Collapsed; + public Visibility RepeatCountVisibility => _repeatCount > 1 ? Visibility.Visible : Visibility.Collapsed; + + public MacroAction Clone() => FromDto(ToDto()); + + /// + /// 将当前 MacroAction 序列化为 DTO(仅包含数据属性,不含 UI 状态和计算属性) + /// + public MacroActionDto ToDto() + { + var dto = new MacroActionDto { ActionType = ActionType }; + + // 反射遍历当前类型及其基类中所有可读写的公共实例属性 + foreach (var prop in GetType().GetProperties( + System.Reflection.BindingFlags.Public | + System.Reflection.BindingFlags.Instance)) + { + if (!prop.CanRead || !prop.CanWrite) continue; + if (DtoSkipProperties.Contains(prop.Name)) continue; + + dto.Properties[prop.Name] = prop.GetValue(this); + } + return dto; + } + + /// + /// 从 DTO 反序列化创建 MacroAction 实例 + /// + public static MacroAction FromDto(MacroActionDto dto) + { + var action = CreateInstance(dto.ActionType); + action.CopyFrom(dto); + return action; + } + + /// + /// 从 DTO 原地恢复数据到当前实例(保留同一引用,不破坏 UI 绑定) + /// + public void CopyFrom(MacroActionDto dto) + { + foreach (var kvp in dto.Properties) + { + var prop = GetType().GetProperty(kvp.Key, + System.Reflection.BindingFlags.Public | + System.Reflection.BindingFlags.Instance); + if (prop != null && prop.CanWrite) + { + var value = kvp.Value; + // 处理 JsonElement 反序列化时的类型转换 + if (value is System.Text.Json.JsonElement jsonElement) + { + value = System.Text.Json.JsonSerializer.Deserialize(jsonElement.GetRawText(), prop.PropertyType); + } + prop.SetValue(this, value); + } + } + } + + /// + /// 根据 ActionType 创建对应的子类实例 + /// + private static MacroAction CreateInstance(MacroActionType actionType) => actionType switch + { + MacroActionType.Command => new MacroActionCommand(), + MacroActionType.Keyboard => new MacroActionKeyboardButton(), + MacroActionType.MouseButton => new MacroActionMouseButton(), + MacroActionType.MouseMove => new MacroActionMouseMove(), + MacroActionType.MouseWheel => new MacroActionMouseWheel(), + MacroActionType.Delay => new MacroActionDelay(), + _ => throw new ArgumentException($"未知的 MacroActionType: {actionType}") + }; + + /// + /// DTO 序列化时跳过的属性名(UI 状态、计算属性、命令等不可序列化成员) + /// + private static readonly HashSet DtoSkipProperties = new() + { + // UI 状态 + nameof(IsSelected), + // WPF 类型(Geometry 有 Transform.Inverse 循环引用,不可序列化) + nameof(Icon), + // 计算属性 + nameof(TotalDuration), + nameof(EndTime), + nameof(Background), + nameof(IsSelectedVisibility), + nameof(PreWaitVisibility), + nameof(PostWaitVisibility), + // 子类计算属性 + "ShortName", + "DownLabelVisibility", + "UpLabelVisibility", + // 静态/类型信息 + "AvailableKeys", + "AvailableButtons", + "AvailableMoveModes", + "AvailableWheelDirections", + // ICommand(不可序列化) + "CaptureFromCommand", + "CaptureToCommand", + }; +} + +/// +/// MacroAction 的数据传输对象,用于序列化/反序列化和 Clone +/// +public class MacroActionDto +{ + public MacroActionType ActionType { get; set; } + public Dictionary Properties { get; set; } = new(); +} + +// 执行命令 +public class MacroActionCommand : MacroAction +{ + private string _command = ""; + public string Command + { + get => _command; + set => SetProperty(ref _command, value); + } + private bool _waitUntilExit = true; + public bool WaitUntilExit + { + get => _waitUntilExit; + set => SetProperty(ref _waitUntilExit, value); + } + public MacroActionCommand() + { + Name = "执行命令"; + ActionType = MacroActionType.Command; + Icon = PathDataGeometry.TerminalRound; + } + +} + +// 键盘 +public class MacroActionKeyboardButton : MacroAction +{ + public Array AvailableKeys => Enum.GetValues(typeof(FipRawKeys)); + private FipRawKeys _key = FipRawKeys.None; + public FipRawKeys Key + { + get => _key; + set + { + SetProperty(ref _key, value); + OnPropertyChanged(nameof(ShortName)); + } + } + + private bool _down = true; + public bool Down + { + get => _down; + set + { + SetProperty(ref _down, value); + OnPropertyChanged(nameof(DownLabelVisibility)); + } + } + + private bool _up = true; + public bool Up + { + get => _up; + set + { + SetProperty(ref _up, value); + OnPropertyChanged(nameof(UpLabelVisibility)); + } + } + + public Visibility DownLabelVisibility => _down? Visibility.Visible:Visibility.Collapsed; + public Visibility UpLabelVisibility => _up ? Visibility.Visible : Visibility.Collapsed; + public string ShortName => _key.ToString().Length > 4 + ? _key.ToString().Substring(0, 4) + : _key.ToString(); + + + public MacroActionKeyboardButton() + { + Name = "键盘"; + ActionType = MacroActionType.Keyboard; + Icon = PathDataGeometry.KeyBoard; + IsIconFilpped = true; + } +} + +//鼠标 +public class MacroActionMouseButton : MacroAction +{ + public Array AvailableButtons => Enum.GetValues(typeof(MouseButtons)); + private MouseButtons _button = MouseButtons.None; + public MouseButtons Button + { + get => _button; + set + { + SetProperty(ref _button, value); + OnPropertyChanged(nameof(ShortName)); + } + } + public string ShortName => _button switch + { + MouseButtons.Left => "L", + MouseButtons.Right => "R", + MouseButtons.Middle => "M", + MouseButtons.XButton1 => "X1", + MouseButtons.XButton2 => "X2", + _ => _button.ToString() + }; + + private bool _down = true; + public bool Down + { + get => _down; + set + { + SetProperty(ref _down, value); + OnPropertyChanged(nameof(DownLabelVisibility)); + } + } + + private bool _up = true; + public bool Up + { + get => _up; + set + { + SetProperty(ref _up, value); + OnPropertyChanged(nameof(UpLabelVisibility)); + } + } + + public Visibility DownLabelVisibility => _down ? Visibility.Visible : Visibility.Collapsed; + public Visibility UpLabelVisibility => _up ? Visibility.Visible : Visibility.Collapsed; + + public MacroActionMouseButton() + { + Name = "鼠标按键"; + ActionType = MacroActionType.MouseButton; + Icon = PathDataGeometry.Mouse; + } +} + +public class MacroActionMouseMove : MacroAction +{ + public Array AvailableMoveModes => Enum.GetValues(typeof(MousMoveMode)); + private MousMoveMode _moveMode = MousMoveMode.Absolute; + public MousMoveMode MoveMode + { + get => _moveMode; + set => SetProperty(ref _moveMode, value); + } + + private int _fromX; + public int FromX + { + get => _fromX; + set => SetProperty(ref _fromX, value); + } + + private int _fromY; + public int FromY + { + get => _fromY; + set => SetProperty(ref _fromY, value); + } + + private int _toX; + public int ToX + { + get => _toX; + set => SetProperty(ref _toX, value); + } + + private int _toY; + public int ToY + { + get => _toY; + set => SetProperty(ref _toY, value); + } + + private int _steps = 10; + public int Steps + { + get => _steps; + set + { + int step_count = Math.Min(Math.Max(1, value), 100); + SetProperty(ref _steps, step_count); + } + } + + [System.Text.Json.Serialization.JsonIgnore] + public ICommand CaptureFromCommand { get; } + [System.Text.Json.Serialization.JsonIgnore] + public ICommand CaptureToCommand { get; } + + public MacroActionMouseMove() + { + Name = "鼠标移动"; + ActionType = MacroActionType.MouseMove; + Icon = PathDataGeometry.Cursor; + CaptureFromCommand = new RelayCommand(() => + { + FullScreenMask fullScreenMask = new FullScreenMask(); + fullScreenMask.Cursor = System.Windows.Input.Cursors.Cross; + fullScreenMask.ShowDialog(); + if (fullScreenMask.DialogResult == true) + { + FromX = fullScreenMask.X; + FromY = fullScreenMask.Y; + } + }); + CaptureToCommand = new RelayCommand(() => + { + FullScreenMask fullScreenMask = new FullScreenMask(); + fullScreenMask.Cursor = System.Windows.Input.Cursors.Cross; + fullScreenMask.ShowDialog(); + if (fullScreenMask.DialogResult == true) + { + ToX = fullScreenMask.X; + ToY = fullScreenMask.Y; + } + }); + } +} + +public class MacroActionMouseWheel : MacroAction +{ + public Array AvailableWheelDirections => Enum.GetValues(typeof(MouseWheelDirection)); + private MouseWheelDirection _wheelDirection = MouseWheelDirection.Up; + public MouseWheelDirection WheelDirection + { + get => _wheelDirection; + set => SetProperty(ref _wheelDirection, value); + } + + private int _steps = 10; + public int Steps + { + get => _steps; + set + { + int step_count = Math.Min(Math.Max(1, value), 100); + SetProperty(ref _steps, step_count); + } + } + + public MacroActionMouseWheel() + { + Name = "鼠标滚轮"; + ActionType = MacroActionType.MouseWheel; + Icon = PathDataGeometry.MouseScroll; + IconShiftX = -2; + } +} + +// 延时 +public class MacroActionDelay : MacroAction +{ + public override int Duration + { + get => _duration; + set + { + int duration_ms = Math.Max(0, value); + SetProperty(ref _duration, duration_ms); + } + } + + public MacroActionDelay() + { + Name = "延时"; + ActionType = MacroActionType.Delay; + Icon = PathDataGeometry.Right; + } +} + + + + + +// ======================= +// 手柄按键、扳机、摇杆暂不实现 +// ======================= +public class MacroActionGamepadButtonDown : MacroAction +{ + private FIPGamepadButtonflags _button = FIPGamepadButtonflags.None; + public FIPGamepadButtonflags Button + { + get => _button; + set => SetProperty(ref _button, value); + } + + public MacroActionGamepadButtonDown() + { + Name = "手柄按下"; + Icon = PathDataGeometry.Gamepad; + } +} + +public class MacroActionGamepadButtonUp : MacroAction +{ + private FIPGamepadButtonflags _button = FIPGamepadButtonflags.None; + public FIPGamepadButtonflags Button + { + get => _button; + set => SetProperty(ref _button, value); + } + + public MacroActionGamepadButtonUp() + { + Name = "手柄抬起"; + Icon = PathDataGeometry.Gamepad; + } +} + +public class MacroActionGamepadButtonPress : MacroAction +{ + private FIPGamepadButtonflags _button = FIPGamepadButtonflags.None; + public FIPGamepadButtonflags Button + { + get => _button; + set => SetProperty(ref _button, value); + } + + public override int Duration + { + get => _duration; + set => SetProperty(ref _duration, value); + } + + public MacroActionGamepadButtonPress() + { + Name = "手柄点击"; + Icon = PathDataGeometry.Gamepad; + } +} + +public class MacroActionGamepadTrigger : MacroAction +{ + private Side _side = Side.Left; + public Side Side + { + get => _side; + set => SetProperty(ref _side, value); + } + + private int _value = 0; + public int Value + { + get => _value; + set => SetProperty(ref _value, value); + } + + public MacroActionGamepadTrigger() + { + Name = "手柄扳机"; + Icon = PathDataGeometry.Gamepad; + } +} + +public class MacroActionGamepadTriggerSequence : MacroAction +{ + private Side _side = Side.Left; + public Side Side + { + get => _side; + set => SetProperty(ref _side, value); + } + + private int _from = 0; + public int From + { + get => _from; + set => SetProperty(ref _from, value); + } + + private int _to = 0; + public int To + { + get => _to; + set => SetProperty(ref _to, value); + } + + private int _steps = 10; + public int Steps + { + get => _steps; + set + { + int step_count = Math.Min(Math.Max(1, value), 100); + SetProperty(ref _steps, step_count); + } + } + + public override int Duration + { + get => _duration; + set => SetProperty(ref _duration, value); + } + + public MacroActionGamepadTriggerSequence() + { + Name = "手柄扳机序列"; + Icon = PathDataGeometry.Gamepad; + } +} + +public class MacroActionGamepadStick : MacroAction +{ + private Side _side = Side.Left; + public Side Side + { + get => _side; + set => SetProperty(ref _side, value); + } + + private int _x = 0; + public int X + { + get => _x; + set => SetProperty(ref _x, value); + } + + private int _y = 0; + public int Y + { + get => _y; + set => SetProperty(ref _y, value); + } + + public MacroActionGamepadStick() + { + Name = "手柄摇杆"; + Icon = PathDataGeometry.Gamepad; + } +} + +public class MacroActionGamepadStickSequence : MacroAction +{ + private Side _side = Side.Left; + public Side Side + { + get => _side; + set => SetProperty(ref _side, value); + } + + private int _fromX = 0; + public int FromX + { + get => _fromX; + set => SetProperty(ref _fromX, value); + } + + private int _fromY = 0; + public int FromY + { + get => _fromY; + set => SetProperty(ref _fromY, value); + } + + private int _toX = 0; + public int ToX + { + get => _toX; + set => SetProperty(ref _toX, value); + } + + private int _toY = 0; + public int ToY + { + get => _toY; + set => SetProperty(ref _toY, value); + } + + private int _steps = 10; + public int Steps + { + get => _steps; + set + { + int step_count = Math.Min(Math.Max(1, value), 100); + SetProperty(ref _steps, step_count); + } + } + + public override int Duration + { + get => _duration; + set => SetProperty(ref _duration, value); + } + + public MacroActionGamepadStickSequence() + { + Name = "手柄摇杆序列"; + Icon = PathDataGeometry.Gamepad; + } +} + + diff --git a/FancyInput/ViewModels/MacroTrackViewModel.cs b/FancyInput/ViewModels/MacroTrackViewModel.cs new file mode 100644 index 0000000..41175b3 --- /dev/null +++ b/FancyInput/ViewModels/MacroTrackViewModel.cs @@ -0,0 +1,548 @@ +using FancyInput.Models; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Text.Json; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Media; + +using ButtonState = FancyInput.Models.ButtonState; +namespace FancyInput.ViewModels +{ + public class MacroTrackViewModel : ViewModelBase, IDisposable + { + private string _name = "轨道"; + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + private int _idx = 0; + public int Idx + { + get => _idx; + set => SetProperty(ref _idx, value); + } + + private bool _isSelected = false; + public bool IsSelected + { + get => _isSelected; + set + { + SetProperty(ref _isSelected, value); + OnPropertyChanged(nameof(BorderBrush)); + } + } + + private MacroAction? _selectedAction; + public MacroAction? SelectedAction + { + get => _selectedAction; + set + { + if (_selectedAction != null) + _selectedAction.IsSelected = false; + if (value != null) + value.IsSelected = true; + SetProperty(ref _selectedAction, value); + OnPropertyChanged(nameof(ActionVisibility)); + } + } + + public Visibility ActionVisibility => SelectedAction != null ? Visibility.Visible : Visibility.Collapsed; + + public Array MacroAddDirectionList => Enum.GetValues(typeof(MacroAddDirection)); + private MacroAddDirection _addDirection = MacroAddDirection.Right; + public MacroAddDirection AddDirection + { + get => _addDirection; + set => SetProperty(ref _addDirection, value); + } + + public SolidColorBrush BorderBrush => IsSelected ? new SolidColorBrush(Colors.MediumPurple) : new SolidColorBrush(Colors.Gray); + + public ObservableCollection MacroActions { get; set; } = new ObservableCollection(); + + private Stopwatch _stopwatch = new Stopwatch(); + private int _startTime = 0; + public InputParser? InputParser { get; set; } = null; + private bool _recordMouseMove = false; + public bool RecordMouseMove + { + get => _recordMouseMove; + set => SetProperty(ref _recordMouseMove, value); + } + private int _recordStartIdx = 0; + public int RecordStartIdx + { + get => _recordStartIdx; + set => SetProperty(ref _recordStartIdx, value); + } + private int _recordingIdx = 0; + private bool _isRecording = false; + public bool IsRecording + { + get => _isRecording; + set + { + if (IsDetectable()) + { + if (!_isRecording && value) + { + _stopwatch.Restart(); + _startTime = 0; + ResetRecordingState(); + StartDetecting(); + } + else if (_isRecording && !value) + { + _stopwatch.Stop(); + StopDetecting(); + } + SetProperty(ref _isRecording, value); + } + else if (!_isRecording && value) + { + AppMessageBox.Show("输入解析器未初始化,无法录制输入"); + } + } + } + + // ===== O(1) 录制状态机 ===== + // 键盘:当前按下的键 + private FipRawKeys? _recordingDownKey = null; + private MacroActionKeyboardButton? _recordingCurrentKeyAction = null; + + // 鼠标按键:当前按下的按钮 + private MouseButtons? _recordingDownMouseButton = null; + private MacroActionMouseButton? _recordingCurrentMouseAction = null; + + // 鼠标移动:当前是否在移动中 + private bool _recordingIsMoving = false; + private MacroActionMouseMove? _recordingCurrentMouseMoveAction = null; + + // 鼠标滚轮:当前滚轮方向 + private bool _recordingIsWheelingUp = false; + private bool _recordingIsWheelingDown = false; + private MacroActionMouseWheel? _recordingCurrentWheelAction = null; + + // 上一个动作(用于 PreWait/PostWait 计算) + private MacroAction? _recordingLastAction = null; + + private void ResetRecordingState() + { + _recordingIdx = 0; + _recordingDownKey = null; + _recordingCurrentKeyAction = null; + _recordingDownMouseButton = null; + _recordingCurrentMouseAction = null; + _recordingIsMoving = false; + _recordingCurrentMouseMoveAction = null; + _recordingIsWheelingUp = false; + _recordingIsWheelingDown = false; + _recordingCurrentWheelAction = null; + _recordingLastAction = null; + } + + public bool IsDetectable() => InputParser != null; + public void StartDetecting() + { + if (InputParser == null) throw new InvalidOperationException("InputParser is not set, cannot start detecting."); + InputParser.GetInput += GetInput; + } + public void StopDetecting() + { + if (InputParser == null) throw new InvalidOperationException("InputParser is not set, cannot stop detecting."); + InputParser.GetInput -= GetInput; + } + + /// + /// O(1) 实时处理每个输入事件,直接创建/更新 MacroAction + /// + public void GetInput(object? sender, InputArgs e) + { + int timestamp = (int)_stopwatch.ElapsedMilliseconds; + if (_startTime == 0) + { + _startTime = timestamp; + } + timestamp = timestamp - _startTime; + + switch (e.Device) + { + case InputDevice.Keyboard: + FinalizeCurrentWheel(); + FinalizeCurrentMove(); + HandleKeyboardInput(timestamp, e); + break; + case InputDevice.MouseButton: + FinalizeCurrentWheel(); + FinalizeCurrentMove(); + HandleMouseButtonInput(timestamp, e); + break; + case InputDevice.MouseMove: + FinalizeCurrentWheel(); + if (_recordMouseMove) + HandleMouseMoveInput(timestamp, e); + break; + case InputDevice.MouseWheel: + FinalizeCurrentMove(); + HandleMouseWheelInput(timestamp, e); + break; + default: + return; + } + } + + private void HandleKeyboardInput(int timestamp, InputArgs e) + { + if (!e.RawKey.HasValue || !e.State.HasValue) return; + + FipRawKeys key = e.RawKey.Value; + ButtonState state = e.State.Value; + + if (state == ButtonState.Pressed) + { + _recordingDownKey = key; + var keyAction = new MacroActionKeyboardButton + { + Key = key, + Down = true, + Up = false, + StartTime = timestamp, + Duration = 0, + }; + _recordingCurrentKeyAction = keyAction; + if (_recordingLastAction != null) + { + MacroAction? delay = SetWaitOrDelay(_recordingLastAction, _recordingCurrentKeyAction); + if (delay != null) AddActionToRecord(delay); + } + AddActionToRecord(keyAction); + } + else if (state == ButtonState.Released && _recordingDownKey == key) + { + _recordingDownKey = null; + if (_recordingCurrentKeyAction != null) + { + _recordingCurrentKeyAction.Up = true; + _recordingCurrentKeyAction.Duration = timestamp - _recordingCurrentKeyAction.StartTime; + } + _recordingCurrentKeyAction = null; + } + else + { + // 单独的释放 + _recordingDownKey = null; + _recordingCurrentKeyAction = null; + var keyAction = new MacroActionKeyboardButton + { + Key = key, + Down = false, + Up = true, + StartTime = timestamp, + Duration = 0, + }; + if (_recordingLastAction != null) + { + MacroAction? delay = SetWaitOrDelay(_recordingLastAction, keyAction); + if (delay != null) AddActionToRecord(delay); + } + AddActionToRecord(keyAction); + } + } + + private void HandleMouseButtonInput(int timestamp, InputArgs e) + { + if (!e.MouseButton.HasValue || !e.State.HasValue) return; + MouseButtons button = e.MouseButton.Value; + ButtonState state = e.State.Value; + if (state == ButtonState.Pressed) + { + _recordingDownMouseButton = button; + var mouseAction = new MacroActionMouseButton + { + Button = button, + Down = true, + Up = false, + StartTime = timestamp, + Duration = 0, + }; + _recordingCurrentMouseAction = mouseAction; + if (_recordingLastAction != null) + { + MacroAction? delay = SetWaitOrDelay(_recordingLastAction, _recordingCurrentMouseAction); + if (delay != null) AddActionToRecord(delay); + } + AddActionToRecord(mouseAction); + } + else if (state == ButtonState.Released && _recordingDownMouseButton == button) + { + _recordingDownMouseButton = null; + if (_recordingCurrentMouseAction != null) + { + _recordingCurrentMouseAction.Up = true; + _recordingCurrentMouseAction.Duration = timestamp - _recordingCurrentMouseAction.StartTime; + } + _recordingCurrentMouseAction = null; + } + else + { + _recordingDownMouseButton = null; + _recordingCurrentMouseAction = null; + var mouseAction = new MacroActionMouseButton + { + Button = button, + Down = false, + Up = true, + StartTime = timestamp, + Duration = 0, + }; + if (_recordingLastAction != null) + { + MacroAction? delay = SetWaitOrDelay(_recordingLastAction, mouseAction); + if (delay != null) AddActionToRecord(delay); + } + AddActionToRecord(mouseAction); + } + } + + private void HandleMouseMoveInput(int timestamp, InputArgs e) + { + if (!e.MoveState.HasValue || e.ValueVector == null || e.ValueVector.Length < 4) return; + MoveState moveState = e.MoveState.Value; + + if (moveState == MoveState.StartMove) + { + _recordingIsMoving = true; + var moveAction = new MacroActionMouseMove + { + FromX = e.ValueVector[2], + FromY = e.ValueVector[3], + ToX = e.ValueVector[2], + ToY = e.ValueVector[3], + Steps = 1, + StartTime = timestamp, + Duration = 0, + }; + _recordingCurrentMouseMoveAction = moveAction; + if (_recordingLastAction != null) + { + MacroAction? delay = SetWaitOrDelay(_recordingLastAction, moveAction); + if (delay != null) AddActionToRecord(delay); + } + AddActionToRecord(moveAction); + } + else if (moveState == MoveState.StopMove && _recordingIsMoving) + { + _recordingIsMoving = false; + if (_recordingCurrentMouseMoveAction != null) + { + _recordingCurrentMouseMoveAction.ToX = e.ValueVector[2]; + _recordingCurrentMouseMoveAction.ToY = e.ValueVector[3]; + _recordingCurrentMouseMoveAction.Duration = timestamp - _recordingCurrentMouseMoveAction.StartTime; + double distance = Math.Sqrt( + Math.Pow(_recordingCurrentMouseMoveAction.ToX - _recordingCurrentMouseMoveAction.FromX, 2) + + Math.Pow(_recordingCurrentMouseMoveAction.ToY - _recordingCurrentMouseMoveAction.FromY, 2)); + _recordingCurrentMouseMoveAction.Steps = Math.Min(Math.Max((int)(distance / 10), 1), 100); + } + _recordingCurrentMouseMoveAction = null; + } + else if (moveState == MoveState.StopMove) + { + // 单独的停止移动 + _recordingIsMoving = false; + _recordingCurrentMouseMoveAction = null; + var moveAction = new MacroActionMouseMove + { + FromX = e.ValueVector[2], + FromY = e.ValueVector[3], + ToX = e.ValueVector[2], + ToY = e.ValueVector[3], + Steps = 1, + StartTime = timestamp, + Duration = 0, + }; + if (_recordingLastAction != null) + { + MacroAction? delay = SetWaitOrDelay(_recordingLastAction, moveAction); + if (delay != null) AddActionToRecord(delay); + } + AddActionToRecord(moveAction); + } + } + + private void HandleMouseWheelInput(int timestamp, InputArgs e) + { + if (!e.Value.HasValue) return; + + int delta = e.Value.Value; + MouseWheelDirection direction = delta > 0 ? MouseWheelDirection.Up : MouseWheelDirection.Down; + if (!_recordingIsWheelingUp && !_recordingIsWheelingDown) + { + // 开始新的滚轮序列 + var wheelAction = new MacroActionMouseWheel + { + WheelDirection = direction, + Steps = 1, + StartTime = timestamp, + Duration = 0, + }; + _recordingCurrentWheelAction = wheelAction; + _recordingIsWheelingUp = direction == MouseWheelDirection.Up; + _recordingIsWheelingDown = direction == MouseWheelDirection.Down; + if (_recordingLastAction != null) + { + MacroAction? delay = SetWaitOrDelay(_recordingLastAction, wheelAction); + if (delay != null) AddActionToRecord(delay); + } + AddActionToRecord(wheelAction); + } + else if ((_recordingIsWheelingUp && direction == MouseWheelDirection.Up) || + (_recordingIsWheelingDown && direction == MouseWheelDirection.Down)) + { + // 同方向继续滚动,累加步数和时长 + if (_recordingCurrentWheelAction != null) + { + _recordingCurrentWheelAction.Steps += 1; + _recordingCurrentWheelAction.Duration = timestamp - _recordingCurrentWheelAction.StartTime; + } + } + else + { + // 方向改变,结束上一个,开始新的 + FinalizeCurrentWheel(); + var wheelAction = new MacroActionMouseWheel + { + WheelDirection = direction, + Steps = 1, + StartTime = timestamp, + Duration = 0, + }; + _recordingCurrentWheelAction = wheelAction; + _recordingIsWheelingUp = direction == MouseWheelDirection.Up; + _recordingIsWheelingDown = direction == MouseWheelDirection.Down; + if (_recordingLastAction != null) + { + MacroAction? delay = SetWaitOrDelay(_recordingLastAction, wheelAction); + if (delay != null) AddActionToRecord(delay); + } + AddActionToRecord(wheelAction); + } + } + + /// + /// 结束当前正在进行的滚轮动作 + /// + private void FinalizeCurrentWheel() + { + _recordingIsWheelingUp = false; + _recordingIsWheelingDown = false; + _recordingCurrentWheelAction = null; + } + private void FinalizeCurrentMove() + { + _recordingIsMoving = false; + _recordingCurrentMouseMoveAction = null; + } + + /// + /// O(1) 添加动作到录制列表,同时更新 UI + /// + private void AddActionToRecord(MacroAction action) + { + _recordingLastAction = action; + MacroActions.Insert(RecordStartIdx + _recordingIdx, action); + _recordingIdx++; + } + + private MacroAction? SetWaitOrDelay(MacroAction lastAction, MacroAction newAction) + { + int interval = newAction.StartTime - lastAction.EndTime; + if (interval >= 5000) + { + var delay = new MacroActionDelay + { + Name = "延时", + ActionType = MacroActionType.Delay, + PreWait = 0, + Duration = interval, + PostWait = 0 + }; + lastAction.PostWait = 0; + newAction.PreWait = 0; + return delay; + + } + else + { + lastAction.PostWait = interval / 2; + newAction.PreWait = interval / 2; + return null; + } + } + + public void Dispose() + { + // 停止录制(取消订阅 InputParser.GetInput 事件) + if (_isRecording) + { + IsRecording = false; + } + // 确保事件已取消订阅 + if (InputParser != null) + { + InputParser.GetInput -= GetInput; + InputParser = null; + } + _stopwatch.Stop(); + } + + public MacroTrackViewModelDto ToDto() + { + return new MacroTrackViewModelDto + { + Name = Name, + Idx = Idx, + AddDirection = AddDirection, + RecordMouseMove = RecordMouseMove, + MacroActionDtos = MacroActions.Select(a => a.ToDto()).ToList(), + }; + } + + public static MacroTrackViewModel FromDto(MacroTrackViewModelDto dto) + { + var vm = new MacroTrackViewModel(); + vm.CopyFrom(dto); + return vm; + } + + /// + /// 从 DTO 原地恢复数据到当前实例(保留 InputParser 引用和 UI 绑定) + /// + public void CopyFrom(MacroTrackViewModelDto dto) + { + Name = dto.Name; + Idx = dto.Idx; + AddDirection = dto.AddDirection; + RecordMouseMove = dto.RecordMouseMove; + + MacroActions.Clear(); + foreach (var actionDto in dto.MacroActionDtos) + { + MacroActions.Add(MacroAction.FromDto(actionDto)); + } + } + } + + public class MacroTrackViewModelDto + { + public string Name { get; set; } = "轨道"; + public int Idx { get; set; } + public MacroAddDirection AddDirection { get; set; } = MacroAddDirection.Right; + public bool RecordMouseMove { get; set; } + public List MacroActionDtos { get; set; } = new(); + } + +} diff --git a/FancyInput/ViewModels/MacroViewModel.cs b/FancyInput/ViewModels/MacroViewModel.cs new file mode 100644 index 0000000..4a96470 --- /dev/null +++ b/FancyInput/ViewModels/MacroViewModel.cs @@ -0,0 +1,826 @@ +using FancyInput.Models; +using FancyInput.Views.Controls; +using System.Text.Json; +using System.Threading.Channels; +using System.Windows; +using System.Windows.Documents; + + + +//using System.Windows.Forms; +using System.Windows.Input; +using System.Windows.Media; +using CancellationTokenSource = System.Threading.CancellationTokenSource; +using InputDevice = FancyInput.Models.InputDevice; +namespace FancyInput.ViewModels +{ + public enum TriggerCommand + { + Start, + Stop + } + + public class MacroViewModel:ViewModelBase,IDisposable + { + public event Action? LogEvent; + + private void LogEventHandler(string msg, OutputLevel level) + { + if (_enableLog) + LogEvent?.Invoke(this, msg, level); + } + private void StdOutEventHandler(string msg) => LogEvent?.Invoke(this, msg, OutputLevel.Info); + private void StdErrEventHandler(string msg) => LogEvent?.Invoke(this, msg, OutputLevel.Error); + + public MacroViewModel() + { + SelectedTriggerDevice = TriggerDevices.First(); + } + public void Dispose() + { + // 1. 先取消订阅 InputParser 事件,防止 StopListenTrigger 期间仍有事件触发 + if (InputParser != null) + { + InputParser.GetInput -= InputParser_GetInput; + InputParser.GetInput -= InputParser_MacroCommandTranslator; + } + // 2. 停止宏监听和运行中的宏 + StopListenTrigger(); + // 3. 清空 InputParser 引用 + InputParser = null; + // 4. Dispose 子轨道(停止录制 + 清理事件订阅) + PreMacroTrack.Dispose(); + OnMacroTrack.Dispose(); + PostMacroTrack.Dispose(); + // 5. 清空事件订阅者,防止外部持有引用无法 GC + LogEvent = null; + } + + private InputParser? _inputParser; + public InputParser? InputParser + { + get=> _inputParser; + set + { + _inputParser = value; + PreMacroTrack.InputParser = value; + OnMacroTrack.InputParser = value; + PostMacroTrack.InputParser = value; + } + + } + private string _name = "宏"; + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + public int Idx { get; set; } = 0; + private bool _isSelected = false; + public bool IsSelected + { + get => _isSelected; + set => SetProperty(ref _isSelected, value); + } + private bool _enableLog = true; + public bool EnableLog + { + get => _enableLog; + set => SetProperty(ref _enableLog, value); + } + + private MacroTrackViewModel? _selectedTrack = null; + public MacroTrackViewModel? SelectedTrack + { + get => _selectedTrack; + set + { + if (_selectedTrack != null) + _selectedTrack.IsSelected = false; + if (value != null) + value.IsSelected = true; + if (_selectedTrack != null && !ReferenceEquals(value, _selectedTrack)) + { + _selectedTrack.IsRecording = false; + } + SetProperty(ref _selectedTrack, value); + OnPropertyChanged(nameof(TrackVisibility)); + } + } + public Visibility TrackVisibility => SelectedTrack != null ? Visibility.Visible : Visibility.Collapsed; + + public List TriggerDevices { get; set; } = new List + { + InputDevice.Keyboard, + InputDevice.MouseButton, + InputDevice.XInputButton, + InputDevice.SDLButton + }; + private InputDevice _selectedTriggerDevice; + public InputDevice SelectedTriggerDevice + { + get => _selectedTriggerDevice; + set + { + SetProperty(ref _selectedTriggerDevice, value); + switch (value) + { + case InputDevice.Keyboard: + TriggerBindingList = Enum.GetValues(typeof(FipRawKeys)); + SelectedTriggerBinding = SelectedTriggerKey; + break; + case InputDevice.MouseButton: + TriggerBindingList = Enum.GetValues(typeof(System.Windows.Forms.MouseButtons)); + SelectedTriggerBinding = SelectedMouseButton; + break; + case InputDevice.XInputButton: + case InputDevice.SDLButton: + TriggerBindingList = Enum.GetValues(typeof(FIPGamepadButtonflags)); + SelectedTriggerBinding = SelectedGamepadButton; + break; + default: + TriggerBindingList = null; + SelectedTriggerBinding = null; + break; + } + } + } + + private Array? _triggerBindingList; + public Array? TriggerBindingList + { + get => _triggerBindingList; + set => SetProperty(ref _triggerBindingList, value); + } + private Enum? _selectedTriggerBinding; + public Enum? SelectedTriggerBinding + { + get => _selectedTriggerBinding; + set => SetProperty(ref _selectedTriggerBinding, value); + } + + public static Array TriggerKeyList => Enum.GetValues(typeof(FipRawKeys)); + private FipRawKeys _selectedTriggerKey = FipRawKeys.None; + public FipRawKeys SelectedTriggerKey + { + get => _selectedTriggerKey; + set => SetProperty(ref _selectedTriggerKey, value); + } + + public static Array MouseButtonList => Enum.GetValues(typeof(System.Windows.Forms.MouseButtons)); + private System.Windows.Forms.MouseButtons _selectedMouseButton; + public System.Windows.Forms.MouseButtons SelectedMouseButton + { + get => _selectedMouseButton; + set => SetProperty(ref _selectedMouseButton, value); + } + + public static Array GamepadButtonList => Enum.GetValues(typeof(FIPGamepadButtonflags)); + private FIPGamepadButtonflags _selectedGamepadButton = Models.FIPGamepadButtonflags.A; + public FIPGamepadButtonflags SelectedGamepadButton + { + get => _selectedGamepadButton; + set => SetProperty(ref _selectedGamepadButton, value); + } + + public static Array TriggerTimingList => Enum.GetValues(typeof(TriggerTiming)); + private TriggerTiming _selectedTriggerTiming = TriggerTiming.Press; + public TriggerTiming SelectedTriggerTiming + { + get => _selectedTriggerTiming; + set + { + if (_selectedTriggerMode == TriggerMode.Hold && _selectedTriggerTiming== TriggerTiming.Press && value == TriggerTiming.Release) + { + LogEventHandler("触发模式为按住时,触发时机不能设置为释放",OutputLevel.Warn); + System.Windows.Application.Current.Dispatcher.BeginInvoke( + new Action(() => OnPropertyChanged(nameof(SelectedTriggerTiming)))); + } + else + SetProperty(ref _selectedTriggerTiming, value); + + } + } + + public static Array TriggerModeList => Enum.GetValues(typeof(TriggerMode)); + private TriggerMode _selectedTriggerMode; + public TriggerMode SelectedTriggerMode + { + get => _selectedTriggerMode; + set + { + if (value == TriggerMode.Hold && _selectedTriggerTiming == TriggerTiming.Release) + { + SelectedTriggerTiming = TriggerTiming.Press; + LogEventHandler("触发模式为按住时,触发时机已自动设置为按下", OutputLevel.Warn); + } + SetProperty(ref _selectedTriggerMode, value); + } + } + + public MacroTrackViewModel PreMacroTrack { get; set; } = new() { Name = "轨道1", Idx = 1 }; + public MacroTrackViewModel OnMacroTrack { get; set; } = new () { Name = "轨道2", Idx = 2 }; + public MacroTrackViewModel PostMacroTrack { get; set; } = new () { Name = "轨道3", Idx = 3 }; + private string _description = string.Empty; + public string Description + { + get => _description; + set => SetProperty(ref _description, value); + } + + private string _triggerConditionString = string.Empty; + public string TriggerConditionString + { + get => _triggerConditionString; + set => SetProperty(ref _triggerConditionString, value); + } + + private bool _isDetectingInput = false; + public bool IsDetectingInput + { + get => _isDetectingInput; + set + { + SetProperty(ref _isDetectingInput, value); + OnPropertyChanged(nameof(DetectBrush)); + OnPropertyChanged(nameof(DetectString)); + } + } + public SolidColorBrush DetectBrush => IsDetectingInput ? new SolidColorBrush(Colors.Red) : new SolidColorBrush(Color.FromArgb(0xFF, 0x65, 0x1F, 0x65)); + public string DetectString => _isDetectingInput ? "检测中..." : "检测"; + public ICommand DetectCommand => new RelayCommand(Detect); + public void Detect() + { + if (InputParser == null) + { + AppMessageBox.Show("输入解析器未初始化,无法检测输入"); + return; + } + if (_isDetectingInput) + { + IsDetectingInput = false; + InputParser.GetInput -= InputParser_GetInput; + return; + } + IsDetectingInput = true; + InputParser.GetInput -= InputParser_GetInput; + InputParser.GetInput += InputParser_GetInput; + } + + private void InputParser_GetInput(object? sender, InputArgs e) + { + if (InputParser == null) return; + switch (e.Device) + { + case InputDevice.Keyboard: + if (e.RawKey.HasValue) + { + SelectedTriggerKey = e.RawKey.Value; + } + break; + case InputDevice.MouseButton: + if (e.MouseButton.HasValue) + { + SelectedMouseButton = e.MouseButton.Value; + } + break; + case InputDevice.XInputButton: + case InputDevice.SDLButton: + if (e.Flag.HasValue) + { + SelectedGamepadButton = e.Flag.Value; + } + break; + default: + return; + } + SelectedTriggerDevice = e.Device; + IsDetectingInput = false; + InputParser.GetInput -= InputParser_GetInput; + } + public ICommand? ShotStartCommand=> new RelayCommand(() => _triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Start)); + public ICommand? ShotStopCommand => new RelayCommand(() => _triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Stop)); + + private bool _isMacroListening = false; + public bool IsMacroListening + { + get => _isMacroListening; + set + { + if (_isMacroListening == value) return; + if (value) + { + if (IsTriggerMatchMacro()) + { + AppMessageBox.Show("宏的动作序列中包含触发器的动作,无法启用宏,请修改宏的动作序列或触发器设置"); + } + else if (IsMacroTooShort() && (SelectedTriggerMode == TriggerMode.Hold || SelectedTriggerMode == TriggerMode.Toggle)) + { + AppMessageBox.Show($"当前触发模式下,宏的总时长不可小于{ShortTimeThreshold}ms,请修改宏的动作序列或触发器设置"); + } + else + { + StartListenTrigger(); + StartTriggerSource(); + } + } + else + { + StopTriggerSource(); + StopListenTrigger(); + } + OnPropertyChanged(nameof(IsMacroListening)); + OnPropertyChanged(nameof(IsEditable)); + OnPropertyChanged(nameof(Cursor)); + OnPropertyChanged(nameof(Tooltip)); + } + } + private void StartTriggerSource() + { + if (InputParser == null) return; + InputParser.GetInput += InputParser_MacroCommandTranslator; + } + private void StopTriggerSource() + { + if (InputParser == null) return; + InputParser.GetInput -= InputParser_MacroCommandTranslator; + } + private void InputParser_MacroCommandTranslator(object? sender, InputArgs e) + { + if (InputParser == null) return; + + switch (SelectedTriggerMode) + { + case TriggerMode.Once: + if (IsTriggerMatchInputArgs(e, includeTiming: true)) + _triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Start); + break; + case TriggerMode.Hold: + if (SelectedTriggerTiming != TriggerTiming.Press) + { + StdErrEventHandler("内部错误:按住模式只能设置触发时机为按下"); + return; + } + if (!IsTriggerMatchInputArgs(e, includeTiming: false)) return; + if (e.State == ButtonState.Pressed) + _triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Start); + else if (e.State == ButtonState.Released) + _triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Stop); + break; + case TriggerMode.Toggle: + if (IsTriggerMatchInputArgs(e, includeTiming: true)) + { + if (_isMacroRunning) + _triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Stop); + else + _triggerCommandChannel?.Writer.TryWrite(TriggerCommand.Start); + } + break; + } + } + private bool IsTriggerMatchInputArgs(InputArgs e, bool includeTiming=false) + { + if (SelectedTriggerDevice != e.Device) return false; + bool timingMatch = (includeTiming && SelectedTriggerTiming == TriggerTiming.Press && e.State == ButtonState.Pressed) || + (includeTiming && SelectedTriggerTiming == TriggerTiming.Release && e.State == ButtonState.Released); + switch (SelectedTriggerDevice) + { + case InputDevice.Keyboard: + return SelectedTriggerKey == e.RawKey && (!includeTiming|| timingMatch); + case InputDevice.MouseButton: + return SelectedMouseButton == e.MouseButton && (!includeTiming || timingMatch); + case InputDevice.XInputButton: + case InputDevice.SDLButton: + return SelectedGamepadButton == e.Flag && (!includeTiming || timingMatch); + default: + return false; + } + } + private bool IsTriggerMactchAction(MacroAction action) + { + switch (action.GetType()) + { + case Type t when t == typeof(MacroActionKeyboardButton): + var keyAction = action as MacroActionKeyboardButton; + if (keyAction == null) return false; + bool sameKey = SelectedTriggerDevice == InputDevice.Keyboard && SelectedTriggerKey == keyAction.Key; + bool sameTiming = (SelectedTriggerTiming == TriggerTiming.Press && keyAction.Down) || + (SelectedTriggerTiming == TriggerTiming.Release && keyAction.Up); + return sameKey && sameTiming; + case Type t when t == typeof(MacroActionMouseButton): + var mouseAction = action as MacroActionMouseButton; + if (mouseAction == null) return false; + bool sameButton = SelectedTriggerDevice == InputDevice.MouseButton && SelectedMouseButton == mouseAction.Button; + bool sameMouseTiming = (SelectedTriggerTiming == TriggerTiming.Press && mouseAction.Down) || + (SelectedTriggerTiming == TriggerTiming.Release && mouseAction.Up); + return sameButton && sameMouseTiming; + default: + return false; + } + } + private bool IsTriggerMatchMacro() + { + foreach (var action in PreMacroTrack.MacroActions) + { + if (IsTriggerMactchAction(action)) + return true; + } + foreach (var action in OnMacroTrack.MacroActions) + { + if (IsTriggerMactchAction(action)) + return true; + } + foreach (var action in PostMacroTrack.MacroActions) + { + if (IsTriggerMactchAction(action)) + return true; + } + return false; + } + + public int ShortTimeThreshold { get; set; } = 200; + private bool IsMacroTooShort() + { + int totalTime = 0; + for (int i = 0; i < PreMacroTrack.MacroActions.Count; i++) + totalTime += PreMacroTrack.MacroActions[i].TotalDuration; + for (int i = 0; i < OnMacroTrack.MacroActions.Count; i++) + totalTime += OnMacroTrack.MacroActions[i].TotalDuration; + for (int i = 0; i < PostMacroTrack.MacroActions.Count; i++) + totalTime += PostMacroTrack.MacroActions[i].TotalDuration; + return totalTime < ShortTimeThreshold; + } + + public bool IsEditable => !_isMacroListening; + public Cursor Cursor => _isMacroListening ? Cursors.No : Cursors.Arrow; + public string? Tooltip => IsMacroListening ? "宏正在运行中,无法编辑" : null; + private Channel? _triggerCommandChannel; + private CancellationTokenSource? _listenTriggerCts; + //private Task? _triggerListeningTask; + private CancellationTokenSource? _macroCts; + //private Task? macroTask; + private bool _isMacroRunning = false; + private readonly object _lock = new object(); + private void StartListenTrigger() + { + try + { + lock (_lock) + { + if (_isMacroListening) return; + //StopInternal(blocking: true); + _triggerCommandChannel = Channel.CreateUnbounded(); + _listenTriggerCts = new CancellationTokenSource(); + _ = Task.Factory.StartNew( + ProcessTriggerAsync, + _listenTriggerCts.Token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default + ).Unwrap(); + _isMacroListening = true; + LogEventHandler("已启用宏:(" + Name + ")", OutputLevel.Success); + } + } + catch (Exception ex) + { + StdErrEventHandler("启用宏时发生异常: " + ex.Message); + } + } + private void StopListenTrigger() + { + try + { + lock (_lock) + { + if (!_isMacroListening) return; + _listenTriggerCts?.Cancel(); + _triggerCommandChannel?.Writer.TryComplete(); + _isMacroListening = false; + + if (_macroCts != null && !_macroCts.IsCancellationRequested ) + { + try { _macroCts.Cancel(); } + catch (ObjectDisposedException) { } + } + _isMacroRunning = false; + } + } + catch (Exception ex) + { + StdErrEventHandler("禁用宏时发生异常: " + ex.Message); + } + } + private async Task ProcessTriggerAsync() + { + try + { + var token = _listenTriggerCts!.Token; + var reader = _triggerCommandChannel!.Reader; + await foreach (TriggerCommand cmd in reader.ReadAllAsync(token)) + { + if (!_isMacroRunning && cmd == TriggerCommand.Start) + { + _isMacroRunning = true; + _macroCts = new CancellationTokenSource(); + var localCts = _macroCts; + var macroToken = _macroCts.Token; + var triggerMode = SelectedTriggerMode; + _ = Task.Run(async () => + { + try + { + await Play(triggerMode, macroToken); + } + finally + { + _isMacroRunning = false; + localCts?.Dispose(); + } + }); + } + else if (_isMacroRunning && cmd == TriggerCommand.Stop) + { + // 仅 Cancel,不置 _isMacroRunning = false; + // 由 Play() 的 finally 负责重置,避免与新 Start 竞态 + _macroCts?.Cancel(); + } + else if (_isMacroRunning && cmd == TriggerCommand.Start) + { + LogEventHandler("宏:(" + Name + ")的动作序列正在执行中,请等待执行完成或停止后再尝试启动",OutputLevel.Warn); + } + } + } + catch (OperationCanceledException) + { + LogEventHandler("已禁用宏:(" + Name + ")",OutputLevel.Success); + } + catch + { + LogEventHandler("宏:(" + Name + ")监听触发器时发生异常", OutputLevel.Error); + } + } + + private async Task Play( TriggerMode triggerMode, CancellationToken cancellationToken) + { + string workingDirectory = AppDomain.CurrentDomain.BaseDirectory; + List allMacroActions = new List(); + for (int i = 0; i < PreMacroTrack.MacroActions.Count; i++) + allMacroActions.Add(PreMacroTrack.MacroActions[i]); + for (int i = 0; i < OnMacroTrack.MacroActions.Count; i++) + allMacroActions.Add(OnMacroTrack.MacroActions[i]); + for (int i = 0; i < PostMacroTrack.MacroActions.Count; i++) + allMacroActions.Add(PostMacroTrack.MacroActions[i]); + int resultCode = 0; + switch (triggerMode) + { + case TriggerMode.Once: + for (int i = 0; i < allMacroActions.Count; i++) + { + MacroAction macroAction = allMacroActions[i]; + var startTime = DateTime.Now; + resultCode = await macroAction.Run( + stdoutCallback: StdOutEventHandler, + stderrCallback: StdErrEventHandler, + workingDirectory: workingDirectory, + cancellationToken: cancellationToken); + var elapsedTime = DateTime.Now - startTime; + if (resultCode == 0) + LogEventHandler($"({macroAction.Name})已执行,耗时 {elapsedTime.TotalMilliseconds} ms", OutputLevel.Success); + else + break; + } + + break; + case TriggerMode.Hold: + case TriggerMode.Toggle: + bool shouldStop = false; + if (allMacroActions.Count == 0) + { + resultCode = -3; + shouldStop = true; + } + int iterCount = 0; + while (!shouldStop) + { + iterCount ++; + for (int i = 0; i < allMacroActions.Count; i++) + { + MacroAction macroAction = allMacroActions[i]; + var startTime = DateTime.Now; + resultCode = await macroAction.Run( + stdoutCallback: StdOutEventHandler, + stderrCallback: StdErrEventHandler, + workingDirectory: workingDirectory, + cancellationToken: cancellationToken); + var elapsedTime = DateTime.Now - startTime; + if (resultCode == 0) + LogEventHandler($"第{iterCount}次循环,({macroAction.Name})已执行,耗时 {elapsedTime.TotalMilliseconds} ms", OutputLevel.Success); + else + { + shouldStop = true; + break; + } + } + try + { + if (!shouldStop && !cancellationToken.IsCancellationRequested) + await Task.Delay(10, cancellationToken); + } + catch (OperationCanceledException) + { + shouldStop = true; + resultCode = -3; + } + + } + break; + default: + break; + } + switch (resultCode) + { + case 0: + LogEventHandler("宏:(" + Name + ")的动作序列已执行完成", OutputLevel.Success); + break; + case -1: + LogEventHandler("宏:(" + Name + ")的动作序列执行失败", OutputLevel.Error); + break; + case -2: + LogEventHandler("宏:(" + Name + ")的动作序列已取消", OutputLevel.Warn); + break; + case -3: + LogEventHandler("宏:(" + Name + ")的动作序列为空, 已退出", OutputLevel.Success); + break; + } + } + + public ICommand SaveCommand => new RelayCommand(Save); + public ICommand LoadCommand => new RelayCommand(Load); + + private void Save() + { + try + { + var dialog = new Microsoft.Win32.SaveFileDialog + { + Filter = "宏文件 (*.fipm)|*.fipm|所有文件 (*.*)|*.*", + DefaultExt = ".fipm", + FileName = $"{Name}.fipm", + }; + if (dialog.ShowDialog() != true) return; + + SaveToFile(dialog.FileName); + } + catch (Exception ex) + { + LogEventHandler($"保存宏失败: {ex.Message}", OutputLevel.Error); + } + } + + private void Load() + { + try + { + var dialog = new Microsoft.Win32.OpenFileDialog + { + Filter = "宏文件 (*.fipm)|*.fipm|所有文件 (*.*)|*.*", + DefaultExt = ".fipm", + }; + if (dialog.ShowDialog() != true) return; + + LoadFromFile(dialog.FileName); + } + catch (Exception ex) + { + LogEventHandler($"加载宏失败: {ex.Message}", OutputLevel.Error); + } + } + public void LoadFromFile(string filePath) + { + try + { + var json = System.IO.File.ReadAllText(filePath); + var dto = JsonSerializer.Deserialize(json); + if (dto == null) + { + LogEventHandler("加载失败: 文件内容为空或格式不正确", OutputLevel.Error); + return; + } + CopyFrom(dto); + LogEventHandler($"宏 \"{Name}\" 已从 {filePath} 加载", OutputLevel.Success); + } + catch (Exception ex) + { + LogEventHandler($"加载宏失败: {ex.Message}", OutputLevel.Error); + } + } + + public void SaveToFile(string filePath) + { + try + { + var dto = ToDto(); + var json = JsonSerializer.Serialize(dto, new JsonSerializerOptions { WriteIndented = true }); + System.IO.File.WriteAllText(filePath, json); + LogEventHandler($"宏 \"{Name}\" 已保存到: {filePath}", OutputLevel.Success); + } + catch (Exception ex) + { + LogEventHandler($"保存宏失败: {ex.Message}", OutputLevel.Error); + } + } + + /// + /// 从 DTO 恢复数据到当前实例(保留 InputParser、LogEvent 等运行时引用) + /// + public void CopyFrom(MacroViewModelDto dto) + { + Name = dto.Name; + Idx = dto.Idx; + Description = dto.Description; + TriggerConditionString = dto.TriggerConditionString; + + // 先设具体按键值,再设 SelectedTriggerDevice, + // 因为 SelectedTriggerDevice 的 setter 会读取 SelectedTriggerKey/SelectedMouseButton/SelectedGamepadButton + SelectedTriggerKey = dto.SelectedTriggerKey; + SelectedMouseButton = dto.SelectedMouseButton; + SelectedGamepadButton = dto.SelectedGamepadButton; + SelectedTriggerDevice = dto.SelectedTriggerDevice; + + SelectedTriggerTiming = dto.SelectedTriggerTiming; + SelectedTriggerMode = dto.SelectedTriggerMode; + ShortTimeThreshold = dto.ShortTimeThreshold; + EnableLog = dto.EnableLog; + + // 原地更新轨道数据(保留同一实例引用,不破坏 SelectedTrack 等 UI 绑定) + PreMacroTrack.CopyFrom(dto.PreMacroTrackDto); + OnMacroTrack.CopyFrom(dto.OnMacroTrackDto); + PostMacroTrack.CopyFrom(dto.PostMacroTrackDto); + } + + public MacroViewModelDto ToDto() + { + return new MacroViewModelDto + { + Name = Name, + Idx = Idx, + Description = Description, + TriggerConditionString = TriggerConditionString, + SelectedTriggerDevice = SelectedTriggerDevice, + SelectedTriggerKey = SelectedTriggerKey, + SelectedMouseButton = SelectedMouseButton, + SelectedGamepadButton = SelectedGamepadButton, + SelectedTriggerTiming = SelectedTriggerTiming, + SelectedTriggerMode = SelectedTriggerMode, + ShortTimeThreshold = ShortTimeThreshold, + EnableLog = EnableLog, + PreMacroTrackDto = PreMacroTrack.ToDto(), + OnMacroTrackDto = OnMacroTrack.ToDto(), + PostMacroTrackDto = PostMacroTrack.ToDto(), + }; + } + + public static MacroViewModel FromDto(MacroViewModelDto dto) + { + var vm = new MacroViewModel + { + Name = dto.Name, + Idx = dto.Idx, + Description = dto.Description, + TriggerConditionString = dto.TriggerConditionString, + // 先设具体按键值,再设 SelectedTriggerDevice(setter 会读取它们) + SelectedTriggerKey = dto.SelectedTriggerKey, + SelectedMouseButton = dto.SelectedMouseButton, + SelectedGamepadButton = dto.SelectedGamepadButton, + SelectedTriggerDevice = dto.SelectedTriggerDevice, + SelectedTriggerTiming = dto.SelectedTriggerTiming, + SelectedTriggerMode = dto.SelectedTriggerMode, + ShortTimeThreshold = dto.ShortTimeThreshold, + EnableLog = dto.EnableLog, + PreMacroTrack = MacroTrackViewModel.FromDto(dto.PreMacroTrackDto), + OnMacroTrack = MacroTrackViewModel.FromDto(dto.OnMacroTrackDto), + PostMacroTrack = MacroTrackViewModel.FromDto(dto.PostMacroTrackDto), + }; + return vm; + } + } + + public class MacroViewModelDto + { + public string Name { get; set; } = "宏"; + public int Idx { get; set; } + public string Description { get; set; } = string.Empty; + public string TriggerConditionString { get; set; } = string.Empty; + public InputDevice SelectedTriggerDevice { get; set; } = InputDevice.Keyboard; + public FipRawKeys SelectedTriggerKey { get; set; } = FipRawKeys.None; + public System.Windows.Forms.MouseButtons SelectedMouseButton { get; set; } + public FIPGamepadButtonflags SelectedGamepadButton { get; set; } = FIPGamepadButtonflags.A; + public TriggerTiming SelectedTriggerTiming { get; set; } = TriggerTiming.Press; + public TriggerMode SelectedTriggerMode { get; set; } = TriggerMode.Once; + public int ShortTimeThreshold { get; set; } = 200; + public bool EnableLog { get; set; } = true; + public MacroTrackViewModelDto PreMacroTrackDto { get; set; } = new(); + public MacroTrackViewModelDto OnMacroTrackDto { get; set; } = new(); + public MacroTrackViewModelDto PostMacroTrackDto { get; set; } = new(); + } +} diff --git a/FancyInput/ViewModels/MacroWindowViewModel.cs b/FancyInput/ViewModels/MacroWindowViewModel.cs new file mode 100644 index 0000000..e55c26b --- /dev/null +++ b/FancyInput/ViewModels/MacroWindowViewModel.cs @@ -0,0 +1,144 @@ +using FancyInput.Models; +using FancyInput.Views.Controls; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Input; +using InputDevice = FancyInput.Models.InputDevice; +namespace FancyInput.ViewModels +{ + + public class MacroWindowViewModel : ViewModelBase,IDisposable + { + public event Action? LogEvent; + private void StdOutHandler(MacroViewModel? macro, string msg, OutputLevel level) => LogEvent?.Invoke(macro, msg, level); + public MacroWindowViewModel() { } + public InputParser? InputParser { get; set; } = null; + + private MacroViewModel? _selectedMacro; + public MacroViewModel? SelectedMacro + { + get => _selectedMacro; + set + { + if (_selectedMacro != null) + _selectedMacro.IsSelected = false; + if (value != null) + value.IsSelected = true; + SetProperty(ref _selectedMacro, value); + OnPropertyChanged(nameof(MacroVisibility)); + } + } + + + + public Visibility MacroVisibility => SelectedMacro != null ? Visibility.Visible : Visibility.Collapsed; + + public ObservableCollection Macros { get; set; } = new ObservableCollection(); + + private double _actionsWidth = 600; + public double ActionsWidth + { + get => _actionsWidth; + set => SetProperty(ref _actionsWidth, value); + } + + public ICommand AddMacroCommand => new RelayCommand(AddMacro); + private void AddMacro() + { + MacroViewModel macroViewModel = new MacroViewModel(); + macroViewModel.Name = $"宏{Macros.Count + 1}"; + macroViewModel.Idx = Macros.Count; + macroViewModel.InputParser = InputParser; + macroViewModel.LogEvent += StdOutHandler; + Macros.Add(macroViewModel); + SelectedMacro = macroViewModel; + } + + public ICommand LoadMacroFromFolderCommand => new RelayCommand(LoadMacroFromFolder); + private void LoadMacroFromFolder() + { + try + { + using (var dialog = new FolderBrowserDialog()) + { + dialog.Description = "选择宏文件夹"; + if (dialog.ShowDialog() == DialogResult.OK) + { + string folderPath = dialog.SelectedPath; + LoadMacroFromFolder(folderPath); + } + } + } + catch (Exception ex) + { + StdOutHandler(null, $"加载宏文件夹时发生错误: {ex.Message}", OutputLevel.Error); + } + } + public void LoadMacroFromFolder(string folderPath) + { + try + { + var macroFiles = System.IO.Directory.GetFiles(folderPath, "*.fipm"); + foreach (var file in macroFiles) + { + MacroViewModel macroViewModel = new MacroViewModel(); + Macros.Add(macroViewModel); + macroViewModel.LogEvent += StdOutHandler; + macroViewModel.InputParser = InputParser; + macroViewModel.LoadFromFile(file); + StdOutHandler(macroViewModel, $"已加载宏 {macroViewModel.Name} 从 {file}", OutputLevel.Info); + } + if (macroFiles.Length == 0) + { + StdOutHandler(null, "没有找到宏文件", OutputLevel.Warn); + } + var lastMacro = Macros.LastOrDefault(); + SelectedMacro = lastMacro; + } + catch (Exception ex) + { + StdOutHandler(null, $"加载宏文件夹时发生错误: {ex.Message}", OutputLevel.Error); + } + } + + public ICommand SaveAllCommand => new RelayCommand(SaveAll); + private void SaveAll() + { + using (var dialog = new FolderBrowserDialog()) + { + dialog.Description = "选择保存宏的文件夹"; + if (dialog.ShowDialog() == DialogResult.OK) + { + string folderPath = dialog.SelectedPath; + foreach (var macro in Macros) + { + string filePath = System.IO.Path.Combine(folderPath, $"{macro.Name}.fipm"); + macro.SaveToFile(filePath); + StdOutHandler(macro, $"已保存宏 {macro.Name} 到 {filePath}", OutputLevel.Info); + } + if (Macros.Count == 0) + { + StdOutHandler(null, "没有宏可保存", OutputLevel.Warn); + } + } + } + } + + public void Dispose() + { + foreach (var item in Macros) + { + item.Dispose(); + } + Macros.Clear(); + SelectedMacro = null; + InputParser = null; + } + } +} diff --git a/FancyInput/ViewModels/MainWindowViewModel.cs b/FancyInput/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..7b59933 --- /dev/null +++ b/FancyInput/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,658 @@ +using FancyInput.Models; +using FancyInput.Views; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Security.Principal; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; + +using SaveFileDialog = Microsoft.Win32.SaveFileDialog; +using OpenFileDialog = Microsoft.Win32.OpenFileDialog; +using System.Text.Json.Serialization; +using System.Reflection.Metadata; +namespace FancyInput.ViewModels +{ + public enum LoadMode + { + PngJson, + ProjectFile + } + + public class MainWindowViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + if (OwnerWindow.IsConfigLoaded) + SaveConfig(); + } + protected virtual bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") + { + if (Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + //PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return true; + } + + public MainWindowViewModel(FancyInputMainWindow window) + { + _ownerWindow = window; + _inputParser = new InputParser(); + MacroWindowViewModel.InputParser = _inputParser; + } + + private Brush _foregroundColor = Brushes.Black; + public Brush ForegroundColor + { + get => _foregroundColor; + set => SetProperty(ref _foregroundColor, value); + } + private Brush _backgroundColor = Brushes.AliceBlue; + public Brush BackgroundColor + { + get => _backgroundColor; + set => SetProperty(ref _backgroundColor, value); + } + private Brush _borderColor = Brushes.Gray; + public Brush BorderColor + { + get => _borderColor; + set => SetProperty(ref _borderColor, value); + } + private Brush _accentColor = new SolidColorBrush(Color.FromArgb(255, 0x65, 0x1F, 0x65)); + public Brush AccentColor + { + get => _accentColor; + set => SetProperty(ref _accentColor, value); + } + private Brush _foreGroundAccentColor = Brushes.White; + public Brush ForeGroundAccentColor + { + get => _foreGroundAccentColor; + set => SetProperty(ref _foreGroundAccentColor, value); + } + private string _macroSavePath = string.Empty; + public string MacroSavePath + { + get => _macroSavePath; + set => SetProperty(ref _macroSavePath, value); + } + private MacroWindowViewModel _macroWindowViewModel = new MacroWindowViewModel(); + public MacroWindowViewModel MacroWindowViewModel + { + get => _macroWindowViewModel; + set => SetProperty(ref _macroWindowViewModel, value); + } + + private InputParser _inputParser; + public InputParser InputParser => _inputParser; + + ObservableCollection _overlayWindowViewModels = new(); + public ObservableCollection OverlayWindowViewModels + { + get { return _overlayWindowViewModels; } + set { SetProperty(ref _overlayWindowViewModels, value); } + } + + private LoadMode _loadMode = LoadMode.PngJson; + public LoadMode LoadMode + { + get => _loadMode; + set => SetProperty(ref _loadMode, value); + } + + private MainWindowPanelType _panelType = MainWindowPanelType.Read; + public static Array PanelTypeList => Enum.GetValues(typeof(MainWindowPanelType)); + public MainWindowPanelType PanelType + { + get => _panelType; + set + { + SetProperty(ref _panelType, value); + + OnPropertyChanged(nameof(SelectedPanelIndex)); + OnPropertyChanged(nameof(ReadButtonStyle)); + OnPropertyChanged(nameof(ManageButtonStyle)); + OnPropertyChanged(nameof(CreateButtonStyle)); + OnPropertyChanged(nameof(SettingsButtonStyle)); + OnPropertyChanged(nameof(WorkshopButtonStyle)); + } + } + public int SelectedPanelIndex + { + get => (int)PanelType; + set + { + if (value >= 0 && value <= PanelTypeList.Length - 1) + { + PanelType = (MainWindowPanelType)value; + } + } + } + public Style ReadButtonStyle => PanelType == MainWindowPanelType.Read ? (Style)OwnerWindow.FindResource("FancySelectedButton") : (Style)OwnerWindow.FindResource("FancyButton"); + public Style ManageButtonStyle => PanelType == MainWindowPanelType.Manage ? (Style)OwnerWindow.FindResource("FancySelectedButton") : (Style)OwnerWindow.FindResource("FancyButton"); + public Style CreateButtonStyle => PanelType == MainWindowPanelType.Create ? (Style)OwnerWindow.FindResource("FancySelectedButton") : (Style)OwnerWindow.FindResource("FancyButton"); + public Style SettingsButtonStyle => PanelType == MainWindowPanelType.Settings ? (Style)OwnerWindow.FindResource("FancySelectedButton") : (Style)OwnerWindow.FindResource("FancyButton"); + public Style WorkshopButtonStyle => PanelType == MainWindowPanelType.Workshop ? (Style)OwnerWindow.FindResource("FancySelectedButton") : (Style)OwnerWindow.FindResource("FancyButton"); + + private string _pngFilePath = string.Empty; + public string PngFilePath + { + get => _pngFilePath; + set => SetProperty(ref _pngFilePath, value); + } + private string _jsonFilePath = string.Empty; + public string JsonFilePath + { + get => _jsonFilePath; + set => SetProperty(ref _jsonFilePath, value); + } + private string _projectFilePath = string.Empty; + public string ProjectFilePath + { + get => _projectFilePath; + set => SetProperty(ref _projectFilePath, value); + } + + //已加载信息 + public List LoadedSingles { get; set; } = new List(); + + //Info + private SolidColorBrush _debugCircleFill = new SolidColorBrush(Colors.Green); + public SolidColorBrush DebugCircleFill + { + get => _debugCircleFill; + set => SetProperty(ref _debugCircleFill, value); + } + private String _debugLabelString = "已就绪"; + public string DebugLabelString + { + get => _debugLabelString; + set => SetProperty(ref _debugLabelString, value); + } + + //选项 + public bool UseCenterMouseMove + { + get => InputHandler.USE_CENTER_MOUSEMOVE; + set + { + InputHandler.USE_CENTER_MOUSEMOVE = value; + OnPropertyChanged(nameof(UseCenterMouseMove)); + } + } + + public bool PreventMouseCentering + { + get => InputParser.PreventMouseCentering; + set + { + InputParser.PreventMouseCentering = value; + OnPropertyChanged(nameof(PreventMouseCentering)); + } + } + + public int MouseMoveSensitivity + { + get => InputHandler.MOUSE_SENSE; + set + { + InputHandler.MOUSE_SENSE = value; + OnPropertyChanged(nameof(MouseMoveSensitivity)); + } + } + + public double TimerTickInterval + { + get => InputParser.TimerInterval; + set + { + InputParser.TimerInterval = value; + OnPropertyChanged(nameof(TimerTickInterval)); + OnPropertyChanged(nameof(TimerTickIntervalString)); + } + } + public string TimerTickIntervalString => $"{TimerTickInterval:F1}"; + + private bool _detectInput = false; + public bool DetectInput + { + get => _detectInput; + set => SetProperty(ref _detectInput, value); + } + private string _inputDetectString = ""; + public string InputDetectString + { + get => _inputDetectString; + set => SetProperty(ref _inputDetectString, value); + } + + public string AdminRestartArg => "--restart-as-admin"; + public string AdminRestartArgs => LoadMode == LoadMode.ProjectFile && ProjectFilePath != string.Empty ? + $"{AdminRestartArg} {ProjectFilePath}" : AdminRestartArg; + private bool _runAsAdmin = false; + public bool RunAsAdmin + { + get => _runAsAdmin; + set + { + if (_runAsAdmin != value) + { + _runAsAdmin = value; + OnPropertyChanged(nameof(RunAsAdmin)); + //FancyInput.AppMessageBox.Show($"{value} {IsRunAsAdmin()} {OwnerWindow.IsConfigLoaded}"); + if (value && !IsRunAsAdmin() && OwnerWindow.IsConfigLoaded) + { + var result = FancyInput.AppMessageBox.Show("是否以管理员身份重新启动?", "需要管理员权限", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (result == MessageBoxResult.Yes) + { + string arguments = AdminRestartArg; + var processInfo = new ProcessStartInfo + { + FileName = Process.GetCurrentProcess().MainModule!.FileName, + UseShellExecute = true, + Arguments = arguments + }; + Process.Start(processInfo); + } + else + { + _runAsAdmin = false; + OnPropertyChanged(nameof(RunAsAdmin)); + } + } + } + } + } + + private bool _minimizeToTray = false; + public bool MinimizeToTray + { + get => _minimizeToTray; + set => SetProperty(ref _minimizeToTray, value); + } + private Visibility _trayIconVisibility = Visibility.Visible; + public Visibility TrayIconVisibility + { + get => _trayIconVisibility; + set => SetProperty(ref _trayIconVisibility, value); + } + + public Array KeyBoardMappingList => Enum.GetValues(typeof(KeyBoardMappingType)); + private KeyBoardMappingType _selectedKeyBoardMapping = KeyBoardMappingType.Windows; + public KeyBoardMappingType SelectedKeyBoardMapping + { + get => _selectedKeyBoardMapping; + set + { + SetProperty(ref _selectedKeyBoardMapping, value); + InputParser.KeyBoardMappingType = value; + OnPropertyChanged(nameof(UseNumpadAsArrowVisibility)); + } + } + public Visibility UseNumpadAsArrowVisibility => SelectedKeyBoardMapping == KeyBoardMappingType.Windows ? Visibility.Visible : Visibility.Collapsed; + private bool _useNumpadAsArrow = false; + public bool UseNumpadAsArrow + { + get => _useNumpadAsArrow; + set + { + SetProperty(ref _useNumpadAsArrow, value); + InputParser.NumPadAsArrow = value; + } + } + + // GamepadBackEnd + public Array GamepadBackEndList => Enum.GetValues(typeof(GamepadBackend)); + private GamepadBackend _selectedGamepadBackEnd = GamepadBackend.SDL; + public GamepadBackend SelectedGamepadBackEnd + { + get => _selectedGamepadBackEnd; + set + { + SetProperty(ref _selectedGamepadBackEnd, value); + InputParser.SetGamepadBackend(SelectedGamepadBackEnd); + } + } + + //更新 + private string _currentVersion = string.Empty; + public string CurrentVersion + { + get => _currentVersion; + set => SetProperty(ref _currentVersion, value); + } + + private string[] _launchUpdateIgnoreVersions = new string[] { }; + public string[] LaunchUpdateIgnoreVersions + { + get => _launchUpdateIgnoreVersions; + set => SetProperty(ref _launchUpdateIgnoreVersions, value); + } + + + private FancyInputMainWindow _ownerWindow; + public FancyInputMainWindow OwnerWindow + { + get => _ownerWindow; + set => SetProperty(ref _ownerWindow, value); + } + + + + public static bool IsRunAsAdmin() + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + + public void UpMoveAt(int idx) + { + if (idx > 0 && idx < OverlayWindowViewModels.Count) + { + var temp = OverlayWindowViewModels[idx - 1]; + OverlayWindowViewModels[idx - 1] = OverlayWindowViewModels[idx]; + OverlayWindowViewModels[idx] = temp; + RefreshOverlayIndices(); + } + RefreshTopMost(); + } + public void DownMoveAt(int idx) + { + if (idx >= 0 && idx < OverlayWindowViewModels.Count - 1) + { + var temp = OverlayWindowViewModels[idx + 1]; + OverlayWindowViewModels[idx + 1] = OverlayWindowViewModels[idx]; + OverlayWindowViewModels[idx] = temp; + RefreshOverlayIndices(); + } + RefreshTopMost(); + } + + public void RefreshOverlayIndices() + { + int gamepadIdx = 0; + for (int i = 0; i < OverlayWindowViewModels.Count; i++) + { + OverlayWindowViewModels[i].Idx = i; + if (OverlayWindowViewModels[i].ElementTreeViewModel.IsGamepad) + { + if (gamepadIdx < Enum.GetValues(typeof(GamepadUserIdx)).Length) + { + OverlayWindowViewModels[i].ElementTreeViewModel.GamepadUserIndex = (GamepadUserIdx)gamepadIdx; + gamepadIdx++; + } + else + { + OverlayWindowViewModels[i].ElementTreeViewModel.GamepadUserIndex = GamepadUserIdx.Four; + } + } + } + } + + public void RefreshTopMost() + { + foreach (var vm in OverlayWindowViewModels) + { + bool shouldBeTopMost = vm.IsTopMost; + vm.IsTopMost = !shouldBeTopMost; + vm.IsTopMost = shouldBeTopMost; + } + } + + private static string GetConfigPath() + { + + string appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string configDir = Path.Combine(appDataDir, "FancyInput"); + if (!Directory.Exists(configDir)) + Directory.CreateDirectory(configDir); + return Path.Combine(configDir, "config.json"); + } + + public void InitAdminState() + { + if (IsRunAsAdmin()) + { + _runAsAdmin = true; + OnPropertyChanged(nameof(RunAsAdmin)); + } + else + { + _runAsAdmin = false; + OnPropertyChanged(nameof(RunAsAdmin)); + } + } + + public void SaveConfig() + { + var config = new FancyInputConfig + { + PngFilePath = this.PngFilePath, + JsonFilePath = this.JsonFilePath, + ProjectFilePath = this.ProjectFilePath, + LoadMode = this.LoadMode, + DetectInput = this.DetectInput, + UseCenterMouseMove = this.UseCenterMouseMove, + PreventMouseCentering = this.PreventMouseCentering, + MouseMoveSensitivity = this.MouseMoveSensitivity, + TimerTickInterval = this.TimerTickInterval, + CurrentVersion = this.CurrentVersion, + LaunchUpdateIgnoreVersions = this.LaunchUpdateIgnoreVersions, + RunAsAdmin = this.RunAsAdmin, + MinimizeToTray = this.MinimizeToTray, + KeyBoardMappingType = this.SelectedKeyBoardMapping, + UseNumPadAsArrow = this.UseNumpadAsArrow, + GamepadBackEnd = this.SelectedGamepadBackEnd, + //Workshop相关配置 + MacroSavePath = this.MacroSavePath, + }; + string json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(GetConfigPath(), json); + } + + public void LoadConfig() + { + string path = GetConfigPath(); + if (File.Exists(path)) + { + try + { + var config = JsonSerializer.Deserialize(File.ReadAllText(path)); + if (config != null) + { + this.PngFilePath = config.PngFilePath; + this.JsonFilePath = config.JsonFilePath; + this.ProjectFilePath = config.ProjectFilePath; + this.LoadMode = config.LoadMode; + this.DetectInput = config.DetectInput; + this.UseCenterMouseMove = config.UseCenterMouseMove; + this.PreventMouseCentering = config.PreventMouseCentering; + this.MouseMoveSensitivity = config.MouseMoveSensitivity; + this.TimerTickInterval = config.TimerTickInterval; + this.CurrentVersion = config.CurrentVersion; + this.LaunchUpdateIgnoreVersions = config.LaunchUpdateIgnoreVersions; + this.RunAsAdmin = config.RunAsAdmin; + this.MinimizeToTray = config.MinimizeToTray; + this.SelectedKeyBoardMapping = config.KeyBoardMappingType; + this.UseNumpadAsArrow = config.UseNumPadAsArrow; + this.SelectedGamepadBackEnd = config.GamepadBackEnd; + //Workshop相关配置 + this.MacroSavePath = config.MacroSavePath; + } + } + catch { /* 可加日志或提示 */ } + } + + } + + public void SaveLoadedGroup() + { + try + { + if (OverlayWindowViewModels.Count==0) + throw new Exception("加载至少一个叠加窗口配置,才能保存叠加组文件。"); + LoadedSingles.Clear(); + SaveFileDialog saveFileDialog = new SaveFileDialog(); + saveFileDialog.Filter = "FancyInput Group Files (*.fips)|*.fips"; + if (saveFileDialog.ShowDialog() == true) + { + string savePath = saveFileDialog.FileName; + bool saveRawData = FancyInput.AppMessageBox.Show( + "是否将原始数据一并保存到叠加组文件中?", + "保存选项", + MessageBoxButton.YesNo, + MessageBoxImage.Question + ) == MessageBoxResult.Yes; + for (int i = 0; i < OverlayWindowViewModels.Count; i++) + { + LoadedSingleInfo singleInfo = new LoadedSingleInfo() + { + LoadMode = OverlayWindowViewModels[i].ElementTreeViewModel.LoadMode, + PngFilePath = OverlayWindowViewModels[i].ElementTreeViewModel.PngFilePath, + JsonFilePath = OverlayWindowViewModels[i].ElementTreeViewModel.JsonFilePath, + ProjectFilePath = OverlayWindowViewModels[i].ElementTreeViewModel.ProjectFilePath, + InitialWidth = OverlayWindowViewModels[i].InitialWidth, + InitialHeight = OverlayWindowViewModels[i].InitialHeight, + IsLocationFixed = OverlayWindowViewModels[i].IsLocationFixed, + IsTopMost = OverlayWindowViewModels[i].IsTopMost, + IsHide = OverlayWindowViewModels[i].IsHide, + IsRemoveYellowBorder = OverlayWindowViewModels[i].IsRemoveYellowBorder, + Scale = OverlayWindowViewModels[i].Scale, + Left = OverlayWindowViewModels[i].Left, + Top = OverlayWindowViewModels[i].Top, + ElementTreeViewModelDto = saveRawData ? ElementTreeViewModel.ToDto(OverlayWindowViewModels[i].ElementTreeViewModel) : null + }; + LoadedSingles.Add(singleInfo); + } + var options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = !saveRawData + }; + string json = JsonSerializer.Serialize(LoadedSingles, options); + File.WriteAllText(savePath, json); + FancyInput.AppMessageBox.Show("保存成功!", "成功", MessageBoxButton.OK, MessageBoxImage.Information); + } + } + catch (Exception ex) + { + FancyInput.AppMessageBox.Show($"保存失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + public void AddLoadedGroup() + { + OpenFileDialog openFileDialog = new OpenFileDialog(); + openFileDialog.Filter = "FancyInput Group Files (*.fips)|*.fips"; + if (openFileDialog.ShowDialog() == true) + { + string openPath = openFileDialog.FileName; + OpenFips(openPath); + } + } + + public void OpenFips(string fipsFilePath) + { + try + { + //var bytes = File.ReadAllBytes(openPath); + //var loadedSingles = System.Text.Json.JsonSerializer.Deserialize>(bytes); + var json = File.ReadAllText(fipsFilePath); + var loadedSingles = System.Text.Json.JsonSerializer.Deserialize>(json); + if (loadedSingles != null) + { + foreach (var single in loadedSingles) + { + OverlayWindowViewModel? vm; + if (single.ElementTreeViewModelDto == null) + { + LoadMode = single.LoadMode; + PngFilePath = single.PngFilePath; + JsonFilePath = single.JsonFilePath; + ProjectFilePath = single.ProjectFilePath; + vm = OwnerWindow.LoadConfig(); + } + else + { + ElementTreeViewModel elementTreeViewModel = ElementTreeViewModel.FromDto(single.ElementTreeViewModelDto); + vm = OwnerWindow.LoadElementTreeViewModel(elementTreeViewModel); + elementTreeViewModel.LoadMode = single.LoadMode; + elementTreeViewModel.PngFilePath = single.PngFilePath; + elementTreeViewModel.JsonFilePath = single.JsonFilePath; + elementTreeViewModel.ProjectFilePath = single.ProjectFilePath; + } + if (vm == null) break; + vm.IsLocationFixed = single.IsLocationFixed; + vm.IsTopMost = single.IsTopMost; + vm.IsHide = single.IsHide; + vm.IsRemoveYellowBorder = single.IsRemoveYellowBorder; + vm.InitialWidth = single.InitialWidth; + vm.InitialHeight = single.InitialHeight; + vm.Scale = single.Scale; + vm.Left = single.Left; + vm.Top = single.Top; + } + RefreshOverlayIndices(); + } + } + catch + { + FancyInput.AppMessageBox.Show("加载失败,文件可能已损坏或格式不正确。", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } + + public class FancyInputConfig + { + public string PngFilePath { get; set; } = string.Empty; + public string JsonFilePath { get; set; } = string.Empty; + public string ProjectFilePath { get; set; } = string.Empty; + public LoadMode LoadMode { get; set; } = LoadMode.PngJson; + public bool DetectInput { get; set; } = false; + public bool UseCenterMouseMove { get; set; } = false; + public bool PreventMouseCentering { get; set; } = false; + public int MouseMoveSensitivity { get; set; } = 10; + public double TimerTickInterval { get; set; } = 16.67; + public string CurrentVersion { get; set; } = string.Empty; + public string[] LaunchUpdateIgnoreVersions { get; set; } = new string[] { }; + public bool RunAsAdmin { get; set; } = false; + public bool MinimizeToTray { get; set; } = false; + public KeyBoardMappingType KeyBoardMappingType { get; set; } = KeyBoardMappingType.Windows; + public bool UseNumPadAsArrow { get; set; } = false; + public GamepadBackend GamepadBackEnd { get; set; } = GamepadBackend.SDL; + //Workshop相关配置 + public string MacroSavePath { get; set; } = string.Empty; + } + + public class LoadedSingleInfo + { + public LoadMode LoadMode { get; set; } = LoadMode.PngJson; + public string PngFilePath { get; set; } = string.Empty; + public string JsonFilePath { get; set; } = string.Empty; + public string ProjectFilePath { get; set; } = string.Empty; + public double InitialWidth { get; set; } = 800; + public double InitialHeight { get; set; } = 800; + public double Scale { get; set; } = 0; + public bool IsLocationFixed { get; set; } = false; + public bool IsTopMost { get; set; } = true; + public bool IsHide { get; set; } = false; + public bool IsRemoveYellowBorder { get; set; } = false; + public double Left { get; set; } = 0; + public double Top { get; set; } = 0; + public ElementTreeViewModelDto? ElementTreeViewModelDto { get; set; } = null; + } + +} \ No newline at end of file diff --git a/FancyInput/ViewModels/OverlayWindowViewModel.cs b/FancyInput/ViewModels/OverlayWindowViewModel.cs new file mode 100644 index 0000000..f6ed717 --- /dev/null +++ b/FancyInput/ViewModels/OverlayWindowViewModel.cs @@ -0,0 +1,295 @@ +using FancyInput.Views; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Threading; +using Brush = System.Windows.Media.Brush; +using Color = System.Windows.Media.Color; + +namespace FancyInput.ViewModels +{ + public class OverlayWindowViewModel:INotifyPropertyChanged + { + private const double GeometryEpsilon = 0.5; + private bool _lastRemoveYellowBorderMode = false; + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + protected virtual bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") + { + if (Equals(field, value)) return false; + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return true; + } + + public const double DEFAULT_MARGIN = 10; + + private bool _isRemoveYellowBorder = false; + public bool IsRemoveYellowBorder + { + get => _isRemoveYellowBorder; + set + { + SetProperty(ref _isRemoveYellowBorder, value); + UpdateWindowSize(); + } + } + + private void UpdateWindowSize() + { + bool modeChanged = _lastRemoveYellowBorderMode != _isRemoveYellowBorder; + + double targetMargin; + double targetLeft; + double targetTop; + double targetWidth; + double targetHeight; + + if (_isRemoveYellowBorder) + { + targetMargin = DEFAULT_MARGIN; + targetLeft = -targetMargin; + targetTop = -targetMargin; + targetWidth = _windowWidth + targetMargin * 2; + targetHeight = _windowHeight + targetMargin * 2; + } + else + { + targetMargin = 0; + targetLeft = Left; + targetTop = Top; + targetWidth = CurrentWidth; + targetHeight = CurrentHeight; + } + + _margin = targetMargin; + OnPropertyChanged(nameof(Margin)); + OnPropertyChanged(nameof(CurrentHeight)); + OnPropertyChanged(nameof(CurrentWidth)); + OnPropertyChanged(nameof(ConditionalLeft)); + OnPropertyChanged(nameof(ConditionalTop)); + + if (modeChanged) + { + OverlayWindow.Opacity = 0; + } + + if (Math.Abs(OverlayWindow.Width - targetWidth) > GeometryEpsilon) + { + OverlayWindow.Width = targetWidth; + } + if (Math.Abs(OverlayWindow.Height - targetHeight) > GeometryEpsilon) + { + OverlayWindow.Height = targetHeight; + } + if (Math.Abs(OverlayWindow.Left - targetLeft) > GeometryEpsilon) + { + OverlayWindow.Left = targetLeft; + } + if (Math.Abs(OverlayWindow.Top - targetTop) > GeometryEpsilon) + { + OverlayWindow.Top = targetTop; + } + + if (modeChanged) + { + OverlayWindow.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => + { + OverlayWindow.Opacity = 1; + })); + } + + _lastRemoveYellowBorderMode = _isRemoveYellowBorder; + + } + + + private double _windowWidth = 300; + public double WindowWidth + { + get => _windowWidth; + set + { + SetProperty(ref _windowWidth, value); + UpdateWindowSize(); + } + } + private double _windowHeight = 300; + public double WindowHeight + { + get => _windowHeight; + set + { + SetProperty(ref _windowHeight, value); + UpdateWindowSize(); + } + } + + + private double _margin = DEFAULT_MARGIN; + public double Margin + { + get => _margin; + set + { + SetProperty(ref _margin, value); + UpdateWindowSize(); + } + } + + + private int _idx = 0; + public int Idx + { + get => _idx; + set => SetProperty(ref _idx, value); + } + + private bool _isClosed = false; + public bool IsClosed + { + get => _isClosed; + set => SetProperty(ref _isClosed, value); + } + + private bool _isLocationFixed = false; + public bool IsLocationFixed + { + get => _isLocationFixed; + set + { + SetProperty(ref _isLocationFixed, value); + OnPropertyChanged(nameof(IsHitTestVisible)); + OnPropertyChanged(nameof(CanvasBackground)); + } + } + + public bool IsHitTestVisible => !IsLocationFixed; + public Brush CanvasBackground => IsLocationFixed ? new SolidColorBrush(Colors.Transparent) : new SolidColorBrush(Color.FromArgb(1,0, 0, 0)); + + private bool _isTopMost = true; + public bool IsTopMost + { + get => _isTopMost; + set => SetProperty(ref _isTopMost, value); + } + + private bool _isHide = false; + public bool IsHide + { + get => _isHide; + set + { + SetProperty(ref _isHide, value); + OnPropertyChanged(nameof(CurrentVisibility)); + } + } + + + //public WindowState CurrentWindowState => IsHide ? WindowState.Minimized : WindowState.Normal; + public Visibility CurrentVisibility => IsHide ? Visibility.Collapsed : Visibility.Visible; + + private double _scale = 0; + public double Scale + { + get => _scale; + set + { + SetProperty(ref _scale, value); + OnPropertyChanged(nameof(CurrentHeight)); + OnPropertyChanged(nameof(CurrentWidth)); + if (!_isRemoveYellowBorder) + { + OverlayWindow.Width = CurrentWidth; + OverlayWindow.Height = CurrentHeight; + } + } + } + + public double ConditionalLeft => IsRemoveYellowBorder ? Left : 0; + public double ConditionalTop => IsRemoveYellowBorder ? Top : 0; + private double _left = 0; + public double Left + { + get => _left; + set + { + SetProperty(ref _left, value); + OnPropertyChanged(nameof(ConditionalLeft)); + if (!_isRemoveYellowBorder) + { + OverlayWindow.Left = value; + } + } + } + private double _top = 0; + public double Top + { + get => _top; + set + { + SetProperty(ref _top, value); + OnPropertyChanged(nameof(ConditionalTop)); + if (!_isRemoveYellowBorder) + { + OverlayWindow.Top = value; + } + } + } + + private double _initialHeight =100; + private double _initialWidth = 100; + public double InitialHeight + { + get => _initialHeight; + set + { + SetProperty(ref _initialHeight, value); + OnPropertyChanged(nameof(CurrentHeight)); + } + } + public double InitialWidth + { + get => _initialWidth; + set + { + SetProperty(ref _initialWidth, value); + OnPropertyChanged(nameof(CurrentWidth)); + } + } + + public double CurrentHeight => InitialHeight * Math.Pow(2, Scale); + public double CurrentWidth => InitialWidth * Math.Pow(2, Scale); + + private ElementTreeViewModel _elementTreeViewModel; + public ElementTreeViewModel ElementTreeViewModel + { + get => _elementTreeViewModel; + set => SetProperty(ref _elementTreeViewModel, value); + } + + private OverlayWindow _overlayWindow; + public OverlayWindow OverlayWindow => _overlayWindow; + + public OverlayWindowViewModel(OverlayWindow overlayWindow, ElementTreeViewModel elementTreeViewModel) + { + _overlayWindow = overlayWindow; + _elementTreeViewModel = elementTreeViewModel; + } + + + } +} diff --git a/FancyInput/ViewModels/TerminalViewModel.cs b/FancyInput/ViewModels/TerminalViewModel.cs new file mode 100644 index 0000000..26dbb57 --- /dev/null +++ b/FancyInput/ViewModels/TerminalViewModel.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using System.IO; +using System.Windows.Media; + +using FancyInput.Models; +namespace FancyInput.ViewModels +{ + public class TerminalViewModel : ViewModelBase + { + private int _fontSize = 15; + public int FontSize + { + get => _fontSize; + set + { + int newValue = Math.Min(Math.Max(value, 10), 30); + SetProperty(ref _fontSize, newValue); + } + } + private SolidColorBrush _backgroundColor = new SolidColorBrush(Colors.Black); + public SolidColorBrush BackgroundColor + { + get => _backgroundColor; + set => SetProperty(ref _backgroundColor, value); + } + private SolidColorBrush _foregroundColor = new SolidColorBrush(Colors.White); + public SolidColorBrush ForegroundColor + { + get => _foregroundColor; + set => SetProperty(ref _foregroundColor, value); + } + public CancellationTokenSource CancellationTokenSource { get; set; } = new CancellationTokenSource(); + + public ObservableCollection Commands { get; set; } = new ObservableCollection(); + private TerminalCommandViewModel? _currentCommandViewModel; + public TerminalCommandViewModel? CurrentCommandViewModel + { + get => _currentCommandViewModel; + set => SetProperty(ref _currentCommandViewModel, value); + } + public Array BackEndList => Enum.GetValues(typeof(CommandBackEnd)); + private CommandBackEnd _backEnd = CommandBackEnd.Cmd; + public CommandBackEnd BackEnd + { + get => _backEnd; + set + { + if (CurrentCommandViewModel != null) + CurrentCommandViewModel.BackEnd = value; + SetProperty(ref _backEnd, value); + } + } + public TerminalViewModel() + { + InitializeNewCommand(); + ClearCommand = new RelayCommand(() => + { + if (_currentCommandViewModel != null) + _currentCommandViewModel.CancelCommand(); + Commands.Clear(); + InitializeNewCommand(); + }); + } + private void InitializeNewCommand() + { + TerminalCommandViewModel firstCmdViewModel = new TerminalCommandViewModel(); + firstCmdViewModel.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + firstCmdViewModel.BackEnd = BackEnd; + firstCmdViewModel.CommandExecuted += AddNewCommand; + firstCmdViewModel.StdoutUpdated += StdOutErrUpdated; + firstCmdViewModel.StdErrUpdated += StdOutErrUpdated; + CurrentCommandViewModel = firstCmdViewModel; + Commands.Add(firstCmdViewModel); + } + + private void AddNewCommand(TerminalCommandViewModel sender) + { + CurrentCommandViewModel!.CommandExecuted -= AddNewCommand; + CurrentCommandViewModel.StdoutUpdated -= StdOutErrUpdatedHanler; + CurrentCommandViewModel.StdErrUpdated -= StdOutErrUpdatedHanler; + + TerminalCommandViewModel newCmdViewModel = new TerminalCommandViewModel(); + string workDir = sender.WorkingDirectory ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + newCmdViewModel.WorkingDirectory = workDir; + newCmdViewModel.BackEnd = BackEnd; + newCmdViewModel.CommandExecuted += AddNewCommand; + newCmdViewModel.StdoutUpdated += StdOutErrUpdatedHanler; + newCmdViewModel.StdErrUpdated += StdOutErrUpdatedHanler; + CurrentCommandViewModel = newCmdViewModel; + Commands.Add(newCmdViewModel); + } + + public event Action? StdOutErrUpdated; + private void StdOutErrUpdatedHanler(TerminalCommandViewModel sender)=> StdOutErrUpdated?.Invoke(sender); + private ICommand? _clearCommand; + public ICommand? ClearCommand + { + get => _clearCommand; + set => SetProperty(ref _clearCommand, value); + } + + } + + public class TerminalCommandViewModel : ViewModelBase + { + private string _name = string.Empty; + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + + private string _workingDirectory = string.Empty; + public string WorkingDirectory + { + get => _workingDirectory; + set + { + SetProperty(ref _workingDirectory, value); + OnPropertyChanged(nameof(WorkingDirectoryShort)); + } + } + + public string WorkingDirectoryShort + { + get + { + if (WorkingDirectory.Length > 20) + { + // 显示前 5 个字符 + "..." + 后 15 个字符 + string start = WorkingDirectory.Substring(0, 5); + string end = WorkingDirectory.Substring(WorkingDirectory.Length - 15); + return "FIP "+ start + "..."+ end + _prompt+ " "; + } + else + { + return "FIP " + WorkingDirectory + _prompt + " "; + } + } + } + + private string _prompt = ">"; + public string Prompt + { + get => _prompt; + set => SetProperty(ref _prompt, value); + } + + private string _command = string.Empty; + public string Command + { + get => _command; + set => SetProperty(ref _command, value); + } + private CommandBackEnd _backEnd = CommandBackEnd.Cmd; + public CommandBackEnd BackEnd + { + get => _backEnd; + set => SetProperty(ref _backEnd, value); + } + private bool _isReadOnly = false; + public bool IsReadOnly + { + get => _isReadOnly; + set => SetProperty(ref _isReadOnly, value); + } + + private string _stdout = string.Empty; + public string Stdout + { + get => _stdout; + set + { + SetProperty(ref _stdout, value); + OnPropertyChanged(nameof(StdoutVisibility)); + } + } + public Visibility StdoutVisibility => string.IsNullOrEmpty(Stdout) ? Visibility.Collapsed : Visibility.Visible; + + + private string _stderr = string.Empty; + public string StdErr + { + get => _stderr; + set + { + SetProperty(ref _stderr, value); + OnPropertyChanged(nameof(StdErrVisibility)); + } + } + public Visibility StdErrVisibility => string.IsNullOrEmpty(StdErr) ? Visibility.Collapsed : Visibility.Visible; + + private ICommand? _executeCommand; + public ICommand? ExecuteCommand + { + get => _executeCommand; + set => SetProperty(ref _executeCommand, value); + } + + private void StdoutWriteLine(string text) + { + if (string.IsNullOrEmpty(Stdout)) + { + Stdout = text; + } + else + { + Stdout += Environment.NewLine + text; + } + StdoutUpdated?.Invoke(this); + } + private void StdErrWriteLine(string text) + { + if (string.IsNullOrEmpty(StdErr)) + { + StdErr = text; + } + else + { + StdErr += Environment.NewLine + text; + } + StdErrUpdated?.Invoke(this); + } + + public event Action? CommandExecuted; + public event Action? StdoutUpdated; + public event Action? StdErrUpdated; + + private CancellationTokenSource? _cts; + public void CancelCommand() => _cts?.Cancel(); + private async void Execute() + { + try + { + if (string.IsNullOrEmpty(Command)) return; + + _cts?.Cancel(); + _cts?.Dispose(); + _cts = new CancellationTokenSource(); + + // 检测是否是 cd/chdir 内置命令,在进程内切换目录而不启动外部进程 + string trimmedCommand = Command.TrimStart(); + bool isCd = trimmedCommand.StartsWith("cd ", StringComparison.OrdinalIgnoreCase) || + trimmedCommand.Equals("cd", StringComparison.OrdinalIgnoreCase) || + trimmedCommand.StartsWith("chdir ", StringComparison.OrdinalIgnoreCase) || + trimmedCommand.Equals("chdir", StringComparison.OrdinalIgnoreCase); + if (isCd) + { + HandleCdCommand(trimmedCommand); + return; + } + + Stdout = string.Empty; + StdErr = string.Empty; + MacroActionCommand action = new MacroActionCommand(); + action.PreWait = 0; + action.PostWait = 0; + action.WaitUntilExit = true; + action.Command = Command; + var (exitCode, _) = await MacroActionPlayer.RunCommandAction( + action, + stdoutCallback: StdoutWriteLine, + stderrCallback: StdErrWriteLine, + workingDirectory: WorkingDirectory, + cancellationToken: _cts.Token, + BackEnd + ); + + if (exitCode == -2) + { + StdErrWriteLine("命令已被用户取消。"); + } + else if (exitCode != 0) + { + StdErrWriteLine($"命令退出代码: {exitCode}"); + } + } + catch (Exception ex) + { + StdErrWriteLine($"命令执行失败: {ex.Message}"); + } + finally + { + IsReadOnly = true; + CommandExecuted?.Invoke(this); + } + } + + /// + /// 处理 cd/chdir 内置命令,在当前进程内切换工作目录 + /// + private void HandleCdCommand(string rawCommand) + { + // 去掉 "cd" 或 "chdir" 前缀,提取路径部分 + string afterCmd = rawCommand; + if (rawCommand.StartsWith("chdir ", StringComparison.OrdinalIgnoreCase)) + afterCmd = rawCommand.Substring(6).TrimStart(); + else if (rawCommand.Equals("chdir", StringComparison.OrdinalIgnoreCase)) + afterCmd = ""; + else if (rawCommand.StartsWith("cd ", StringComparison.OrdinalIgnoreCase)) + afterCmd = rawCommand.Substring(3).TrimStart(); + else if (rawCommand.Equals("cd", StringComparison.OrdinalIgnoreCase)) + afterCmd = ""; + + string pathPart = afterCmd.Trim(); + + // 处理 "/D" 开关(cmd cd 命令的跨驱动器开关) + if (pathPart.StartsWith("/D ", StringComparison.OrdinalIgnoreCase)) + { + pathPart = pathPart.Substring(3).TrimStart(); + } + + if (string.IsNullOrEmpty(pathPart)) + { + // cd 无参数 → 显示当前目录 + StdoutWriteLine(WorkingDirectory); + return; + } + + // 展开环境变量 %VAR% 并解析为绝对路径 + string expandedPath = Environment.ExpandEnvironmentVariables(pathPart); + + try + { + string fullPath = Path.GetFullPath(expandedPath); + + if (Directory.Exists(fullPath)) + { + WorkingDirectory = fullPath; + } + else + { + StdErrWriteLine($"找不到路径 '{pathPart}',因为该目录不存在。"); + } + } + catch (Exception ex) + { + StdErrWriteLine($"cd 命令出错: {ex.Message}"); + } + } + + public TerminalCommandViewModel() + { + ExecuteCommand = new RelayCommand(Execute); + } + } +} diff --git a/FancyInput/ViewModels/ViewModelBase.cs b/FancyInput/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..af59366 --- /dev/null +++ b/FancyInput/ViewModels/ViewModelBase.cs @@ -0,0 +1,44 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows.Input; + +namespace FancyInput.ViewModels +{ + public class ViewModelBase:INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + protected virtual bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") + { + if (Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } + } + + public class RelayCommand : ICommand + { + private readonly Action _execute; + private readonly Func? _canExecute; + + public RelayCommand(Action execute, Func? canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public bool CanExecute(object? parameter) => _canExecute == null || _canExecute(); + + public void Execute(object? parameter) => _execute(); + + public event EventHandler? CanExecuteChanged + { + add => CommandManager.RequerySuggested += value; + remove => CommandManager.RequerySuggested -= value; + } + } +} diff --git a/FancyInput/Views/AboutWindow.xaml b/FancyInput/Views/AboutWindow.xaml new file mode 100644 index 0000000..dd70e4d --- /dev/null +++ b/FancyInput/Views/AboutWindow.xaml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + diff --git a/FancyInput/Views/AboutWindow.xaml.cs b/FancyInput/Views/AboutWindow.xaml.cs new file mode 100644 index 0000000..27736aa --- /dev/null +++ b/FancyInput/Views/AboutWindow.xaml.cs @@ -0,0 +1,93 @@ +using System.Windows; + +namespace FancyInput.Views +{ + /// + /// helpWindow.xaml 的交互逻辑 + /// + public partial class AboutWindow : Window + { + private string _version = "1.0.0"; + public string Version + { + get { return _version; } + set + { + _version = value; + VersionLabel.Content = _version; + } + } + private string _date = "2026-2-11"; + public string Date + { + get { return _date; } + set + { + _date = value; + DateLabel.Content = _date; + } + } + private string _mail = "contact@xlworkspace.com"; + public string Mail + { + get { return _mail; } + set + { + _mail = value; + MailButton.Content = _mail; + } + } + + public event Action? CheckUpdateRequested; + public event Action? UpdateTimelineRequested; + public AboutWindow(string version, string date,string mail) + { + InitializeComponent(); + Version = version; + Date = date; + Mail = mail; + } + + private void MailButton_Click(object sender, RoutedEventArgs e) + { + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = $"mailto:{Mail}", + UseShellExecute = true + }); + } + catch (System.Exception ex) + { + FancyInput.AppMessageBox.Show($"无法打开邮件客户端: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void CheckUpdateButton_Click(object sender, RoutedEventArgs e) + { + CheckUpdateRequested?.Invoke(); + } + + private void UpdateTimelineButton_Click(object sender, RoutedEventArgs e) + { + UpdateTimelineRequested?.Invoke(); + } + + private void SupportButton_Click(object sender, RoutedEventArgs e) + { + QRCodeWindow qRCodeWindow = new QRCodeWindow(); + qRCodeWindow.Owner = this; + qRCodeWindow.WindowStartupLocation = WindowStartupLocation.CenterOwner; + qRCodeWindow.ShowDialog(); + } + + private void LicenseButton_Click(object sender, RoutedEventArgs e) + { + OpenSourceStatementWindow window = new OpenSourceStatementWindow(); + window.Owner = this; + window.WindowStartupLocation = WindowStartupLocation.CenterOwner; + window.ShowDialog(); + } + } +} diff --git a/FancyInput/Views/ConsoleWindow.xaml b/FancyInput/Views/ConsoleWindow.xaml new file mode 100644 index 0000000..1b9342c --- /dev/null +++ b/FancyInput/Views/ConsoleWindow.xaml @@ -0,0 +1,18 @@ + + + + + diff --git a/FancyInput/Views/ConsoleWindow.xaml.cs b/FancyInput/Views/ConsoleWindow.xaml.cs new file mode 100644 index 0000000..2c221b7 --- /dev/null +++ b/FancyInput/Views/ConsoleWindow.xaml.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace FancyInput.Views.Windows +{ + /// + /// ConsoleWindow.xaml 的交互逻辑 + /// + public partial class ConsoleWindow : Window + { + public ConsoleWindow() + { + InitializeComponent(); + } + + public void WriteLine(string text) + { + OutputBox.AppendText(text + "\r\n"); + OutputBox.ScrollToEnd(); + } + + // 可选:批量输出 + public void Write(string text) + { + OutputBox.AppendText(text); + OutputBox.ScrollToEnd(); + } + } +} diff --git a/FancyInput/Views/Controls/AnimatedImage.xaml b/FancyInput/Views/Controls/AnimatedImage.xaml new file mode 100644 index 0000000..b6ca548 --- /dev/null +++ b/FancyInput/Views/Controls/AnimatedImage.xaml @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/FancyInput/Views/Controls/AnimatedImage.xaml.cs b/FancyInput/Views/Controls/AnimatedImage.xaml.cs new file mode 100644 index 0000000..a440202 --- /dev/null +++ b/FancyInput/Views/Controls/AnimatedImage.xaml.cs @@ -0,0 +1,127 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System; +using System.IO; +using System.Runtime.InteropServices; + + +namespace FancyInput.Views.Controls +{ + /// + /// AnimatedImage.xaml 的交互逻辑 + /// + public partial class AnimatedImage : Image + { + private ImageSource? _lastAppliedSource; + private bool _lastWasAnimated; + + public AnimatedImage() + { + InitializeComponent(); + } + + // Keep an input property separate from Image.Source. + // WpfAnimatedGif internally animates Source each frame, so using Source as input causes re-entry issues. + public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register( + nameof(ImageSource), + typeof(ImageSource), + typeof(AnimatedImage), + new PropertyMetadata(null, OnImageSourceChanged)); + + public ImageSource? ImageSource + { + get => (ImageSource?)GetValue(ImageSourceProperty); + set => SetValue(ImageSourceProperty, value); + } + + private static void OnImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AnimatedImage control) + { + control.UpdateAnimatedGif(e.NewValue as ImageSource); + } + } + + private static bool IsGifImageSource(ImageSource? source) + { + if (source is BitmapFrame frame) + { + string mimeTypes = frame.Decoder?.CodecInfo?.MimeTypes ?? string.Empty; + return mimeTypes.IndexOf("gif", StringComparison.OrdinalIgnoreCase) >= 0; + } + + if (source is BitmapImage bitmapImage) + { + if (bitmapImage.UriSource != null && + bitmapImage.UriSource.AbsolutePath.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + var stream = bitmapImage.StreamSource; + if (stream != null && stream.CanSeek) + { + long oldPosition = stream.Position; + try + { + stream.Seek(0, SeekOrigin.Begin); + Span header = stackalloc byte[6]; + int read = stream.Read(header); + return read >= 6 && + header[0] == (byte)'G' && + header[1] == (byte)'I' && + header[2] == (byte)'F'; + } + finally + { + stream.Seek(oldPosition, SeekOrigin.Begin); + } + } + } + + return false; + } + + private void UpdateAnimatedGif(ImageSource? source) + { + bool shouldAnimate = IsGifImageSource(source); + if (ReferenceEquals(source, _lastAppliedSource) && shouldAnimate == _lastWasAnimated) + return; + + _lastAppliedSource = source; + _lastWasAnimated = shouldAnimate; + + if (shouldAnimate) + { + try + { + // Avoid Freezable inheritance-context conflicts between Source and AnimatedSource. + WpfAnimatedGif.ImageBehavior.SetAnimatedSource(this, null); + SetCurrentValue(SourceProperty, null); + WpfAnimatedGif.ImageBehavior.SetAnimatedSource(this, source); + } + catch (COMException) + { + // Fallback: show static image instead of crashing on problematic GIF metadata/frame composition. + WpfAnimatedGif.ImageBehavior.SetAnimatedSource(this, null); + SetCurrentValue(SourceProperty, source); + _lastWasAnimated = false; + } + catch (ArgumentException) + { + // Fallback for context/argument issues when initializing animation. + WpfAnimatedGif.ImageBehavior.SetAnimatedSource(this, null); + SetCurrentValue(SourceProperty, source); + _lastWasAnimated = false; + } + return; + } + + WpfAnimatedGif.ImageBehavior.SetAnimatedSource(this, null); + SetCurrentValue(SourceProperty, source); + } + + } +} diff --git a/FancyInput/Views/Controls/CreatePage.xaml b/FancyInput/Views/Controls/CreatePage.xaml new file mode 100644 index 0000000..261acb4 --- /dev/null +++ b/FancyInput/Views/Controls/CreatePage.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FancyInput/Views/Controls/CreatePage.xaml.cs b/FancyInput/Views/Controls/CreatePage.xaml.cs new file mode 100644 index 0000000..9b3eb9b --- /dev/null +++ b/FancyInput/Views/Controls/CreatePage.xaml.cs @@ -0,0 +1,44 @@ +using FancyInput.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace FancyInput.Views.Controls +{ + /// + /// CreatePage.xaml 的交互逻辑 + /// + public partial class CreatePage : UserControl + { + public CreatePage() + { + InitializeComponent(); + } + + // ConfigEditClick + public static readonly RoutedEvent CreateFromEmptyClickEvent = EventManager.RegisterRoutedEvent(nameof(CreateFromEmptyClick), RoutingStrategy.Bubble, + typeof(RoutedEventHandler), typeof(CreatePage)); + + public event RoutedEventHandler CreateFromEmptyClick + { + add => AddHandler(CreateFromEmptyClickEvent, value); + remove => RemoveHandler(CreateFromEmptyClickEvent, value); + } + + private void CreateFromEmpty(object sender, MouseButtonEventArgs e) + { + RaiseEvent(new RoutedEventArgs(CreateFromEmptyClickEvent)); + } + } +} diff --git a/FancyInput/Views/Controls/ElementViewPanel.xaml b/FancyInput/Views/Controls/ElementViewPanel.xaml new file mode 100644 index 0000000..4d6d92f --- /dev/null +++ b/FancyInput/Views/Controls/ElementViewPanel.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/FancyInput/Views/Controls/ManagePage.xaml.cs b/FancyInput/Views/Controls/ManagePage.xaml.cs new file mode 100644 index 0000000..d5f16d7 --- /dev/null +++ b/FancyInput/Views/Controls/ManagePage.xaml.cs @@ -0,0 +1,114 @@ +using FancyInput.ViewModels; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace FancyInput.Views.Controls +{ + public class ElementMoveActionEventArgs : RoutedEventArgs + { + public int Index { get; } + public ElementMoveActionEventArgs(RoutedEvent routedEvent, int index) : base(routedEvent) + { + Index = index; + } + } + + /// + /// ManagePage.xaml 的交互逻辑 + /// + public partial class ManagePage : UserControl + { + public ManagePage() + { + InitializeComponent(); + } + // OverlayWindowViewModels + public static readonly DependencyProperty OverlayWindowViewModelsProperty = DependencyProperty.Register(nameof(OverlayWindowViewModels), + typeof(ObservableCollection), typeof(ManagePage), new PropertyMetadata(new ObservableCollection())); + public ObservableCollection OverlayWindowViewModels + { + get => (ObservableCollection)GetValue(OverlayWindowViewModelsProperty); + set => SetValue(OverlayWindowViewModelsProperty, value); + } + + // ConfigLoadClick + public static readonly RoutedEvent SaveGroupClickEvent = EventManager.RegisterRoutedEvent(nameof(SaveGroupClick), RoutingStrategy.Bubble, + typeof(RoutedEventHandler), typeof(ManagePage)); + + public event RoutedEventHandler SaveGroupClick + { + add => AddHandler(SaveGroupClickEvent, value); + remove => RemoveHandler(SaveGroupClickEvent, value); + } + + // ConfigEditClick + public static readonly RoutedEvent LoadGroupClickEvent = EventManager.RegisterRoutedEvent(nameof(LoadGroupClick), RoutingStrategy.Bubble, + typeof(RoutedEventHandler), typeof(ManagePage)); + + public event RoutedEventHandler LoadGroupClick + { + add => AddHandler(LoadGroupClickEvent, value); + remove => RemoveHandler(LoadGroupClickEvent, value); + } + + // UpMoveClick + public static readonly RoutedEvent UpMoveClickEvent = EventManager.RegisterRoutedEvent(nameof(UpMoveClick), RoutingStrategy.Bubble, + typeof(EventHandler), typeof(ManagePage)); + public event EventHandler UpMoveClick + { + add => AddHandler(UpMoveClickEvent, value); + remove => RemoveHandler(UpMoveClickEvent, value); + } + + // DownMoveClick + public static readonly RoutedEvent DownMoveClickEvent = EventManager.RegisterRoutedEvent(nameof(DownMoveClick), RoutingStrategy.Bubble, + typeof(EventHandler), typeof(ManagePage)); + public event EventHandler DownMoveClick + { + add => AddHandler(DownMoveClickEvent, value); + remove => RemoveHandler(DownMoveClickEvent, value); + } + + private void DeleteOverlayWindow_Click(object sender, RoutedEventArgs e) + { + if (sender is Button btn && btn.DataContext is OverlayWindowViewModel vm) + { + OverlayWindow overlayWindow = vm.OverlayWindow; + overlayWindow.Close(); + } + } + + private void SaveGroup(object sender, RoutedEventArgs e) => RaiseEvent(new RoutedEventArgs(SaveGroupClickEvent)); + + private void LoadGroup(object sender, RoutedEventArgs e)=> RaiseEvent(new RoutedEventArgs(LoadGroupClickEvent)); + + private void UpMove(object sender, RoutedEventArgs e) + { + if (sender is Button btn && btn.DataContext is OverlayWindowViewModel vm) + { + RaiseEvent(new ElementMoveActionEventArgs(UpMoveClickEvent, OverlayWindowViewModels.IndexOf(vm))); + } + } + + private void DownMove(object sender, RoutedEventArgs e) + { + if (sender is Button btn && btn.DataContext is OverlayWindowViewModel vm) + { + RaiseEvent(new ElementMoveActionEventArgs(DownMoveClickEvent, OverlayWindowViewModels.IndexOf(vm))); + } + } + } +} diff --git a/FancyInput/Views/Controls/OutputPanel.xaml b/FancyInput/Views/Controls/OutputPanel.xaml new file mode 100644 index 0000000..bc21ac7 --- /dev/null +++ b/FancyInput/Views/Controls/OutputPanel.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FancyInput/Views/Controls/Terminal.xaml.cs b/FancyInput/Views/Controls/Terminal.xaml.cs new file mode 100644 index 0000000..57cdcd2 --- /dev/null +++ b/FancyInput/Views/Controls/Terminal.xaml.cs @@ -0,0 +1,130 @@ +using FancyInput.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace FancyInput.Views.Controls +{ + /// + /// Terminal.xaml 的交互逻辑 + /// + public partial class Terminal : UserControl + { + public TerminalViewModel ViewModel { get; set; } = new TerminalViewModel(); + + public Terminal() + { + InitializeComponent(); + this.DataContext = ViewModel; + + ViewModel.Commands.CollectionChanged += Commands_CollectionChanged; + ViewModel.StdOutErrUpdated += (s) => + { + Dispatcher.BeginInvoke(new Action(() => + { + TerminalScroller.ScrollToEnd(); + }), System.Windows.Threading.DispatcherPriority.Loaded); + }; + } + + private void TerminalScroller_PreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + if (Keyboard.Modifiers == ModifierKeys.Control) + { + // Ctrl + 滚轮 → 放大/缩小字体 + if (e.Delta > 0) + ViewModel.FontSize += 1; + else if (e.Delta < 0 && ViewModel.FontSize > 1) + ViewModel.FontSize -= 1; + e.Handled = true; // 阻止滚动事件继续传递 + } + } + + private void TerminalScroller_Loaded(object sender, RoutedEventArgs e) + { + // 使用 Dispatcher 延迟,确保 ItemsControl 已完成布局生成容器 + Dispatcher.BeginInvoke(new Action(FocusAndScrollToLastCommand), + System.Windows.Threading.DispatcherPriority.Loaded); + } + + /// + /// 集合新增条目时 → 聚焦新命令行 + 滚动到底部 + /// + private void Commands_CollectionChanged(object? sender, + System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) + { + Dispatcher.BeginInvoke(new Action(FocusAndScrollToLastCommand), + System.Windows.Threading.DispatcherPriority.Loaded); + } + } + + /// + /// 找到最后一条命令的可编辑 TextBox,聚焦并使光标闪烁;同时滚动到最底部 + /// + private void FocusAndScrollToLastCommand() + { + if (ViewModel.Commands.Count == 0) return; + + var lastIndex = ViewModel.Commands.Count - 1; + var container = CommandItemsControl.ItemContainerGenerator.ContainerFromIndex(lastIndex); + + if (container != null) + { + var textBox = FindEditableTextBox(container); + if (textBox != null) + { + textBox.Focus(); + Keyboard.Focus(textBox); + //textBox.CaretIndex = textBox.Text.Length; + } + } + TerminalScroller.ScrollToEnd(); + } + + /// + /// 递归遍历可视化树,找到第一个 IsReadOnly=false 的 TextBox + /// + private static TextBox? FindEditableTextBox(DependencyObject parent) + { + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + if (child is TextBox tb && !tb.IsReadOnly) + return tb; + + var result = FindEditableTextBox(child); + if (result != null) + return result; + } + return null; + } + + private void TerminalScroller_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + FocusAndScrollToLastCommand(); + } + + + private void TerminalScroller_PreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.KeyboardDevice.Modifiers == ModifierKeys.Control && e.Key == Key.C) + { + ViewModel.CurrentCommandViewModel?.CancelCommand(); + } + } + + } +} diff --git a/FancyInput/Views/Controls/WindowControlBar.xaml b/FancyInput/Views/Controls/WindowControlBar.xaml new file mode 100644 index 0000000..f963ba1 --- /dev/null +++ b/FancyInput/Views/Controls/WindowControlBar.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + diff --git a/FancyInput/Views/Controls/WindowControlBar.xaml.cs b/FancyInput/Views/Controls/WindowControlBar.xaml.cs new file mode 100644 index 0000000..de0def2 --- /dev/null +++ b/FancyInput/Views/Controls/WindowControlBar.xaml.cs @@ -0,0 +1,217 @@ +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Timers; + + +namespace FancyInput.Views.Controls +{ + /// + /// WindowControlBar.xaml 的交互逻辑 + /// + public partial class WindowControlBar : UserControl + { + public static readonly DependencyProperty ParentWindowProperty = + DependencyProperty.Register( + "ParentWindow", + typeof(Window), + typeof(WindowControlBar), + new PropertyMetadata(null) + ); + + public Window ParentWindow + { + get { return (Window)GetValue(ParentWindowProperty); } + set { SetValue(ParentWindowProperty, value); } + } + + public int i = 0; + public static readonly DependencyProperty ResizeModeProperty = + DependencyProperty.Register("ResizeMode", + typeof(ResizeMode), + typeof(WindowControlBar), + new PropertyMetadata(ResizeMode.CanResize, OnResizeModeChanged) + ); + + public ResizeMode ResizeMode + { + get { return (ResizeMode)GetValue(ResizeModeProperty); } + set { SetValue(ResizeModeProperty, value); } + } + + public WindowControlBar() + { + InitializeComponent(); + } + + // 属性改变回调函数 + private static void OnResizeModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + WindowControlBar myUserControl = (WindowControlBar)d; + ResizeMode newResizeMode = (ResizeMode)e.NewValue; + if (newResizeMode == ResizeMode.NoResize) + { + myUserControl.MaximizeButton.Visibility = Visibility.Collapsed; + myUserControl.NormalmizeButton.Visibility = Visibility.Collapsed; + } + + else if(newResizeMode == ResizeMode.CanResize) + { + myUserControl.MaximizeButton.Visibility = Visibility.Visible; + myUserControl.NormalmizeButton.Visibility = Visibility.Visible; + Grid.SetColumn(myUserControl.MinimizeButton, 1); + } + + } + + private void CloseButton_Click(object sender, RoutedEventArgs e) + { + if (ParentWindow != null) { ParentWindow.Close(); } + } + + private void MinimizeButton_Click(object sender, RoutedEventArgs e) + { + // Debug.WriteLine($"ResizeMode = {ResizeMode}"); + if (ParentWindow != null) { ParentWindow.WindowState = WindowState.Minimized; } + } + + private void MaximizeButton_Click(object sender, RoutedEventArgs e) + { + if (ParentWindow != null) + { + NormalmizeButton.Visibility = Visibility.Visible; + MaximizeButton.Visibility = Visibility.Collapsed; + ParentWindow.WindowState = WindowState.Maximized; + } + } + + private void NormalmizeButton_Click(object sender, RoutedEventArgs e) + { + if (ParentWindow != null) + { + NormalmizeButton.Visibility = Visibility.Collapsed; + MaximizeButton.Visibility = Visibility.Visible; + ParentWindow.WindowState = WindowState.Normal; + } + } + + private void DragLabel_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + _isMousePressed = true; + //if (ParentWindow == null) return; + //var mousePos = e.GetPosition(this); + //var lastMousePos = mousePos; + //var allowDrag = true; + //while (true) + //{ + // var mousePosOnBar = e.GetPosition(this); + // if (lastMousePos != mousePos) + // break; + // if (Mouse.LeftButton != MouseButtonState.Pressed) + // { + // allowDrag = false; + // break; + // } + // Debug.WriteLine($"Mouse Position: {mousePosOnBar.X}, {mousePosOnBar.Y},lastMousePos: {lastMousePos.X}, {lastMousePos.Y},Mouse.LeftButton={Mouse.LeftButton}"); + // lastMousePos = mousePosOnBar; + //} + //if (!allowDrag) return; + + + //if (ParentWindow.WindowState != WindowState.Maximized) + //{ + // CompositionTarget.Rendering += WindowControlBar_Rendering; + // if (e.LeftButton == MouseButtonState.Pressed && ParentWindow != null) { ParentWindow.DragMove(); } + // CompositionTarget.Rendering -= WindowControlBar_Rendering; + //} + //else + //{ + // var mousePosOnBar = e.GetPosition(this); + // var mousePosOnScreen = PointToScreen(mousePosOnBar); + + // var width1 = ParentWindow.ActualWidth; + // NormalmizeButton.Visibility = Visibility.Collapsed; + // MaximizeButton.Visibility = Visibility.Visible; + // ParentWindow.WindowState = WindowState.Normal; + // var width2 = ParentWindow.ActualWidth; + // var horizontalScale = (float)width2 / (float)width1; + // var xmove = mousePosOnScreen.X - (int)(mousePosOnScreen.X* horizontalScale); + // ParentWindow.Left = xmove; + // ParentWindow.Top = mousePosOnScreen.Y; + + // CompositionTarget.Rendering += WindowControlBar_Rendering; + // if (e.LeftButton == MouseButtonState.Pressed) { ParentWindow.DragMove(); } + // CompositionTarget.Rendering -= WindowControlBar_Rendering; + //} + + } + + //private bool _isMouseInLabel = false; + private bool _isMousePressed = false; + public void WindowControlBar_Rendering(object? sender, EventArgs e) + { + if (ParentWindow != null) + { + if (ParentWindow.WindowState == WindowState.Maximized) + { + NormalmizeButton.Visibility = Visibility.Visible; + MaximizeButton.Visibility = Visibility.Collapsed; + } + } + } + + private void DragLabel_MouseMove(object sender, MouseEventArgs e) + { + if (ParentWindow == null) return; + if (_isMousePressed) + { + if (ParentWindow.WindowState != WindowState.Maximized) + { + CompositionTarget.Rendering += WindowControlBar_Rendering; + if (e.LeftButton == MouseButtonState.Pressed && ParentWindow != null) { ParentWindow.DragMove(); } + CompositionTarget.Rendering -= WindowControlBar_Rendering; + } + else + { + var mousePosOnBar = e.GetPosition(this); + var mousePosOnScreen = PointToScreen(mousePosOnBar); + + var width1 = ParentWindow.ActualWidth; + NormalmizeButton.Visibility = Visibility.Collapsed; + MaximizeButton.Visibility = Visibility.Visible; + ParentWindow.WindowState = WindowState.Normal; + var width2 = ParentWindow.ActualWidth; + var horizontalScale = (float)width2 / (float)width1; + var xmove = mousePosOnScreen.X - (int)(mousePosOnScreen.X * horizontalScale); + ParentWindow.Left = xmove; + ParentWindow.Top = mousePosOnScreen.Y; + + CompositionTarget.Rendering += WindowControlBar_Rendering; + if (e.LeftButton == MouseButtonState.Pressed) { ParentWindow.DragMove(); } + CompositionTarget.Rendering -= WindowControlBar_Rendering; + } + } + + } + + private void DragLabel_MouseEnter(object sender, MouseEventArgs e) + { + //_isMouseInLabel = true; + if (_isMousePressed) _isMousePressed = false; + } + + private void DragLabel_MouseLeave(object sender, MouseEventArgs e) + { + //_isMouseInLabel = false; + if (_isMousePressed) _isMousePressed = false; + } + + private void DragLabel_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + _isMousePressed = false; + } + } +} diff --git a/FancyInput/Views/Controls/WorkshopPanel.xaml b/FancyInput/Views/Controls/WorkshopPanel.xaml new file mode 100644 index 0000000..29e5490 --- /dev/null +++ b/FancyInput/Views/Controls/WorkshopPanel.xaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + diff --git a/FancyInput/Views/Controls/WorkshopPanel.xaml.cs b/FancyInput/Views/Controls/WorkshopPanel.xaml.cs new file mode 100644 index 0000000..95fe161 --- /dev/null +++ b/FancyInput/Views/Controls/WorkshopPanel.xaml.cs @@ -0,0 +1,43 @@ +using System.Windows; +using System.Windows.Controls; + +namespace FancyInput.Views.Controls +{ + /// + /// WorkshopPanel.xaml 的交互逻辑 + /// + public partial class WorkshopPanel : UserControl + { + public WorkshopPanel() + { + InitializeComponent(); + } + + private void ChangeMacroPath(object sender, RoutedEventArgs e) + { + using (var dialog = new System.Windows.Forms.FolderBrowserDialog()) + { + dialog.Description = "选择宏文件夹"; + if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) + { + string folderPath = dialog.SelectedPath; + MacroPathTextBox.Text = folderPath; + } + } + + } + + public static readonly RoutedEvent MacroWindowOpenClickEvent = EventManager.RegisterRoutedEvent(nameof(MacroWindowOpenClick), RoutingStrategy.Bubble, + typeof(RoutedEventHandler), typeof(WorkshopPanel)); + public event RoutedEventHandler MacroWindowOpenClick + { + add { AddHandler(MacroWindowOpenClickEvent, value); } + remove { RemoveHandler(MacroWindowOpenClickEvent, value); } + } + + private void OpenMacroWindow(object sender, RoutedEventArgs e) + { + RaiseEvent(new RoutedEventArgs(MacroWindowOpenClickEvent)); + } + } +} diff --git a/FancyInput/Views/ElementTreeHelpWindow.xaml b/FancyInput/Views/ElementTreeHelpWindow.xaml new file mode 100644 index 0000000..09efeca --- /dev/null +++ b/FancyInput/Views/ElementTreeHelpWindow.xaml @@ -0,0 +1,384 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 当前版本:Unknown + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FancyInput/Views/ElementTreeHelpWindow.xaml.cs b/FancyInput/Views/ElementTreeHelpWindow.xaml.cs new file mode 100644 index 0000000..d9b5986 --- /dev/null +++ b/FancyInput/Views/ElementTreeHelpWindow.xaml.cs @@ -0,0 +1,16 @@ +using System.Windows; + +namespace FancyInput.Views +{ + /// + /// ElementTreeHelpWindow.xaml 的交互逻辑 + /// + public partial class ElementTreeHelpWindow : Window + { + public ElementTreeHelpWindow() + { + InitializeComponent(); + VersionTextBlock.Text = $"当前版本: {System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}"; + } + } +} diff --git a/FancyInput/Views/ElementTreeWindow.xaml b/FancyInput/Views/ElementTreeWindow.xaml new file mode 100644 index 0000000..2325e8e --- /dev/null +++ b/FancyInput/Views/ElementTreeWindow.xaml @@ -0,0 +1,480 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +