From d80276d86709b0fcd96269e41f4412eb77daa44f Mon Sep 17 00:00:00 2001 From: RL-Xiang Date: Wed, 2 Sep 2026 20:08:33 +0800 Subject: [PATCH] FancyInput --- .gitignore | 39 + FancyInput.sln | 28 + FancyInput/App.xaml | 24 + FancyInput/App.xaml.cs | 217 ++++ FancyInput/AppMessageBox.cs | 50 + FancyInput/AppMessageBoxWindow.xaml | 114 ++ FancyInput/AppMessageBoxWindow.xaml.cs | 242 ++++ FancyInput/AssemblyInfo.cs | 10 + FancyInput/Common/ButtonHelpers.cs | 26 + FancyInput/Common/Converters.cs | 48 + FancyInput/Common/Enums.cs | 320 +++++ FancyInput/Common/IntegerTextBoxBehavior.cs | 130 ++ FancyInput/Common/PathData.cs | 257 ++++ FancyInput/Common/Utility.cs | 133 +++ FancyInput/FancyInput.csproj | 47 + FancyInput/MainWindow.xaml | 132 +++ FancyInput/MainWindow.xaml.cs | 729 ++++++++++++ FancyInput/Models/EnumDescriptionConverter.cs | 21 + FancyInput/Models/HidHideManager.cs | 350 ++++++ FancyInput/Models/InputGenerator.cs | 326 ++++++ FancyInput/Models/InputHandler.cs | 549 +++++++++ FancyInput/Models/InputMacro.cs | 95 ++ FancyInput/Models/InputParser.cs | 803 +++++++++++++ FancyInput/Models/MacroActionPlayer.cs | 362 ++++++ FancyInput/Models/MacroEnums.cs | 27 + FancyInput/Models/OverlayParser.cs | 96 ++ FancyInput/Models/RawInputParser.cs | 653 +++++++++++ FancyInput/Models/SimpleProgressConverter.cs | 56 + FancyInput/Models/VirtualControllerManager.cs | 311 +++++ FancyInput/Models/XImage.cs | 662 +++++++++++ FancyInput/Resources/Icons/Icon.ico | Bin 0 -> 36380 bytes FancyInput/Resources/Icons/Icon.png | Bin 0 -> 53260 bytes FancyInput/Resources/QRCodes/bilibili.jpg | Bin 0 -> 29078 bytes FancyInput/Resources/QRCodes/wechat.jpg | Bin 0 -> 19559 bytes FancyInput/Resources/QRCodes/xlworkspace.jpg | Bin 0 -> 23075 bytes FancyInput/Resources/Styles/BorderStyles.xaml | 25 + FancyInput/Resources/Styles/ButtonStyles.xaml | 254 ++++ .../Resources/Styles/CheckBoxStyles.xaml | 224 ++++ .../Resources/Styles/ComboBoxStyles.xaml | 133 +++ .../Resources/Styles/ControlStyles.xaml | 42 + FancyInput/Resources/Styles/LabelStyles.xaml | 29 + FancyInput/Resources/Styles/MenuStyles.xaml | 125 ++ .../Resources/Styles/ScrollViewerStyles.xaml | 164 +++ FancyInput/Resources/Styles/SliderStyles.xaml | 266 +++++ .../Resources/Styles/TextBoxStyles.xaml | 66 ++ .../ViewModels/ElementSegmentViewModel.cs | 308 +++++ FancyInput/ViewModels/ElementTreeViewModel.cs | 756 ++++++++++++ .../ViewModels/ElementViewModel.Data.cs | 439 +++++++ .../ViewModels/ElementViewModel.Display.cs | 721 ++++++++++++ .../ViewModels/ElementViewModel.Test.cs | 586 +++++++++ FancyInput/ViewModels/ImageEditorViewModel.cs | 713 +++++++++++ FancyInput/ViewModels/MacroActionViewModel.cs | 740 ++++++++++++ FancyInput/ViewModels/MacroTrackViewModel.cs | 548 +++++++++ FancyInput/ViewModels/MacroViewModel.cs | 826 +++++++++++++ FancyInput/ViewModels/MacroWindowViewModel.cs | 144 +++ FancyInput/ViewModels/MainWindowViewModel.cs | 658 +++++++++++ .../ViewModels/OverlayWindowViewModel.cs | 295 +++++ FancyInput/ViewModels/TerminalViewModel.cs | 356 ++++++ FancyInput/ViewModels/ViewModelBase.cs | 44 + FancyInput/Views/AboutWindow.xaml | 100 ++ FancyInput/Views/AboutWindow.xaml.cs | 93 ++ FancyInput/Views/ConsoleWindow.xaml | 18 + FancyInput/Views/ConsoleWindow.xaml.cs | 40 + FancyInput/Views/Controls/AnimatedImage.xaml | 8 + .../Views/Controls/AnimatedImage.xaml.cs | 127 ++ FancyInput/Views/Controls/CreatePage.xaml | 52 + FancyInput/Views/Controls/CreatePage.xaml.cs | 44 + .../Views/Controls/ElementViewPanel.xaml | 45 + .../Views/Controls/ElementViewPanel.xaml.cs | 163 +++ FancyInput/Views/Controls/IconButton.xaml | 50 + FancyInput/Views/Controls/IconButton.xaml.cs | 345 ++++++ FancyInput/Views/Controls/ManagePage.xaml | 143 +++ FancyInput/Views/Controls/ManagePage.xaml.cs | 114 ++ FancyInput/Views/Controls/OutputPanel.xaml | 32 + FancyInput/Views/Controls/OutputPanel.xaml.cs | 134 +++ FancyInput/Views/Controls/RGBColorSelect.xaml | 34 + .../Views/Controls/RGBColorSelect.xaml.cs | 255 ++++ FancyInput/Views/Controls/ReadPage.xaml | 114 ++ FancyInput/Views/Controls/ReadPage.xaml.cs | 211 ++++ FancyInput/Views/Controls/SettingsPage.xaml | 195 +++ .../Views/Controls/SettingsPage.xaml.cs | 52 + FancyInput/Views/Controls/Terminal.xaml | 82 ++ FancyInput/Views/Controls/Terminal.xaml.cs | 130 ++ .../Views/Controls/WindowControlBar.xaml | 74 ++ .../Views/Controls/WindowControlBar.xaml.cs | 217 ++++ FancyInput/Views/Controls/WorkshopPanel.xaml | 61 + .../Views/Controls/WorkshopPanel.xaml.cs | 43 + FancyInput/Views/ElementTreeHelpWindow.xaml | 384 ++++++ .../Views/ElementTreeHelpWindow.xaml.cs | 16 + FancyInput/Views/ElementTreeWindow.xaml | 480 ++++++++ FancyInput/Views/ElementTreeWindow.xaml.cs | 1006 ++++++++++++++++ FancyInput/Views/Elements/AnalogStick.xaml | 112 ++ FancyInput/Views/Elements/AnalogStick.xaml.cs | 58 + FancyInput/Views/Elements/ElementBase.cs | 358 ++++++ FancyInput/Views/Elements/ElementTest.xaml | 311 +++++ FancyInput/Views/Elements/ElementTest.xaml.cs | 1043 +++++++++++++++++ FancyInput/Views/Elements/GamepadButton.xaml | 97 ++ .../Views/Elements/GamepadButton.xaml.cs | 56 + FancyInput/Views/Elements/GamepadDpad.xaml | 128 ++ FancyInput/Views/Elements/GamepadDpad.xaml.cs | 38 + .../Views/Elements/GamepadPlayerId.xaml | 118 ++ .../Views/Elements/GamepadPlayerId.xaml.cs | 30 + FancyInput/Views/Elements/GamepadTrigger.xaml | 130 ++ .../Views/Elements/GamepadTrigger.xaml.cs | 41 + FancyInput/Views/Elements/KeyBoardButton.xaml | 98 ++ .../Views/Elements/KeyBoardButton.xaml.cs | 74 ++ FancyInput/Views/Elements/MouseButton.xaml | 96 ++ FancyInput/Views/Elements/MouseButton.xaml.cs | 60 + FancyInput/Views/Elements/MouseMovement.xaml | 90 ++ .../Views/Elements/MouseMovement.xaml.cs | 36 + FancyInput/Views/Elements/MouseWheel.xaml | 91 ++ FancyInput/Views/Elements/MouseWheel.xaml.cs | 26 + FancyInput/Views/Elements/Texture.xaml | 76 ++ FancyInput/Views/Elements/Texture.xaml.cs | 33 + FancyInput/Views/FullScreenMask.xaml | 24 + FancyInput/Views/FullScreenMask.xaml.cs | 53 + FancyInput/Views/ImageEditor.xaml | 238 ++++ FancyInput/Views/ImageEditor.xaml.cs | 354 ++++++ FancyInput/Views/Macro/FormRow.xaml | 23 + FancyInput/Views/Macro/FormRow.xaml.cs | 80 ++ .../Views/Macro/MacroActionPropertyPanel.xaml | 237 ++++ FancyInput/Views/Macro/MacroButton.xaml | 93 ++ FancyInput/Views/Macro/MacroButton.xaml.cs | 130 ++ .../Macro/MacroButtonTemplateSelector.cs | 37 + .../Macro/MacroPropertyTemplateSelector.cs | 38 + FancyInput/Views/Macro/MacroTrack.xaml | 153 +++ FancyInput/Views/Macro/MacroTrack.xaml.cs | 407 +++++++ FancyInput/Views/Macro/MacroWindow.xaml | 331 ++++++ FancyInput/Views/Macro/MacroWindow.xaml.cs | 121 ++ .../Views/OpenSourceStatementWindow.xaml | 118 ++ .../Views/OpenSourceStatementWindow.xaml.cs | 102 ++ FancyInput/Views/OverlayWindow.xaml | 61 + FancyInput/Views/OverlayWindow.xaml.cs | 195 +++ FancyInput/Views/QRCodeWindow.xaml | 54 + FancyInput/Views/QRCodeWindow.xaml.cs | 27 + FancyInput/Views/RgbInputDialog.xaml | 49 + FancyInput/Views/RgbInputDialog.xaml.cs | 130 ++ FancyInput/Views/TestWindow.xaml | 14 + FancyInput/Views/TestWindow.xaml.cs | 27 + FancyInput/Views/UpdatePromptWindow.xaml | 97 ++ FancyInput/Views/UpdatePromptWindow.xaml.cs | 204 ++++ FancyInput/Views/UpdateTimelineWindow.xaml | 96 ++ FancyInput/Views/UpdateTimelineWindow.xaml.cs | 51 + LICENSE | 21 + README.md | 99 ++ 145 files changed, 27045 insertions(+) create mode 100644 .gitignore create mode 100644 FancyInput.sln create mode 100644 FancyInput/App.xaml create mode 100644 FancyInput/App.xaml.cs create mode 100644 FancyInput/AppMessageBox.cs create mode 100644 FancyInput/AppMessageBoxWindow.xaml create mode 100644 FancyInput/AppMessageBoxWindow.xaml.cs create mode 100644 FancyInput/AssemblyInfo.cs create mode 100644 FancyInput/Common/ButtonHelpers.cs create mode 100644 FancyInput/Common/Converters.cs create mode 100644 FancyInput/Common/Enums.cs create mode 100644 FancyInput/Common/IntegerTextBoxBehavior.cs create mode 100644 FancyInput/Common/PathData.cs create mode 100644 FancyInput/Common/Utility.cs create mode 100644 FancyInput/FancyInput.csproj create mode 100644 FancyInput/MainWindow.xaml create mode 100644 FancyInput/MainWindow.xaml.cs create mode 100644 FancyInput/Models/EnumDescriptionConverter.cs create mode 100644 FancyInput/Models/HidHideManager.cs create mode 100644 FancyInput/Models/InputGenerator.cs create mode 100644 FancyInput/Models/InputHandler.cs create mode 100644 FancyInput/Models/InputMacro.cs create mode 100644 FancyInput/Models/InputParser.cs create mode 100644 FancyInput/Models/MacroActionPlayer.cs create mode 100644 FancyInput/Models/MacroEnums.cs create mode 100644 FancyInput/Models/OverlayParser.cs create mode 100644 FancyInput/Models/RawInputParser.cs create mode 100644 FancyInput/Models/SimpleProgressConverter.cs create mode 100644 FancyInput/Models/VirtualControllerManager.cs create mode 100644 FancyInput/Models/XImage.cs create mode 100644 FancyInput/Resources/Icons/Icon.ico create mode 100644 FancyInput/Resources/Icons/Icon.png create mode 100644 FancyInput/Resources/QRCodes/bilibili.jpg create mode 100644 FancyInput/Resources/QRCodes/wechat.jpg create mode 100644 FancyInput/Resources/QRCodes/xlworkspace.jpg create mode 100644 FancyInput/Resources/Styles/BorderStyles.xaml create mode 100644 FancyInput/Resources/Styles/ButtonStyles.xaml create mode 100644 FancyInput/Resources/Styles/CheckBoxStyles.xaml create mode 100644 FancyInput/Resources/Styles/ComboBoxStyles.xaml create mode 100644 FancyInput/Resources/Styles/ControlStyles.xaml create mode 100644 FancyInput/Resources/Styles/LabelStyles.xaml create mode 100644 FancyInput/Resources/Styles/MenuStyles.xaml create mode 100644 FancyInput/Resources/Styles/ScrollViewerStyles.xaml create mode 100644 FancyInput/Resources/Styles/SliderStyles.xaml create mode 100644 FancyInput/Resources/Styles/TextBoxStyles.xaml create mode 100644 FancyInput/ViewModels/ElementSegmentViewModel.cs create mode 100644 FancyInput/ViewModels/ElementTreeViewModel.cs create mode 100644 FancyInput/ViewModels/ElementViewModel.Data.cs create mode 100644 FancyInput/ViewModels/ElementViewModel.Display.cs create mode 100644 FancyInput/ViewModels/ElementViewModel.Test.cs create mode 100644 FancyInput/ViewModels/ImageEditorViewModel.cs create mode 100644 FancyInput/ViewModels/MacroActionViewModel.cs create mode 100644 FancyInput/ViewModels/MacroTrackViewModel.cs create mode 100644 FancyInput/ViewModels/MacroViewModel.cs create mode 100644 FancyInput/ViewModels/MacroWindowViewModel.cs create mode 100644 FancyInput/ViewModels/MainWindowViewModel.cs create mode 100644 FancyInput/ViewModels/OverlayWindowViewModel.cs create mode 100644 FancyInput/ViewModels/TerminalViewModel.cs create mode 100644 FancyInput/ViewModels/ViewModelBase.cs create mode 100644 FancyInput/Views/AboutWindow.xaml create mode 100644 FancyInput/Views/AboutWindow.xaml.cs create mode 100644 FancyInput/Views/ConsoleWindow.xaml create mode 100644 FancyInput/Views/ConsoleWindow.xaml.cs create mode 100644 FancyInput/Views/Controls/AnimatedImage.xaml create mode 100644 FancyInput/Views/Controls/AnimatedImage.xaml.cs create mode 100644 FancyInput/Views/Controls/CreatePage.xaml create mode 100644 FancyInput/Views/Controls/CreatePage.xaml.cs create mode 100644 FancyInput/Views/Controls/ElementViewPanel.xaml create mode 100644 FancyInput/Views/Controls/ElementViewPanel.xaml.cs create mode 100644 FancyInput/Views/Controls/IconButton.xaml create mode 100644 FancyInput/Views/Controls/IconButton.xaml.cs create mode 100644 FancyInput/Views/Controls/ManagePage.xaml create mode 100644 FancyInput/Views/Controls/ManagePage.xaml.cs create mode 100644 FancyInput/Views/Controls/OutputPanel.xaml create mode 100644 FancyInput/Views/Controls/OutputPanel.xaml.cs create mode 100644 FancyInput/Views/Controls/RGBColorSelect.xaml create mode 100644 FancyInput/Views/Controls/RGBColorSelect.xaml.cs create mode 100644 FancyInput/Views/Controls/ReadPage.xaml create mode 100644 FancyInput/Views/Controls/ReadPage.xaml.cs create mode 100644 FancyInput/Views/Controls/SettingsPage.xaml create mode 100644 FancyInput/Views/Controls/SettingsPage.xaml.cs create mode 100644 FancyInput/Views/Controls/Terminal.xaml create mode 100644 FancyInput/Views/Controls/Terminal.xaml.cs create mode 100644 FancyInput/Views/Controls/WindowControlBar.xaml create mode 100644 FancyInput/Views/Controls/WindowControlBar.xaml.cs create mode 100644 FancyInput/Views/Controls/WorkshopPanel.xaml create mode 100644 FancyInput/Views/Controls/WorkshopPanel.xaml.cs create mode 100644 FancyInput/Views/ElementTreeHelpWindow.xaml create mode 100644 FancyInput/Views/ElementTreeHelpWindow.xaml.cs create mode 100644 FancyInput/Views/ElementTreeWindow.xaml create mode 100644 FancyInput/Views/ElementTreeWindow.xaml.cs create mode 100644 FancyInput/Views/Elements/AnalogStick.xaml create mode 100644 FancyInput/Views/Elements/AnalogStick.xaml.cs create mode 100644 FancyInput/Views/Elements/ElementBase.cs create mode 100644 FancyInput/Views/Elements/ElementTest.xaml create mode 100644 FancyInput/Views/Elements/ElementTest.xaml.cs create mode 100644 FancyInput/Views/Elements/GamepadButton.xaml create mode 100644 FancyInput/Views/Elements/GamepadButton.xaml.cs create mode 100644 FancyInput/Views/Elements/GamepadDpad.xaml create mode 100644 FancyInput/Views/Elements/GamepadDpad.xaml.cs create mode 100644 FancyInput/Views/Elements/GamepadPlayerId.xaml create mode 100644 FancyInput/Views/Elements/GamepadPlayerId.xaml.cs create mode 100644 FancyInput/Views/Elements/GamepadTrigger.xaml create mode 100644 FancyInput/Views/Elements/GamepadTrigger.xaml.cs create mode 100644 FancyInput/Views/Elements/KeyBoardButton.xaml create mode 100644 FancyInput/Views/Elements/KeyBoardButton.xaml.cs create mode 100644 FancyInput/Views/Elements/MouseButton.xaml create mode 100644 FancyInput/Views/Elements/MouseButton.xaml.cs create mode 100644 FancyInput/Views/Elements/MouseMovement.xaml create mode 100644 FancyInput/Views/Elements/MouseMovement.xaml.cs create mode 100644 FancyInput/Views/Elements/MouseWheel.xaml create mode 100644 FancyInput/Views/Elements/MouseWheel.xaml.cs create mode 100644 FancyInput/Views/Elements/Texture.xaml create mode 100644 FancyInput/Views/Elements/Texture.xaml.cs create mode 100644 FancyInput/Views/FullScreenMask.xaml create mode 100644 FancyInput/Views/FullScreenMask.xaml.cs create mode 100644 FancyInput/Views/ImageEditor.xaml create mode 100644 FancyInput/Views/ImageEditor.xaml.cs create mode 100644 FancyInput/Views/Macro/FormRow.xaml create mode 100644 FancyInput/Views/Macro/FormRow.xaml.cs create mode 100644 FancyInput/Views/Macro/MacroActionPropertyPanel.xaml create mode 100644 FancyInput/Views/Macro/MacroButton.xaml create mode 100644 FancyInput/Views/Macro/MacroButton.xaml.cs create mode 100644 FancyInput/Views/Macro/MacroButtonTemplateSelector.cs create mode 100644 FancyInput/Views/Macro/MacroPropertyTemplateSelector.cs create mode 100644 FancyInput/Views/Macro/MacroTrack.xaml create mode 100644 FancyInput/Views/Macro/MacroTrack.xaml.cs create mode 100644 FancyInput/Views/Macro/MacroWindow.xaml create mode 100644 FancyInput/Views/Macro/MacroWindow.xaml.cs create mode 100644 FancyInput/Views/OpenSourceStatementWindow.xaml create mode 100644 FancyInput/Views/OpenSourceStatementWindow.xaml.cs create mode 100644 FancyInput/Views/OverlayWindow.xaml create mode 100644 FancyInput/Views/OverlayWindow.xaml.cs create mode 100644 FancyInput/Views/QRCodeWindow.xaml create mode 100644 FancyInput/Views/QRCodeWindow.xaml.cs create mode 100644 FancyInput/Views/RgbInputDialog.xaml create mode 100644 FancyInput/Views/RgbInputDialog.xaml.cs create mode 100644 FancyInput/Views/TestWindow.xaml create mode 100644 FancyInput/Views/TestWindow.xaml.cs create mode 100644 FancyInput/Views/UpdatePromptWindow.xaml create mode 100644 FancyInput/Views/UpdatePromptWindow.xaml.cs create mode 100644 FancyInput/Views/UpdateTimelineWindow.xaml create mode 100644 FancyInput/Views/UpdateTimelineWindow.xaml.cs create mode 100644 LICENSE create mode 100644 README.md 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 0000000000000000000000000000000000000000..63411632b92591403d3d984dc1d1a5ccdefdc721 GIT binary patch literal 36380 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 literal 0 HcmV?d00001 diff --git a/FancyInput/Resources/Icons/Icon.png b/FancyInput/Resources/Icons/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..08184ceb916288f3ee54222479bb474a1abbdb3b GIT binary patch literal 53260 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 literal 0 HcmV?d00001 diff --git a/FancyInput/Resources/QRCodes/bilibili.jpg b/FancyInput/Resources/QRCodes/bilibili.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b9fa1bbfe4c47163755bb7d87582d5e4a2ad83f7 GIT binary patch literal 29078 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

