From aafa5fd5563be0f68b5fd2cb333a5e685a3d884c Mon Sep 17 00:00:00 2001 From: Vermino Date: Tue, 22 Oct 2024 02:03:06 -0700 Subject: [PATCH 1/4] - F3 toggle Z level splicing - ALT + Plus or Minus to go up or down a Z level - Shift hotkey has been added to decrease Z Level --- ACViewer/Config/MapViewerOptions.cs | 3 + ACViewer/Extensions/CommandHandler.cs | 1 + ACViewer/Extensions/Vector3Extensions.cs | 5 ++ ACViewer/Model/VertexInstance.cs | 3 +- ACViewer/Render/Buffer.cs | 93 +++++++++++++++++++++++- ACViewer/Render/Camera.cs | 26 +++++++ ACViewer/Render/GfxObjInstance_Shared.cs | 55 ++++++++++++-- ACViewer/Render/InstanceBatch.cs | 29 +++++++- ACViewer/Render/Render.cs | 34 +++++++-- ACViewer/Render/TerrainBatch.cs | 30 +++++++- 10 files changed, 260 insertions(+), 19 deletions(-) diff --git a/ACViewer/Config/MapViewerOptions.cs b/ACViewer/Config/MapViewerOptions.cs index 6daf1a9..ed40d08 100644 --- a/ACViewer/Config/MapViewerOptions.cs +++ b/ACViewer/Config/MapViewerOptions.cs @@ -5,6 +5,9 @@ namespace ACViewer.Config public class MapViewerOptions { public MapViewerMode Mode { get; set; } + public bool EnableZSlicing { get; set; } = false; + public int CurrentZLevel { get; set; } = 1; + public float LevelHeight { get; set; } = 10.0f; public MapViewerOptions() { diff --git a/ACViewer/Extensions/CommandHandler.cs b/ACViewer/Extensions/CommandHandler.cs index 2ab31e4..3ef602e 100644 --- a/ACViewer/Extensions/CommandHandler.cs +++ b/ACViewer/Extensions/CommandHandler.cs @@ -1,5 +1,6 @@ using System; using System.Windows.Input; +using ACViewer.Config; namespace ACViewer.Extensions { diff --git a/ACViewer/Extensions/Vector3Extensions.cs b/ACViewer/Extensions/Vector3Extensions.cs index 808c1f2..de74dc0 100644 --- a/ACViewer/Extensions/Vector3Extensions.cs +++ b/ACViewer/Extensions/Vector3Extensions.cs @@ -1,11 +1,16 @@ using System; +using Microsoft.Xna.Framework; + +using System.Numerics; + using ACE.Server.Physics; namespace ACViewer { public static class Vector3Extensions { + public static System.Numerics.Vector3 ToNumerics(this Microsoft.Xna.Framework.Vector3 v) { return new System.Numerics.Vector3(v.X, v.Y, v.Z); diff --git a/ACViewer/Model/VertexInstance.cs b/ACViewer/Model/VertexInstance.cs index cbfa891..8c3a6d2 100644 --- a/ACViewer/Model/VertexInstance.cs +++ b/ACViewer/Model/VertexInstance.cs @@ -5,9 +5,10 @@ namespace ACViewer.Model { public struct VertexInstance : IVertexType { - public Vector3 Position; + public Vector3 Position { get; set; } public Vector4 Orientation; public Vector3 Scale; + public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration ( diff --git a/ACViewer/Render/Buffer.cs b/ACViewer/Render/Buffer.cs index 1afad3f..36b524a 100644 --- a/ACViewer/Render/Buffer.cs +++ b/ACViewer/Render/Buffer.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using ACE.DatLoader.Entity; using ACE.Server.Physics; - +using ACViewer.Config; +using ACViewer.Extensions; using ACViewer.Enum; using ACViewer.Model; @@ -86,6 +88,95 @@ public void Init() AnimatedTextureAtlasChains = new Dictionary(); } + + private bool IsInCurrentZLevel(Vector3 position) + { + if (!ConfigManager.Config.MapViewer.EnableZSlicing) + return true; + + var config = ConfigManager.Config.MapViewer; + float levelBottom = (config.CurrentZLevel - 1) * config.LevelHeight; + float levelTop = levelBottom + config.LevelHeight; + + return position.Z >= levelBottom && position.Z < levelTop; + } + + public void DrawWithZSlicing() + { + Effect.Parameters["xWorld"].SetValue(Matrix.Identity); + Effect.Parameters["xLightDirection"].SetValue(-Vector3.UnitZ); + Effect.Parameters["xAmbient"].SetValue(0.5f); + + Effect_Clamp.Parameters["xWorld"].SetValue(Matrix.Identity); + Effect_Clamp.Parameters["xLightDirection"].SetValue(-Vector3.UnitZ); + Effect_Clamp.Parameters["xAmbient"].SetValue(0.5f); + + PerfTimer.Start(ProfilerSection.Draw); + + if (drawTerrain) + { + SetRasterizerState(); + TerrainBatch.DrawWithZFiltering(IsInCurrentZLevel); + } + + if (drawEnvCells) + DrawBufferWithZSlicing(RB_EnvCell, true); + + if (drawStaticObjs) + DrawBufferWithZSlicing(RB_StaticObjs); + + if (drawBuildings) + DrawBufferWithZSlicing(RB_Buildings); + + if (drawScenery) + DrawBufferWithZSlicing(RB_Scenery); + + if (drawInstances && Server.InstancesLoaded) + DrawBufferWithZSlicing(RB_Instances); + + if (drawEncounters && Server.EncountersLoaded) + DrawBufferWithZSlicing(RB_Encounters); + + DrawBufferWithZSlicing(RB_Animated); + + if (Picker.HitVertices != null) + Picker.DrawHitPoly(); + + PerfTimer.Stop(ProfilerSection.Draw); + } + + private void DrawBufferWithZSlicing(Dictionary batches) + { + SetRasterizerState(CullMode.None); + + foreach (var batch in batches.Values) + batch.DrawFiltered(IsInCurrentZLevel); + } + + private void DrawBufferWithZSlicing(Dictionary batches) + { + SetRasterizerState(CullMode.None); + + foreach (var batch in batches.Values) + batch.DrawFiltered(IsInCurrentZLevel); + } + + private void DrawBufferWithZSlicing(Dictionary batches, bool culling = false) + { + var cullMode = WorldViewer.Instance.DungeonMode || culling ? + CullMode.CullClockwiseFace : CullMode.None; + + SetRasterizerState(cullMode); + + Effect.CurrentTechnique = Effect.Techniques["TexturedInstanceEnv"]; + Effect_Clamp.CurrentTechnique = Effect_Clamp.Techniques["TexturedInstanceEnv"]; + + foreach (var batch in batches.Values) + { + batch.DrawFiltered(IsInCurrentZLevel); + } + } + public void ClearBuffer() { diff --git a/ACViewer/Render/Camera.cs b/ACViewer/Render/Camera.cs index e758253..e2a72d8 100644 --- a/ACViewer/Render/Camera.cs +++ b/ACViewer/Render/Camera.cs @@ -295,6 +295,31 @@ public void Update(GameTime gameTime) Position -= Vector3.Cross(Up, Dir) * Speed; if (keyboardState.IsKeyDown(Keys.Space)) Position += Up * Speed; + // Shift key control for downward movement + if (keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift)) + Position -= Up * Speed; + + // Z-level controls + if (keyboardState.IsKeyDown(Keys.F3) && LastKeyboardState != null && !LastKeyboardState.IsKeyDown(Keys.F3)) + { + ConfigManager.Config.MapViewer.EnableZSlicing = !ConfigManager.Config.MapViewer.EnableZSlicing; + ConfigManager.Config.MapViewer.CurrentZLevel = 1; + } + + // Z-level adjustment + if (ConfigManager.Config.MapViewer.EnableZSlicing) + { + if ((keyboardState.IsKeyDown(Keys.LeftAlt) || keyboardState.IsKeyDown(Keys.RightAlt))) + { + var config = ConfigManager.Config.MapViewer; + if (keyboardState.IsKeyDown(Keys.OemPlus) && !LastKeyboardState.IsKeyDown(Keys.OemPlus)) + config.CurrentZLevel = Math.Min(config.CurrentZLevel + 1, 20); + if (keyboardState.IsKeyDown(Keys.OemMinus) && !LastKeyboardState.IsKeyDown(Keys.OemMinus)) + config.CurrentZLevel--; + } + } + + LastKeyboardState = keyboardState; // camera speed control if (mouseState.ScrollWheelValue != PrevMouseState.ScrollWheelValue) @@ -368,6 +393,7 @@ public void Update(GameTime gameTime) //Console.WriteLine("Camera dir: " + GameView.Instance.Render.Camera.Dir); } + private KeyboardState LastKeyboardState; public int centerX => GameView.GraphicsDevice.Viewport.Width / 2; public int centerY => GameView.GraphicsDevice.Viewport.Height / 2; diff --git a/ACViewer/Render/GfxObjInstance_Shared.cs b/ACViewer/Render/GfxObjInstance_Shared.cs index da16e5e..175a00e 100644 --- a/ACViewer/Render/GfxObjInstance_Shared.cs +++ b/ACViewer/Render/GfxObjInstance_Shared.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; - +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -25,9 +26,8 @@ public class GfxObjInstance_Shared public Dictionary BaseFormats_Alpha { get; set; } public List Vertices { get; set; } - + public List Instances { get; set; } - public VertexInstance[] Instances_ { get; set; } public VertexBuffer Shared_VB { get; set; } @@ -39,9 +39,7 @@ public class GfxObjInstance_Shared public GfxObjInstance_Shared(GfxObj gfxObj, Dictionary textureAtlasChains, Dictionary textureChanges = null, PaletteChanges paletteChanges = null) { GfxObj = gfxObj; - BuildStatic(gfxObj, textureAtlasChains, textureChanges, paletteChanges); - Instances = new List(); } @@ -185,6 +183,51 @@ public void Draw() foreach (var baseFormat in BaseFormats_Alpha.Values) baseFormat.Draw(Instances.Count); } + + public void DrawFiltered(Func filter) + { + if (Bindings == null) return; + + if (isDirty) + { + Instances_VB.SetData(Instances_); + isDirty = false; + } + + // Store original instances + var originalInstances = Instances_.ToArray(); + + // Filter instances + var filteredInstances = Instances.Where(instance => filter(instance.Position)).ToArray(); + + if (filteredInstances.Length > 0) + { + // Update vertex buffer with filtered instances + Instances_ = filteredInstances; + Instances_VB.SetData(filteredInstances); + + GraphicsDevice.SetVertexBuffers(Bindings); + + Effect.CurrentTechnique = Effect.Techniques["TexturedInstance"]; + Effect_Clamp.CurrentTechnique = Effect_Clamp.Techniques["TexturedInstance"]; + + foreach (var baseFormat in BaseFormats_Solid.Values) + baseFormat.Draw(filteredInstances.Length); + + if (Buffer.drawAlpha) + { + Effect.CurrentTechnique = Effect.Techniques["TexturedInstanceAlpha"]; + Effect_Clamp.CurrentTechnique = Effect_Clamp.Techniques["TexturedInstanceAlpha"]; + } + + foreach (var baseFormat in BaseFormats_Alpha.Values) + baseFormat.Draw(filteredInstances.Length); + } + + // Restore original instances + Instances_ = originalInstances; + Instances_VB.SetData(originalInstances); + } public void Dispose() { diff --git a/ACViewer/Render/InstanceBatch.cs b/ACViewer/Render/InstanceBatch.cs index 10c413b..b7d363c 100644 --- a/ACViewer/Render/InstanceBatch.cs +++ b/ACViewer/Render/InstanceBatch.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; - +using System; +using System.Collections.Generic; +using System.Linq; using ACE.Entity.Enum; - +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace ACViewer.Render @@ -17,6 +18,28 @@ public class InstanceBatch public VertexBuffer InstanceBuffer { get; set; } public R_Environment R_Environment { get; set; } + + public void DrawFiltered(Func filter) + { + // Store original instances + var originalInstances = new List(Instances_Env); + + // Filter instances based on Z position + Instances_Env = Instances_Env.Where(instance => filter(instance.Position)).ToList(); + + if (Instances_Env.Count > 0) + { + // Rebuild instance buffer with filtered instances + BuildInstanceBuffer(); + BuildBindings(); + Draw(); + } + + // Restore original instances + Instances_Env = originalInstances; + BuildInstanceBuffer(); + BuildBindings(); + } public InstanceBatch(R_EnvCell envCell) { diff --git a/ACViewer/Render/Render.cs b/ACViewer/Render/Render.cs index 110a17a..56d44d2 100644 --- a/ACViewer/Render/Render.cs +++ b/ACViewer/Render/Render.cs @@ -25,6 +25,9 @@ public class Render // multiple SamplerStates in the same .fx file apparently don't work public static Effect Effect_Clamp { get; set; } + + // Add to existing properties + private MapViewerOptions Config => ConfigManager.Config.MapViewer; public Camera Camera { @@ -67,21 +70,23 @@ public void SetRasterizerState(bool wireframe = true) GraphicsDevice.RasterizerState = rs; } - + public void Draw() { GraphicsDevice.Clear(ConfigManager.Config.BackgroundColors.WorldViewer); SetRasterizerState(false); - + Effect.Parameters["xView"].SetValue(Camera.ViewMatrix); Effect_Clamp.Parameters["xView"].SetValue(Camera.ViewMatrix); - //landblock.Draw(); - Buffer.Draw(); + if (ConfigManager.Config.MapViewer.EnableZSlicing) + Buffer.DrawWithZSlicing(); + else + Buffer.Draw(); - //DrawEmitters_Naive(); DrawEmitters_Batch(); + DrawHUD(); } public bool ParticlesInitted { get; set; } @@ -186,14 +191,29 @@ public void DestroyEmitters() private static readonly Vector2 TextPos = new Vector2(10, 10); + // DrawHUD to show Z-slice information public void DrawHUD() { - var cameraPos = GameView.Camera.GetPosition(); + var text = ""; + + if (ConfigManager.Config.MapViewer.EnableZSlicing) + { + var config = ConfigManager.Config.MapViewer; + string levelPrefix = config.CurrentZLevel < 0 ? "B" : ""; // Add "B" prefix for basement levels + int displayLevel = config.CurrentZLevel < 0 ? -config.CurrentZLevel : config.CurrentZLevel; + + text += $"Current Z-Level: {levelPrefix}{displayLevel}\n"; // Shows B1, B2, etc. for basement levels + text += $"Height Range: {(config.CurrentZLevel - 1) * config.LevelHeight:F1}m - {config.CurrentZLevel * config.LevelHeight:F1}m\n"; + } + var cameraPos = Camera.GetPosition(); if (cameraPos != null) + text += $"Location: {cameraPos}"; + + if (!string.IsNullOrEmpty(text)) { SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.LinearClamp); - SpriteBatch.DrawString(Font, $"Location: {cameraPos}", TextPos, Color.White); + SpriteBatch.DrawString(Font, text, TextPos, Color.White); SpriteBatch.End(); } } diff --git a/ACViewer/Render/TerrainBatch.cs b/ACViewer/Render/TerrainBatch.cs index 5615084..7c6b764 100644 --- a/ACViewer/Render/TerrainBatch.cs +++ b/ACViewer/Render/TerrainBatch.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; - +using System.Linq; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace ACViewer.Render @@ -56,6 +57,33 @@ public void Draw() foreach (var batch in Batches) batch.Draw(); } + + public void DrawWithZFiltering(Func filter) + { + Effect.CurrentTechnique = Effect.Techniques["LandscapeSinglePass"]; + + if (OverlayAtlasChain.TextureAtlases.Count > 0) + Effect.Parameters["xOverlays"].SetValue(OverlayAtlasChain.TextureAtlases[0]._Textures); + if (AlphaAtlasChain.TextureAtlases.Count > 0) + Effect.Parameters["xAlphas"].SetValue(AlphaAtlasChain.TextureAtlases[0]._Textures); + + foreach (var batch in Batches) + { + if (batch.Vertices.Count == 0) continue; + + var originalVertices = new List(batch.Vertices); + batch.Vertices = batch.Vertices.Where(v => filter(v.Position)).ToList(); + + if (batch.Vertices.Count > 0) + { + batch.OnCompleted(); + batch.Draw(); + } + + batch.Vertices = originalVertices; + batch.OnCompleted(); + } + } public void Dispose() { From 24cd1a196ac16469303f4920c8abd08ac4794766 Mon Sep 17 00:00:00 2001 From: Vermino Date: Tue, 22 Oct 2024 17:07:51 -0700 Subject: [PATCH 2/4] feat: Add Customizable Keyboard Configuration System Implements a comprehensive keyboard configuration system allowing users to customize and manage keybindings throughout the application. This new system includes a dedicated configuration UI accessible through Tools -> Keyboard Configuration. Core Features: - Configurable bindings for camera and Z-level controls - Multiple predefined keyboard layouts (WASD, ESDF, Arrow keys) - Import/Export functionality for sharing keybinding configurations - Real-time conflict detection and validation - Search functionality to easily find specific bindings - Category-based organization of keybindings Technical Implementation: - New KeyBinding system with modifier key support - Action-based input handling system - JSON-based configuration persistence - Migration system for future config updates - Extensible architecture for adding new bindable actions UI Components: - KeyboardConfig window for managing all bindings - KeyBindingDialog for capturing new key combinations - SearchBox for filtering available bindings - Profile selector for quick layout switching Modified Components: - Camera.cs: Removed hardcoded keys, now using action system - Config.cs: Added KeyBindings support - MainMenu.xaml: Added Keyboard Configuration option - ConfigManager: Added keybinding save/load functionality New Files: - Entity/KeyBinding.cs - Config/KeyBindingConfig.cs - Input/InputManager.cs - Input/KeyRecorder.cs - Model/UserAction.cs - Model/ActionRegistry.cs - Config/ConfigMigration.cs - View/KeyboardConfig.xaml/cs - View/KeyBindingDialog.xaml/cs Testing: - Verified key recording functionality - Tested conflict detection - Validated import/export features - Confirmed proper persistence of settings - Checked migration system functionality Note: This change removes hardcoded key bindings from the Camera class and introduces a more flexible, maintainable approach to input handling throughout the application. --- ACViewer/Commands/RelayCommand.cs | 33 +++ ACViewer/Config/Config.cs | 6 + ACViewer/Config/ConfigManager.cs | 76 ++++-- ACViewer/Config/KeyBindingConfig.cs | 183 ++++++++++++++ ACViewer/Entity/KeyBinding.cs | 43 ++++ ACViewer/Input/BindingValidator.cs | 76 ++++++ ACViewer/Input/InputManager.cs | 81 +++++++ ACViewer/Input/KeyRecorder.cs | 264 ++++++++++++++++++++ ACViewer/Model/ActionRegistry.cs | 56 +++++ ACViewer/Model/UserAction.cs | 21 ++ ACViewer/View/KeyBindingDialog.xaml | 33 +++ ACViewer/View/KeyBindingDialog.xaml.cs | 145 +++++++++++ ACViewer/View/KeyboardConfig.xaml | 120 +++++++++ ACViewer/View/KeyboardConfig.xaml.cs | 324 +++++++++++++++++++++++++ ACViewer/View/MainMenu.xaml | 7 + ACViewer/View/MainMenu.xaml.cs | 48 +++- 16 files changed, 1497 insertions(+), 19 deletions(-) create mode 100644 ACViewer/Commands/RelayCommand.cs create mode 100644 ACViewer/Config/KeyBindingConfig.cs create mode 100644 ACViewer/Entity/KeyBinding.cs create mode 100644 ACViewer/Input/BindingValidator.cs create mode 100644 ACViewer/Input/InputManager.cs create mode 100644 ACViewer/Input/KeyRecorder.cs create mode 100644 ACViewer/Model/ActionRegistry.cs create mode 100644 ACViewer/Model/UserAction.cs create mode 100644 ACViewer/View/KeyBindingDialog.xaml create mode 100644 ACViewer/View/KeyBindingDialog.xaml.cs create mode 100644 ACViewer/View/KeyboardConfig.xaml create mode 100644 ACViewer/View/KeyboardConfig.xaml.cs diff --git a/ACViewer/Commands/RelayCommand.cs b/ACViewer/Commands/RelayCommand.cs new file mode 100644 index 0000000..76aeef6 --- /dev/null +++ b/ACViewer/Commands/RelayCommand.cs @@ -0,0 +1,33 @@ +using System; +using System.Windows.Input; + +namespace ACViewer.Commands +{ + public class RelayCommand : ICommand + { + private readonly Action _execute; + private readonly Func _canExecute; + + public event EventHandler CanExecuteChanged + { + add { CommandManager.RequerySuggested += value; } + remove { CommandManager.RequerySuggested -= value; } + } + + public RelayCommand(Action execute, Func canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public bool CanExecute(object parameter) + { + return _canExecute == null || _canExecute(parameter); + } + + public void Execute(object parameter) + { + _execute(parameter); + } + } +} \ No newline at end of file diff --git a/ACViewer/Config/Config.cs b/ACViewer/Config/Config.cs index d7318cd..10d69f4 100644 --- a/ACViewer/Config/Config.cs +++ b/ACViewer/Config/Config.cs @@ -10,5 +10,11 @@ public class Config public BackgroundColors BackgroundColors { get; set; } = new BackgroundColors(); public string Theme { get; set; } public Mouse Mouse { get; set; } = new Mouse(); + public KeyBindingConfig KeyBindingConfig { get; set; } + + public Config() + { + KeyBindingConfig = new KeyBindingConfig(); + } } } diff --git a/ACViewer/Config/ConfigManager.cs b/ACViewer/Config/ConfigManager.cs index 0d89599..fd85e3d 100644 --- a/ACViewer/Config/ConfigManager.cs +++ b/ACViewer/Config/ConfigManager.cs @@ -1,6 +1,7 @@ -using System; +// File: ACViewer/Config/ConfigManager.cs +using System; using System.IO; - +using System.Windows; using Newtonsoft.Json; namespace ACViewer.Config @@ -8,7 +9,7 @@ namespace ACViewer.Config public static class ConfigManager { private static readonly string Filename = "ACViewer.json"; - + private static readonly string KeybindingsFile = "keybindings.json"; private static Config config { get; set; } public static Config Config @@ -17,7 +18,6 @@ public static Config Config { if (config == null) config = new Config(); - return config; } } @@ -26,11 +26,12 @@ public static Config Config public static void SaveConfig() { - var settings = new JsonSerializerSettings(); - settings.Formatting = Formatting.Indented; - - var json = JsonConvert.SerializeObject(config, settings); + var settings = new JsonSerializerSettings + { + Formatting = Formatting.Indented + }; + var json = JsonConvert.SerializeObject(config, settings); File.WriteAllText(Filename, json); } @@ -41,10 +42,10 @@ public static void LoadConfig() public static Config ReadConfig() { - if (!File.Exists(Filename)) return null; + if (!File.Exists(Filename)) + return null; var json = File.ReadAllText(Filename); - var _config = JsonConvert.DeserializeObject(json); if (_config == null) @@ -52,6 +53,7 @@ public static Config ReadConfig() Console.WriteLine($"ConfigManager.LoadConfig() - failed to parse {Filename}"); return null; } + return _config; } @@ -69,12 +71,54 @@ public static bool HasDBInfo { get { - return config != null && config.Database != null && !string.IsNullOrWhiteSpace(config.Database.Host) && - config.Database.Port > 0 && - !string.IsNullOrWhiteSpace(config.Database.DatabaseName) && - !string.IsNullOrWhiteSpace(config.Database.Username) && - !string.IsNullOrWhiteSpace(config.Database.Password); + return config != null && + config.Database != null && + !string.IsNullOrWhiteSpace(config.Database.Host) && + config.Database.Port > 0 && + !string.IsNullOrWhiteSpace(config.Database.DatabaseName) && + !string.IsNullOrWhiteSpace(config.Database.Username) && + !string.IsNullOrWhiteSpace(config.Database.Password); + } + } + + public static bool SaveKeyBindings() + { + try + { + var json = JsonConvert.SerializeObject(Config.KeyBindingConfig, Formatting.Indented); + File.WriteAllText(KeybindingsFile, json); + return true; + } + catch (Exception ex) + { + MessageBox.Show($"Error saving keybindings: {ex.Message}", "Save Error", + MessageBoxButton.OK, MessageBoxImage.Error); + return false; + } + } + + public static bool LoadKeyBindings() + { + if (!File.Exists(KeybindingsFile)) + return false; + + try + { + var json = File.ReadAllText(KeybindingsFile); + var bindings = JsonConvert.DeserializeObject(json); + if (bindings != null) + { + Config.KeyBindingConfig = bindings; + Config.KeyBindingConfig.ValidateConfig(); + return true; + } + } + catch (Exception ex) + { + MessageBox.Show($"Error loading keybindings: {ex.Message}", "Load Error", + MessageBoxButton.OK, MessageBoxImage.Error); } + return false; } } -} +} \ No newline at end of file diff --git a/ACViewer/Config/KeyBindingConfig.cs b/ACViewer/Config/KeyBindingConfig.cs new file mode 100644 index 0000000..d3b3b4c --- /dev/null +++ b/ACViewer/Config/KeyBindingConfig.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Windows.Input; +using Microsoft.Xna.Framework.Input; +using Newtonsoft.Json; +using ACViewer.Entity; + +namespace ACViewer.Config +{ + public class KeyBindingConfig + { + // Camera Controls + public GameKeyBinding MoveForward { get; set; } + public GameKeyBinding MoveBackward { get; set; } + public GameKeyBinding StrafeLeft { get; set; } + public GameKeyBinding StrafeRight { get; set; } + public GameKeyBinding MoveUp { get; set; } + public GameKeyBinding MoveDown { get; set; } + + // Z-Level Controls + public GameKeyBinding ToggleZLevel { get; set; } + public GameKeyBinding IncreaseZLevel { get; set; } + public GameKeyBinding DecreaseZLevel { get; set; } + + // Custom bindings for extensibility + public Dictionary CustomBindings { get; set; } + private readonly Dictionary _actionBindings = new(); + + public KeyBindingConfig() + { + CustomBindings = new Dictionary(); + SetDefaults(); + } + + public void SetDefaults() + { + // Camera controls + MoveForward = new GameKeyBinding(Keys.W, ModifierKeys.None, "Move Forward", "Camera"); + MoveBackward = new GameKeyBinding(Keys.S, ModifierKeys.None, "Move Backward", "Camera"); + StrafeLeft = new GameKeyBinding(Keys.A, ModifierKeys.None, "Strafe Left", "Camera"); + StrafeRight = new GameKeyBinding(Keys.D, ModifierKeys.None, "Strafe Right", "Camera"); + MoveUp = new GameKeyBinding(Keys.Space, ModifierKeys.None, "Move Up", "Camera"); + MoveDown = new GameKeyBinding(Keys.LeftShift, ModifierKeys.None, "Move Down", "Camera"); + + // Z-Level controls + ToggleZLevel = new GameKeyBinding(Keys.F3, ModifierKeys.None, "Toggle Z-Level", "Z-Level"); + IncreaseZLevel = new GameKeyBinding(Keys.OemPlus, ModifierKeys.Alt, "Increase Z-Level", "Z-Level"); + DecreaseZLevel = new GameKeyBinding(Keys.OemMinus, ModifierKeys.Alt, "Decrease Z-Level", "Z-Level"); + } + + public void ValidateConfig() + { + // Ensure no null bindings + if (MoveForward == null) MoveForward = new GameKeyBinding(); + if (MoveBackward == null) MoveBackward = new GameKeyBinding(); + if (StrafeLeft == null) StrafeLeft = new GameKeyBinding(); + if (StrafeRight == null) StrafeRight = new GameKeyBinding(); + if (MoveUp == null) MoveUp = new GameKeyBinding(); + if (MoveDown == null) MoveDown = new GameKeyBinding(); + if (ToggleZLevel == null) ToggleZLevel = new GameKeyBinding(); + if (IncreaseZLevel == null) IncreaseZLevel = new GameKeyBinding(); + if (DecreaseZLevel == null) DecreaseZLevel = new GameKeyBinding(); + if (CustomBindings == null) CustomBindings = new Dictionary(); + } + + public GameKeyBinding GetBindingForAction(string actionName) + { + switch (actionName) + { + case "Move Forward": return MoveForward; + case "Move Backward": return MoveBackward; + case "Strafe Left": return StrafeLeft; + case "Strafe Right": return StrafeRight; + case "Move Up": return MoveUp; + case "Move Down": return MoveDown; + case "Toggle Z-Level": return ToggleZLevel; + case "Increase Z-Level": return IncreaseZLevel; + case "Decrease Z-Level": return DecreaseZLevel; + } + + if (_actionBindings.TryGetValue(actionName, out var binding)) + return binding; + + return new GameKeyBinding(); + } + + public void SetBindingForAction(string actionName, GameKeyBinding binding) + { + switch (actionName) + { + case "Move Forward": MoveForward = binding; break; + case "Move Backward": MoveBackward = binding; break; + case "Strafe Left": StrafeLeft = binding; break; + case "Strafe Right": StrafeRight = binding; break; + case "Move Up": MoveUp = binding; break; + case "Move Down": MoveDown = binding; break; + case "Toggle Z-Level": ToggleZLevel = binding; break; + case "Increase Z-Level": IncreaseZLevel = binding; break; + case "Decrease Z-Level": DecreaseZLevel = binding; break; + default: + _actionBindings[actionName] = binding; + break; + } + } + + public void ExportToFile(string filePath) + { + try + { + var settings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + TypeNameHandling = TypeNameHandling.Auto + }; + + var json = JsonConvert.SerializeObject(this, settings); + File.WriteAllText(filePath, json); + } + catch (Exception ex) + { + throw new Exception("Failed to export keybindings", ex); + } + } + + public static KeyBindingConfig ImportFromFile(string filePath) + { + try + { + var json = File.ReadAllText(filePath); + var settings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto + }; + + var config = JsonConvert.DeserializeObject(json, settings); + config.ValidateConfig(); + return config; + } + catch (Exception ex) + { + throw new Exception("Failed to import keybindings", ex); + } + } + + public static KeyBindingConfig CreateProfile(string profileType) + { + var config = new KeyBindingConfig(); + + switch (profileType.ToLower()) + { + case "default": + // Already handled by constructor + break; + + case "alternative": + // Alternative ESDF layout + config.MoveForward = new GameKeyBinding(Keys.E, ModifierKeys.None, "Move Forward", "Camera"); + config.MoveBackward = new GameKeyBinding(Keys.D, ModifierKeys.None, "Move Backward", "Camera"); + config.StrafeLeft = new GameKeyBinding(Keys.S, ModifierKeys.None, "Strafe Left", "Camera"); + config.StrafeRight = new GameKeyBinding(Keys.F, ModifierKeys.None, "Strafe Right", "Camera"); + config.MoveUp = new GameKeyBinding(Keys.Space, ModifierKeys.None, "Move Up", "Camera"); + config.MoveDown = new GameKeyBinding(Keys.LeftControl, ModifierKeys.None, "Move Down", "Camera"); + break; + + case "arrows": + // Arrow key layout + config.MoveForward = new GameKeyBinding(Keys.Up, ModifierKeys.None, "Move Forward", "Camera"); + config.MoveBackward = new GameKeyBinding(Keys.Down, ModifierKeys.None, "Move Backward", "Camera"); + config.StrafeLeft = new GameKeyBinding(Keys.Left, ModifierKeys.None, "Strafe Left", "Camera"); + config.StrafeRight = new GameKeyBinding(Keys.Right, ModifierKeys.None, "Strafe Right", "Camera"); + config.MoveUp = new GameKeyBinding(Keys.PageUp, ModifierKeys.None, "Move Up", "Camera"); + config.MoveDown = new GameKeyBinding(Keys.PageDown, ModifierKeys.None, "Move Down", "Camera"); + break; + + default: + throw new ArgumentException($"Unknown profile type: {profileType}"); + } + + return config; + } + } +} \ No newline at end of file diff --git a/ACViewer/Entity/KeyBinding.cs b/ACViewer/Entity/KeyBinding.cs new file mode 100644 index 0000000..8549fd4 --- /dev/null +++ b/ACViewer/Entity/KeyBinding.cs @@ -0,0 +1,43 @@ +using Microsoft.Xna.Framework.Input; +using System; +using System.Windows.Input; + +namespace ACViewer.Entity +{ + public class GameKeyBinding + { + public Keys MainKey { get; set; } + public ModifierKeys Modifiers { get; set; } + public string DisplayName { get; set; } + public string Category { get; set; } + + public GameKeyBinding(Keys mainKey = Keys.None, ModifierKeys modifiers = ModifierKeys.None, string displayName = "", string category = "") + { + MainKey = mainKey; + Modifiers = modifiers; + DisplayName = displayName; + Category = category; + } + + public bool IsEmpty => MainKey == Keys.None; + + public bool Matches(KeyboardState state, ModifierKeys currentModifiers) + { + if (IsEmpty) return false; + if (!state.IsKeyDown(MainKey)) return false; + return currentModifiers == Modifiers; + } + + public string GetDisplayString() + { + if (IsEmpty) return "None"; + var modifierStr = Modifiers != ModifierKeys.None ? $"{Modifiers}+" : ""; + return $"{modifierStr}{MainKey}"; + } + + public GameKeyBinding Clone() + { + return new GameKeyBinding(MainKey, Modifiers, DisplayName, Category); + } + } +} \ No newline at end of file diff --git a/ACViewer/Input/BindingValidator.cs b/ACViewer/Input/BindingValidator.cs new file mode 100644 index 0000000..1f7e70c --- /dev/null +++ b/ACViewer/Input/BindingValidator.cs @@ -0,0 +1,76 @@ +using ACViewer.Entity; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Input; +using Microsoft.Xna.Framework.Input; + +namespace ACViewer.Input +{ + public class BindingValidator + { + private readonly Dictionary> _categoryBindings; + + public BindingValidator() + { + _categoryBindings = new Dictionary>(); + } + + public void RegisterBinding(GameKeyBinding binding) + { + if (!_categoryBindings.ContainsKey(binding.Category)) + _categoryBindings[binding.Category] = new List(); + + _categoryBindings[binding.Category].Add(binding); + } + + public void UnregisterBinding(GameKeyBinding binding) + { + if (_categoryBindings.ContainsKey(binding.Category)) + _categoryBindings[binding.Category].Remove(binding); + } + + public (bool isValid, string error) ValidateBinding(GameKeyBinding newBinding) + { + if (newBinding.IsEmpty) + return (true, null); + + if (IsSystemReserved(newBinding)) + return (false, $"The combination {newBinding.GetDisplayString()} is reserved by the system"); + + var conflicts = FindConflicts(newBinding); + if (conflicts.Any()) + { + var conflictList = string.Join(", ", conflicts.Select(b => $"{b.DisplayName} ({b.GetDisplayString()})")); + return (false, $"Conflicts with existing bindings: {conflictList}"); + } + + return (true, null); + } + + private List FindConflicts(GameKeyBinding binding) + { + var conflicts = new List(); + + foreach (var categoryBindings in _categoryBindings.Values) + { + conflicts.AddRange(categoryBindings.Where(b => + b != binding && + b.MainKey == binding.MainKey && + b.Modifiers == binding.Modifiers)); + } + + return conflicts; + } + + private bool IsSystemReserved(GameKeyBinding binding) + { + if (binding.Modifiers.HasFlag(ModifierKeys.Alt)) + { + if (binding.MainKey == Keys.F4 || binding.MainKey == Keys.Tab) + return true; + } + + return false; + } + } +} diff --git a/ACViewer/Input/InputManager.cs b/ACViewer/Input/InputManager.cs new file mode 100644 index 0000000..967163a --- /dev/null +++ b/ACViewer/Input/InputManager.cs @@ -0,0 +1,81 @@ +using System; +using System.Windows.Input; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using ACViewer.Config; +using ACViewer.Entity; +using GameTime = Microsoft.Xna.Framework.GameTime; +using Keyboard = Microsoft.Xna.Framework.Input.Keyboard; + + +namespace ACViewer.Input +{ + public class InputManager + { + private KeyBindingConfig _config; + private KeyboardState _currentState; + private KeyboardState _previousState; + private ModifierKeys _currentModifiers; + + public event Action BindingPressed; + + public InputManager(KeyBindingConfig config) + { + _config = config; + _currentState = Keyboard.GetState(); + _previousState = _currentState; + } + + public void Update(GameTime gameTime) + { + _previousState = _currentState; + _currentState = Keyboard.GetState(); + UpdateModifiers(); + + CheckBinding(_config.MoveForward); + CheckBinding(_config.MoveBackward); + CheckBinding(_config.StrafeLeft); + CheckBinding(_config.StrafeRight); + CheckBinding(_config.MoveUp); + CheckBinding(_config.MoveDown); + CheckBinding(_config.ToggleZLevel); + CheckBinding(_config.IncreaseZLevel); + CheckBinding(_config.DecreaseZLevel); + + foreach (var binding in _config.CustomBindings.Values) + CheckBinding(binding); + } + + private void CheckBinding(GameKeyBinding binding) + { + if (binding.Matches(_currentState, _currentModifiers)) + BindingPressed?.Invoke(binding); + } + + private void UpdateModifiers() + { + _currentModifiers = ModifierKeys.None; + + if (_currentState.IsKeyDown(Keys.LeftShift) || _currentState.IsKeyDown(Keys.RightShift)) + _currentModifiers |= ModifierKeys.Shift; + + if (_currentState.IsKeyDown(Keys.LeftControl) || _currentState.IsKeyDown(Keys.RightControl)) + _currentModifiers |= ModifierKeys.Control; + + if (_currentState.IsKeyDown(Keys.LeftAlt) || _currentState.IsKeyDown(Keys.RightAlt)) + _currentModifiers |= ModifierKeys.Alt; + } + + public bool IsBindingActive(GameKeyBinding binding) + { + return binding.Matches(_currentState, _currentModifiers); + } + + public void UpdateBindings(KeyBindingConfig newConfig) + { + _config = newConfig; + } + + public ModifierKeys GetCurrentModifiers() => _currentModifiers; + } +} \ No newline at end of file diff --git a/ACViewer/Input/KeyRecorder.cs b/ACViewer/Input/KeyRecorder.cs new file mode 100644 index 0000000..25add42 --- /dev/null +++ b/ACViewer/Input/KeyRecorder.cs @@ -0,0 +1,264 @@ +using System; +using System.Windows.Input; +using System.Windows.Threading; +using System.Windows; +using Microsoft.Xna.Framework.Input; +using ACViewer.Entity; +using Keyboard = System.Windows.Input.Keyboard; +using Keys = Microsoft.Xna.Framework.Input.Keys; + +namespace ACViewer.Input +{ + public class KeyRecorder + { + private bool _isRecording; + private ModifierKeys _lastModifiers; + private Window _parentWindow; + private readonly DispatcherTimer _timer; + private int _remainingTime; + private const int TIMEOUT_SECONDS = 3; // Reduced to 3 seconds + + public event Action OnKeyStateChanged; + public event Action RecordingComplete; + public event Action RecordingCancelled; + public event Action RecordingError; + + public KeyRecorder() + { + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(1) + }; + _timer.Tick += Timer_Tick; + } + + private void Timer_Tick(object sender, EventArgs e) + { + _remainingTime--; + System.Diagnostics.Debug.WriteLine($"Timer tick: {_remainingTime} seconds remaining"); + + if (_remainingTime <= 0) + { + // If only modifiers are held, use them as the key + if (_lastModifiers != ModifierKeys.None) + { + System.Diagnostics.Debug.WriteLine($"Timeout with modifiers: {_lastModifiers}"); + var modifierKey = GetModifierAsKey(_lastModifiers); + if (modifierKey != Keys.None) + { + var binding = new GameKeyBinding(modifierKey, ModifierKeys.None); + StopRecording(); + RecordingComplete?.Invoke(binding); + return; + } + } + + StopRecording(); + RecordingCancelled?.Invoke(); + } + } + + public void StartRecording(Window parentWindow) + { + try + { + if (_isRecording) return; + + System.Diagnostics.Debug.WriteLine("Starting key recording..."); + _isRecording = true; + _parentWindow = parentWindow; + _remainingTime = TIMEOUT_SECONDS; + _lastModifiers = ModifierKeys.None; + + _parentWindow.PreviewKeyDown += Window_PreviewKeyDown; + _parentWindow.PreviewKeyUp += Window_PreviewKeyUp; + _timer.Start(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error starting recording: {ex}"); + RecordingError?.Invoke($"Error starting recording: {ex.Message}"); + } + } + + public void StopRecording() + { + if (!_isRecording) return; + + System.Diagnostics.Debug.WriteLine("Stopping key recording..."); + _isRecording = false; + _timer.Stop(); + + if (_parentWindow != null) + { + _parentWindow.PreviewKeyDown -= Window_PreviewKeyDown; + _parentWindow.PreviewKeyUp -= Window_PreviewKeyUp; + _parentWindow = null; + } + } + + private void Window_PreviewKeyUp(object sender, KeyEventArgs e) + { + if (!_isRecording) return; + + _lastModifiers = Keyboard.Modifiers; + OnKeyStateChanged?.Invoke(Keys.None, _lastModifiers); + e.Handled = true; + } + + private void Window_PreviewKeyDown(object sender, KeyEventArgs e) + { + if (!_isRecording) return; + + System.Diagnostics.Debug.WriteLine($"Key pressed: {e.Key}, System Key: {e.SystemKey}, Modifiers: {Keyboard.Modifiers}"); + + _lastModifiers = Keyboard.Modifiers; + + // Handle escape to cancel + if (e.Key == Key.Escape) + { + e.Handled = true; + StopRecording(); + RecordingCancelled?.Invoke(); + return; + } + + // Handle system keys (like Alt+ combinations) + var actualKey = e.Key == Key.System ? e.SystemKey : e.Key; + + // If it's a modifier key alone, just update the display + if (IsModifierKey(actualKey)) + { + OnKeyStateChanged?.Invoke(Keys.None, _lastModifiers); + e.Handled = true; + return; + } + + // Convert WPF key to XNA key + var xnaKey = ConvertToXNAKey(actualKey); + if (xnaKey != Keys.None) + { + var binding = new GameKeyBinding(xnaKey, _lastModifiers); + if (ValidateBinding(binding)) + { + e.Handled = true; + StopRecording(); + RecordingComplete?.Invoke(binding); + } + } + + OnKeyStateChanged?.Invoke(xnaKey, _lastModifiers); + e.Handled = true; + } + + private Keys GetModifierAsKey(ModifierKeys modifier) + { + // Return the specific modifier key that was pressed + switch (modifier) + { + case ModifierKeys.Shift: + return Keyboard.IsKeyDown(Key.LeftShift) ? Keys.LeftShift : Keys.RightShift; + case ModifierKeys.Control: + return Keyboard.IsKeyDown(Key.LeftCtrl) ? Keys.LeftControl : Keys.RightControl; + case ModifierKeys.Alt: + return Keyboard.IsKeyDown(Key.LeftAlt) ? Keys.LeftAlt : Keys.RightAlt; + default: + return Keys.None; + } + } + + private bool IsModifierKey(Key key) + { + return key == Key.LeftShift || key == Key.RightShift || + key == Key.LeftCtrl || key == Key.RightCtrl || + key == Key.LeftAlt || key == Key.RightAlt; + } + + private bool ValidateBinding(GameKeyBinding binding) + { + if (binding.Modifiers.HasFlag(ModifierKeys.Alt) && binding.MainKey == Keys.F4) + { + RecordingError?.Invoke("Alt+F4 is reserved by the system"); + return false; + } + + return true; + } + + private Keys ConvertToXNAKey(Key wpfKey) + { + // Handle system keys (Alt+ combinations) + if (wpfKey == Key.System) + return Keys.None; + + // This is a basic conversion - add more cases as needed + return wpfKey switch + { + Key.A => Keys.A, + Key.B => Keys.B, + Key.C => Keys.C, + Key.D => Keys.D, + Key.E => Keys.E, + Key.F => Keys.F, + Key.G => Keys.G, + Key.H => Keys.H, + Key.I => Keys.I, + Key.J => Keys.J, + Key.K => Keys.K, + Key.L => Keys.L, + Key.M => Keys.M, + Key.N => Keys.N, + Key.O => Keys.O, + Key.P => Keys.P, + Key.Q => Keys.Q, + Key.R => Keys.R, + Key.S => Keys.S, + Key.T => Keys.T, + Key.U => Keys.U, + Key.V => Keys.V, + Key.W => Keys.W, + Key.X => Keys.X, + Key.Y => Keys.Y, + Key.Z => Keys.Z, + Key.D0 => Keys.D0, + Key.D1 => Keys.D1, + Key.D2 => Keys.D2, + Key.D3 => Keys.D3, + Key.D4 => Keys.D4, + Key.D5 => Keys.D5, + Key.D6 => Keys.D6, + Key.D7 => Keys.D7, + Key.D8 => Keys.D8, + Key.D9 => Keys.D9, + Key.Space => Keys.Space, + Key.Enter => Keys.Enter, + Key.Tab => Keys.Tab, + Key.Up => Keys.Up, + Key.Down => Keys.Down, + Key.Left => Keys.Left, + Key.Right => Keys.Right, + Key.F1 => Keys.F1, + Key.F2 => Keys.F2, + Key.F3 => Keys.F3, + Key.F4 => Keys.F4, + Key.F5 => Keys.F5, + Key.F6 => Keys.F6, + Key.F7 => Keys.F7, + Key.F8 => Keys.F8, + Key.F9 => Keys.F9, + Key.F10 => Keys.F10, + Key.F11 => Keys.F11, + Key.F12 => Keys.F12, + Key.OemPlus => Keys.OemPlus, + Key.OemMinus => Keys.OemMinus, + Key.LeftShift => Keys.LeftShift, + Key.RightShift => Keys.RightShift, + Key.LeftCtrl => Keys.LeftControl, + Key.RightCtrl => Keys.RightControl, + Key.LeftAlt => Keys.LeftAlt, + Key.RightAlt => Keys.RightAlt, + _ => Keys.None + }; + } + } +} \ No newline at end of file diff --git a/ACViewer/Model/ActionRegistry.cs b/ACViewer/Model/ActionRegistry.cs new file mode 100644 index 0000000..2e07444 --- /dev/null +++ b/ACViewer/Model/ActionRegistry.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; + +namespace ACViewer.Model +{ + public class ActionRegistry + { + private static ActionRegistry _instance; + public static ActionRegistry Instance => _instance ??= new ActionRegistry(); + + private readonly Dictionary _actions = new(); + private readonly Dictionary> _categories = new(); + + public void RegisterAction(UserAction action) + { + _actions[action.Name] = action; + + if (!_categories.ContainsKey(action.Category)) + _categories[action.Category] = new List(); + + _categories[action.Category].Add(action.Name); + } + + public UserAction GetAction(string name) + { + return _actions.TryGetValue(name, out var action) ? action : null; + } + + public IReadOnlyList GetCategories() + { + return _categories.Keys.ToList(); + } + + public IReadOnlyList GetActionsInCategory(string category) + { + if (!_categories.ContainsKey(category)) + return new List(); + + return _categories[category] + .Select(name => _actions[name]) + .ToList(); + } + + public void ExecuteAction(string name) + { + if (_actions.TryGetValue(name, out var action) && action.IsEnabled) + action.ExecuteAction?.Invoke(); + } + + public void Clear() + { + _actions.Clear(); + _categories.Clear(); + } + } +} \ No newline at end of file diff --git a/ACViewer/Model/UserAction.cs b/ACViewer/Model/UserAction.cs new file mode 100644 index 0000000..bb48c5d --- /dev/null +++ b/ACViewer/Model/UserAction.cs @@ -0,0 +1,21 @@ +using System; + +namespace ACViewer.Model +{ + public class UserAction + { + public string Name { get; set; } + public string Category { get; set; } + public Action ExecuteAction { get; set; } + public string Description { get; set; } + public bool IsEnabled { get; set; } = true; + + public UserAction(string name, string category, Action executeAction, string description = "") + { + Name = name; + Category = category; + ExecuteAction = executeAction; + Description = description; + } + } +} \ No newline at end of file diff --git a/ACViewer/View/KeyBindingDialog.xaml b/ACViewer/View/KeyBindingDialog.xaml new file mode 100644 index 0000000..34b21e4 --- /dev/null +++ b/ACViewer/View/KeyBindingDialog.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ACViewer/View/KeyBindingDialog.xaml.cs b/ACViewer/View/KeyBindingDialog.xaml.cs new file mode 100644 index 0000000..097b6a5 --- /dev/null +++ b/ACViewer/View/KeyBindingDialog.xaml.cs @@ -0,0 +1,145 @@ +using System; +using System.ComponentModel; +using System.Windows.Input; +using System.Windows; +using System.Windows.Threading; +using Microsoft.Xna.Framework.Input; +using ACViewer.Entity; +using ACViewer.Input; +using MessageBox = System.Windows.MessageBox; + +namespace ACViewer.View +{ + public partial class KeyBindingDialog : Window + { + private readonly DispatcherTimer _timer; + private int _remainingTime; + private const int TIMEOUT_SECONDS = 5; + private readonly KeyRecorder _recorder; + private bool _isClosing; + + public GameKeyBinding ResultBinding { get; private set; } + + public KeyBindingDialog() + { + InitializeComponent(); + + _remainingTime = TIMEOUT_SECONDS; + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(1) + }; + _timer.Tick += Timer_Tick; + + _recorder = new KeyRecorder(); + _recorder.OnKeyStateChanged += Recorder_KeyStateChanged; + _recorder.RecordingComplete += Recorder_RecordingComplete; + _recorder.RecordingCancelled += Recorder_RecordingCancelled; + _recorder.RecordingError += Recorder_RecordingError; + + Loaded += KeyBindingDialog_Loaded; + Closing += KeyBindingDialog_Closing; + } + + private void KeyBindingDialog_Loaded(object sender, RoutedEventArgs e) + { + UpdateTimeoutText(); + _timer.Start(); + _recorder.StartRecording(this); + } + + private void KeyBindingDialog_Closing(object sender, CancelEventArgs e) + { + _isClosing = true; + _timer.Stop(); + _recorder.StopRecording(); + } + + private void Recorder_KeyStateChanged(Keys mainKey, ModifierKeys modifiers) + { + Dispatcher.BeginInvoke(new Action(() => + { + string displayText; + if (mainKey == Keys.None) + { + if (modifiers != ModifierKeys.None) + displayText = $"{modifiers}+"; + else + displayText = $"Waiting for input... ({_remainingTime}s)"; + } + else + { + var modifierStr = modifiers != ModifierKeys.None ? $"{modifiers}+" : ""; + displayText = $"{modifierStr}{mainKey}"; + } + + txtCurrentKeys.Text = displayText; + })); + } + + private void Timer_Tick(object sender, EventArgs e) + { + _remainingTime--; + UpdateTimeoutText(); + + if (_remainingTime <= 0) + { + _timer.Stop(); + SafeClose(false); + } + } + + private void UpdateTimeoutText() + { + Dispatcher.BeginInvoke(new Action(() => + { + txtCurrentKeys.Text = $"Waiting for input... ({_remainingTime}s)"; + })); + } + + private void SafeClose(bool? dialogResult) + { + if (!_isClosing) + { + _isClosing = true; + DialogResult = dialogResult; + Close(); + } + } + + private void Recorder_RecordingComplete(GameKeyBinding binding) + { + Dispatcher.BeginInvoke(new Action(() => + { + if (!_isClosing) + { + ResultBinding = binding; + SafeClose(true); + } + })); + } + + private void Recorder_RecordingCancelled() + { + Dispatcher.BeginInvoke(new Action(() => + { + if (!_isClosing) + { + SafeClose(false); + } + })); + } + + private void Recorder_RecordingError(string error) + { + Dispatcher.BeginInvoke(new Action(() => + { + if (!_isClosing) + { + MessageBox.Show(this, error, "Error", MessageBoxButton.OK, MessageBoxImage.Error); + SafeClose(false); + } + })); + } + } +} \ No newline at end of file diff --git a/ACViewer/View/KeyboardConfig.xaml b/ACViewer/View/KeyboardConfig.xaml new file mode 100644 index 0000000..0948c12 --- /dev/null +++ b/ACViewer/View/KeyboardConfig.xaml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +