using AIStudio.Components; using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; namespace AIStudio.Dialogs; /// /// A dialog for capturing and configuring keyboard shortcuts. /// public partial class ShortcutDialog : MSGComponentBase { [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; [Inject] private RustService RustService { get; init; } = null!; /// /// The initial shortcut value (in internal format, e.g., "CmdOrControl+1"). /// [Parameter] public string InitialShortcut { get; set; } = string.Empty; /// /// The identifier of the shortcut for conflict detection. /// [Parameter] public Shortcut ShortcutId { get; set; } private static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); private string currentShortcut = string.Empty; private string originalShortcut = string.Empty; private string validationMessage = string.Empty; private Severity validationSeverity = Severity.Info; private bool hasValidationError; // // Current key state: // private bool hasCtrl; private bool hasShift; private bool hasAlt; private bool hasMeta; private string? currentKey; private MudTextField? inputField; #region Overrides of ComponentBase protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); // Configure the spellchecking for the user input: this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); this.currentShortcut = this.InitialShortcut; this.originalShortcut = this.InitialShortcut; this.ParseExistingShortcut(); } #endregion private string ShowText => string.IsNullOrWhiteSpace(this.currentShortcut) ? T("Press a key combination...") : this.GetDisplayShortcut(); private void ParseExistingShortcut() { if (string.IsNullOrWhiteSpace(this.currentShortcut)) return; // Parse the existing shortcut to set the state var parts = this.currentShortcut.Split('+'); foreach (var part in parts) { switch (part.ToLowerInvariant()) { case "cmdorcontrol": case "commandorcontrol": case "ctrl": case "control": case "cmd": case "command": this.hasCtrl = true; break; case "shift": this.hasShift = true; break; case "alt": this.hasAlt = true; break; case "meta": case "super": this.hasMeta = true; break; default: this.currentKey = part; break; } } } private async Task HandleKeyDown(KeyboardEventArgs e) { // Ignore pure modifier key presses: if (IsModifierKey(e.Code)) { this.UpdateModifiers(e); this.currentKey = null; this.UpdateShortcutString(); return; } this.UpdateModifiers(e); // Get the key: this.currentKey = TranslateKeyCode(e.Code); // Validate: must have at least one modifier + a key if (!this.hasCtrl && !this.hasShift && !this.hasAlt && !this.hasMeta) { this.validationMessage = T("Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd)."); this.validationSeverity = Severity.Warning; this.hasValidationError = true; this.StateHasChanged(); return; } this.UpdateShortcutString(); await this.ValidateShortcut(); this.StateHasChanged(); } private void UpdateModifiers(KeyboardEventArgs e) { this.hasCtrl = e.CtrlKey || e.MetaKey; // Treat Meta (Cmd on Mac) same as Ctrl for cross-platform this.hasShift = e.ShiftKey; this.hasAlt = e.AltKey; this.hasMeta = e is { MetaKey: true, CtrlKey: false }; // Only set meta if not already using ctrl } private void UpdateShortcutString() { var parts = new List(); if (this.hasCtrl) parts.Add("CmdOrControl"); if (this.hasShift) parts.Add("Shift"); if (this.hasAlt) parts.Add("Alt"); if (!string.IsNullOrWhiteSpace(this.currentKey)) parts.Add(this.currentKey); this.currentShortcut = parts.Count > 0 ? string.Join("+", parts) : string.Empty; this.StateHasChanged(); } private async Task ValidateShortcut() { if (string.IsNullOrWhiteSpace(this.currentShortcut) || string.IsNullOrWhiteSpace(this.currentKey)) { this.validationMessage = string.Empty; this.hasValidationError = false; return; } // Check if the shortcut is valid by trying to register it with Rust var result = await this.RustService.ValidateShortcut(this.currentShortcut); if (result.IsValid) { if (!string.IsNullOrWhiteSpace(this.originalShortcut) && this.currentShortcut.Equals(this.originalShortcut, StringComparison.OrdinalIgnoreCase)) { this.validationMessage = T("This is the shortcut you previously used."); this.validationSeverity = Severity.Info; this.hasValidationError = false; this.StateHasChanged(); return; } if (result.HasConflict) { this.validationMessage = string.Format(T("This shortcut conflicts with: {0}"), result.ConflictDescription); this.validationSeverity = Severity.Warning; this.hasValidationError = false; // Allow saving, but warn } else { this.validationMessage = T("Shortcut is valid and available."); this.validationSeverity = Severity.Success; this.hasValidationError = false; } } else { this.validationMessage = string.Format(T("Invalid shortcut: {0}"), result.ErrorMessage); this.validationSeverity = Severity.Error; this.hasValidationError = true; } this.StateHasChanged(); } private string GetDisplayShortcut() { // Convert internal format to display format: return this.currentShortcut .Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl") .Replace("CommandOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl"); } private void ClearShortcut() { this.currentShortcut = string.Empty; this.currentKey = null; this.hasCtrl = false; this.hasShift = false; this.hasAlt = false; this.hasMeta = false; this.validationMessage = string.Empty; this.hasValidationError = false; this.StateHasChanged(); } private void Cancel() => this.MudDialog.Cancel(); private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.currentShortcut)); /// /// Checks if the key code represents a modifier key. /// private static bool IsModifierKey(string code) => code switch { "ShiftLeft" or "ShiftRight" => true, "ControlLeft" or "ControlRight" => true, "AltLeft" or "AltRight" => true, "MetaLeft" or "MetaRight" => true, _ => false, }; /// /// Translates a JavaScript KeyboardEvent.code to Tauri shortcut format. /// private static string TranslateKeyCode(string code) => code switch { // Letters "KeyA" => "A", "KeyB" => "B", "KeyC" => "C", "KeyD" => "D", "KeyE" => "E", "KeyF" => "F", "KeyG" => "G", "KeyH" => "H", "KeyI" => "I", "KeyJ" => "J", "KeyK" => "K", "KeyL" => "L", "KeyM" => "M", "KeyN" => "N", "KeyO" => "O", "KeyP" => "P", "KeyQ" => "Q", "KeyR" => "R", "KeyS" => "S", "KeyT" => "T", "KeyU" => "U", "KeyV" => "V", "KeyW" => "W", "KeyX" => "X", "KeyY" => "Y", "KeyZ" => "Z", // Numbers "Digit0" => "0", "Digit1" => "1", "Digit2" => "2", "Digit3" => "3", "Digit4" => "4", "Digit5" => "5", "Digit6" => "6", "Digit7" => "7", "Digit8" => "8", "Digit9" => "9", // Function keys "F1" => "F1", "F2" => "F2", "F3" => "F3", "F4" => "F4", "F5" => "F5", "F6" => "F6", "F7" => "F7", "F8" => "F8", "F9" => "F9", "F10" => "F10", "F11" => "F11", "F12" => "F12", "F13" => "F13", "F14" => "F14", "F15" => "F15", "F16" => "F16", "F17" => "F17", "F18" => "F18", "F19" => "F19", "F20" => "F20", "F21" => "F21", "F22" => "F22", "F23" => "F23", "F24" => "F24", // Special keys "Space" => "Space", "Enter" => "Enter", "Tab" => "Tab", "Escape" => "Escape", "Backspace" => "Backspace", "Delete" => "Delete", "Insert" => "Insert", "Home" => "Home", "End" => "End", "PageUp" => "PageUp", "PageDown" => "PageDown", // Arrow keys "ArrowUp" => "Up", "ArrowDown" => "Down", "ArrowLeft" => "Left", "ArrowRight" => "Right", // Numpad "Numpad0" => "Num0", "Numpad1" => "Num1", "Numpad2" => "Num2", "Numpad3" => "Num3", "Numpad4" => "Num4", "Numpad5" => "Num5", "Numpad6" => "Num6", "Numpad7" => "Num7", "Numpad8" => "Num8", "Numpad9" => "Num9", "NumpadAdd" => "NumAdd", "NumpadSubtract" => "NumSubtract", "NumpadMultiply" => "NumMultiply", "NumpadDivide" => "NumDivide", "NumpadDecimal" => "NumDecimal", "NumpadEnter" => "NumEnter", // Punctuation "Minus" => "Minus", "Equal" => "Equal", "BracketLeft" => "BracketLeft", "BracketRight" => "BracketRight", "Backslash" => "Backslash", "Semicolon" => "Semicolon", "Quote" => "Quote", "Backquote" => "Backquote", "Comma" => "Comma", "Period" => "Period", "Slash" => "Slash", // Default: return as-is _ => code, }; private void HandleBlur() { // Re-focus the input field to keep capturing keys: this.inputField?.FocusAsync(); } }