H)M@x6$ZKW^W`>OS~TU(H2Zm#*v{n$h@Qj3`X3e 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 literal 0 HcmV?d00001 diff --git a/FancyInput/Resources/QRCodes/xlworkspace.jpg b/FancyInput/Resources/QRCodes/xlworkspace.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dac3941c0350117fb7fe6f0bfb0485dcc628d45a GIT binary patch literal 23075 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⁢lrmnL;lUS_D(pO; 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 g{JpRr}*W< z%?YtyWJO8SyDc~|YYmQgk9k(xnN7P?X!k3c^tov+mN$syIo5muz7=a1g;37r&=d#B z$8?HQFezM9nz)DeG`6r*^_yuRb{fgk=m$D)+zz&%))vQG0eAF#Br(o$M}~1qM~0E{ z0*11n?Zf<<#ARYq2dA0*TAXt}!-@p4iCrF5DQ2s+5PhK#DHL_(Ds*UKKQ|OUxsMEe z-_4|X$^iO=BvHpm7n+xHLYI{*F-<{ajU=1CICQYrW31hIw!Gct62jMZPY=49*8*_( z9<^TgwMZU3L12yXUcQu<=v5n)f@!M%8XZFCQat!C7}9QSFOl7|#m4Q9P|z8TT#Fau z_AWl7i4?1Ty9ZsPiO)t)r|s=sv=u<}blB$i+u2hp&s0xlv*&K+0aFznTc ziJJPa5YqUg9ftA80}X|0CBoJE?kT-gt<=_@yo9U&nk=v`F%7^DZ#*{x$?SQaSoJ`< z3?qC;9R`RBKZJn=JfnpD; ze`yN#IXsD#9n*P)t>LL&4e zNMvd5hU|lM@Zs7(<~6J*>VL0jUHiE4Dx{f8zoOt1mr{CQNfnAW=-%l)A0Kr4{((S& zEu&rC-%+1SwE&d|5DuOx(%=I(%mrTIVaQ$`pznbI%-KteUWZ|=gK)FMS5>}avXg)$ zOMp6I5|atuq1icJ)%2o4`rL^owTpU_?S>r6ZP@Y+6H2lDc0O#bA?xL8f&W_k%-*-v zhP1_>tG;H8F_sJURQ;(n_8%V>5v>`-&Jt;Ur7$ zK|!Ncvm)_8pT6X@IW17yOh;GP>jr(Vi(bi{@?z(uSsa8}Y?2E3=4VlD-r(ANow8aK zGOO%|hgbFPZsm`oiSFER;kTy>u@q^uQdL(8!OtUn5##OtyRl!uA7SmXWyYb9-Y;DO zlZ8CA2TorkJ>Er5W%>;6-s2hL_YXYh5>9gsz7iO(soAIi!p!x4WaFEgo)1+;0&^jC z4$}~DXDVBh?Q&9`hbVl^iN)u88mFk|A?wP&(DHP5{}Ds;eS0hreP*P`!lRVHd;I0HH02zeZR|F`PA%aw%C^} zDO3}rM#1)wB@-f=@+!Goi)GwCowM+p$=$%I9TBVcJ0yeaY7vADZi5(NRlW1V7o?#>%p zPm;TW^}O%U5zSE2cHD6?Cothjd3~wR&$uv%D-bVs(I`gSZl4-JyOCcC$f`!3k!)SG zcDNcg4!+*76%S9$lp2$^qou`YxeHFxlaPOhec-tBRz>8u7Dzt7TFgvudPM`kTk3iv z6`_c#aw8Qjeq81mU0ZbgjmNI1>z--f-FPG?7|rw4w$PAU3JQXhi6ew!=K|D*rA6Mh zsLT2EC2kO6;5gKrkP?mCA;A6uE{P7!5oLbBa%Mq4ZhM)Yr zBgWlHhf*?7AJ`ap!hcXFBLF)1yVf*eYpjTs=Fv}zrb}9r?-2&&)+jK5EHk5_%o6@N z6=0)#%Pn)keZ_vdzxs2RnU7GijU0c#RzowrRcEA=;i{8*2My4c{AnXV^)ez9RVAnw9th=!Jz}*8SLQQc8&r+08!GFQGV)#d$ zu@2X-zhXRvf15NM*AzEDUGei2)@{Y2s5vQy#*jc~-pZY + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +