mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 19:27:56 +01:00
[Keyboard Manager] New Keyboard Manager - Text Remapping Page and Code-behind functionalities (#39888)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request > [Target feature branch ``feature/KeyboardManagerWinUI3``] Refactor the New Keyboard Manager code to generalize functions (like Keyboard Hook) for use across pages. Implement Code-behind and the Text Remapping Page. - Implement the Text Remapping page - Restructure the Key Remappings code - Implement unified Keyboard Hook for all Pages - Implement Save and Delete for Text Page - Adjust the Text Page list UI - Create Input Control for Text Page - Validate the user input in Text Page and show teaching tip for the validation error - Move Remapping Saving into Helper - Move Remapping Delete into Helper - Don't check for duplicates if the original keys are the same as the remapping being edited - Use InputMode to indicate the active toggle for Original Keys/Remapped Keys - Add Rectangle for all pages      
This commit is contained in:
@@ -515,12 +515,26 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
|
||||
bool AddShortcutRemap(void* config,
|
||||
const wchar_t* originalKeys,
|
||||
const wchar_t* targetKeys,
|
||||
const wchar_t* targetApp)
|
||||
const wchar_t* targetApp,
|
||||
int operationType = 0)
|
||||
{
|
||||
auto mappingConfig = static_cast<MappingConfiguration*>(config);
|
||||
|
||||
Shortcut originalShortcut(originalKeys);
|
||||
Shortcut targetShortcut(targetKeys);
|
||||
|
||||
KeyShortcutTextUnion targetShortcut;
|
||||
|
||||
switch (operationType)
|
||||
{
|
||||
case 3:
|
||||
targetShortcut = std::wstring(targetKeys);
|
||||
break;
|
||||
|
||||
default:
|
||||
targetShortcut = Shortcut(targetKeys);
|
||||
std::get<Shortcut>(targetShortcut).operationType = static_cast<Shortcut::OperationType>(operationType);
|
||||
break;
|
||||
}
|
||||
|
||||
std::wstring app(targetApp ? targetApp : L"");
|
||||
|
||||
@@ -609,6 +623,18 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DeleteSingleKeyToTextRemap(void* config, int originalKey)
|
||||
{
|
||||
auto mappingConfig = static_cast<MappingConfiguration*>(config);
|
||||
auto it = mappingConfig->singleKeyToTextReMap.find(originalKey);
|
||||
if (it != mappingConfig->singleKeyToTextReMap.end())
|
||||
{
|
||||
mappingConfig->singleKeyToTextReMap.erase(it);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to delete a shortcut remapping
|
||||
bool DeleteShortcutRemap(void* config, const wchar_t* originalKeys, const wchar_t* targetApp)
|
||||
{
|
||||
@@ -659,20 +685,6 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
|
||||
}
|
||||
}
|
||||
|
||||
// Test function to call the remapping helper function
|
||||
bool CheckIfRemappingsAreValid()
|
||||
{
|
||||
RemapBuffer remapBuffer;
|
||||
|
||||
// Mock valid key to key remappings
|
||||
remapBuffer.push_back(RemapBufferRow{ RemapBufferItem({ (DWORD)0x41, (DWORD)0x42 }), std::wstring() });
|
||||
remapBuffer.push_back(RemapBufferRow{ RemapBufferItem({ (DWORD)0x42, (DWORD)0x43 }), std::wstring() });
|
||||
|
||||
auto result = LoadingAndSavingRemappingHelper::CheckIfRemappingsAreValid(remapBuffer);
|
||||
|
||||
return result == ShortcutErrorType::NoError;
|
||||
}
|
||||
|
||||
// Get the list of keyboard keys in Editor
|
||||
int GetKeyboardKeysList(bool isShortcut, KeyNamePair* keyList, int maxCount)
|
||||
{
|
||||
|
||||
@@ -62,7 +62,8 @@ extern "C"
|
||||
__declspec(dllexport) bool AddShortcutRemap(void* config,
|
||||
const wchar_t* originalKeys,
|
||||
const wchar_t* targetKeys,
|
||||
const wchar_t* targetApp);
|
||||
const wchar_t* targetApp,
|
||||
int operationType);
|
||||
|
||||
__declspec(dllexport) void GetKeyDisplayName(int keyCode, wchar_t* keyName, int maxCount);
|
||||
__declspec(dllexport) int GetKeyCodeFromName(const wchar_t* keyName);
|
||||
@@ -73,7 +74,7 @@ extern "C"
|
||||
__declspec(dllexport) bool AreShortcutsEqual(const wchar_t* lShort, const wchar_t* rShort);
|
||||
|
||||
__declspec(dllexport) bool DeleteSingleKeyRemap(void* config, int originalKey);
|
||||
__declspec(dllexport) bool DeleteSingleKeyToTextRemap(void* config, int originalKey);
|
||||
__declspec(dllexport) bool DeleteShortcutRemap(void* config, const wchar_t* originalKeys, const wchar_t* targetApp);
|
||||
}
|
||||
extern "C" __declspec(dllexport) bool CheckIfRemappingsAreValid();
|
||||
extern "C" __declspec(dllexport) int GetKeyboardKeysList(bool isShortcut, KeyNamePair* keyList, int maxCount);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace KeyboardManagerEditorUI.Helpers
|
||||
{
|
||||
public enum KeyInputMode
|
||||
{
|
||||
OriginalKeys,
|
||||
RemappedKeys,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using KeyboardManagerEditorUI.Interop;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Windows.System;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Helpers
|
||||
{
|
||||
public class KeyboardHookHelper : IDisposable
|
||||
{
|
||||
private static KeyboardHookHelper? _instance;
|
||||
|
||||
public static KeyboardHookHelper Instance => _instance ??= new KeyboardHookHelper();
|
||||
|
||||
private KeyboardMappingService _mappingService;
|
||||
|
||||
private HotkeySettingsControlHook? _keyboardHook;
|
||||
|
||||
// The active page using this keyboard hook
|
||||
private IKeyboardHookTarget? _activeTarget;
|
||||
|
||||
private HashSet<VirtualKey> _currentlyPressedKeys = new();
|
||||
private List<VirtualKey> _keyPressOrder = new();
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
// Singleton to make sure only one instance of the hook is active
|
||||
private KeyboardHookHelper()
|
||||
{
|
||||
_mappingService = new KeyboardMappingService();
|
||||
}
|
||||
|
||||
public void ActivateHook(IKeyboardHookTarget target)
|
||||
{
|
||||
CleanupHook();
|
||||
|
||||
_activeTarget = target;
|
||||
|
||||
_currentlyPressedKeys.Clear();
|
||||
_keyPressOrder.Clear();
|
||||
|
||||
_keyboardHook = new HotkeySettingsControlHook(
|
||||
KeyDown,
|
||||
KeyUp,
|
||||
() => true,
|
||||
(key, extraInfo) => true);
|
||||
}
|
||||
|
||||
public void CleanupHook()
|
||||
{
|
||||
if (_keyboardHook != null)
|
||||
{
|
||||
_keyboardHook.Dispose();
|
||||
_keyboardHook = null;
|
||||
}
|
||||
|
||||
_currentlyPressedKeys.Clear();
|
||||
_keyPressOrder.Clear();
|
||||
_activeTarget = null;
|
||||
}
|
||||
|
||||
private void KeyDown(int key)
|
||||
{
|
||||
if (_activeTarget == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VirtualKey virtualKey = (VirtualKey)key;
|
||||
|
||||
if (_currentlyPressedKeys.Contains(virtualKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// if no keys are pressed, clear the lists when a new key is pressed
|
||||
if (_currentlyPressedKeys.Count == 0)
|
||||
{
|
||||
_activeTarget.ClearKeys();
|
||||
_keyPressOrder.Clear();
|
||||
}
|
||||
|
||||
// Count current modifiers
|
||||
int modifierCount = _currentlyPressedKeys.Count(k => RemappingHelper.IsModifierKey(k));
|
||||
|
||||
// If adding this key would exceed the limits (4 modifiers + 1 action key), don't add it and show notification
|
||||
if ((RemappingHelper.IsModifierKey(virtualKey) && modifierCount >= 4) ||
|
||||
(!RemappingHelper.IsModifierKey(virtualKey) && _currentlyPressedKeys.Count >= 5))
|
||||
{
|
||||
_activeTarget.OnInputLimitReached();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a different variant of a modifier key already pressed
|
||||
if (RemappingHelper.IsModifierKey(virtualKey))
|
||||
{
|
||||
// Remove existing variant of this modifier key if a new one is pressed
|
||||
// This is to ensure that only one variant of a modifier key is displayed at a time
|
||||
RemoveExistingModifierVariant(virtualKey);
|
||||
}
|
||||
|
||||
if (_currentlyPressedKeys.Add(virtualKey))
|
||||
{
|
||||
_keyPressOrder.Add(virtualKey);
|
||||
|
||||
// Notify the target page
|
||||
_activeTarget.OnKeyDown(virtualKey, GetFormattedKeyList());
|
||||
}
|
||||
}
|
||||
|
||||
private void KeyUp(int key)
|
||||
{
|
||||
if (_activeTarget == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VirtualKey virtualKey = (VirtualKey)key;
|
||||
|
||||
if (_currentlyPressedKeys.Remove(virtualKey))
|
||||
{
|
||||
_keyPressOrder.Remove(virtualKey);
|
||||
|
||||
_activeTarget.OnKeyUp(virtualKey, GetFormattedKeyList());
|
||||
}
|
||||
}
|
||||
|
||||
// Display the modifier keys and the action key in order, e.g. "Ctrl + Alt + A"
|
||||
private List<string> GetFormattedKeyList()
|
||||
{
|
||||
if (_activeTarget == null)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
List<string> keyList = new List<string>();
|
||||
List<VirtualKey> modifierKeys = new List<VirtualKey>();
|
||||
VirtualKey? actionKey = null;
|
||||
|
||||
foreach (var key in _keyPressOrder)
|
||||
{
|
||||
if (!_currentlyPressedKeys.Contains(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RemappingHelper.IsModifierKey(key))
|
||||
{
|
||||
if (!modifierKeys.Contains(key))
|
||||
{
|
||||
modifierKeys.Add(key);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
actionKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in modifierKeys)
|
||||
{
|
||||
keyList.Add(_mappingService.GetKeyDisplayName((int)key));
|
||||
}
|
||||
|
||||
if (actionKey.HasValue)
|
||||
{
|
||||
keyList.Add(_mappingService.GetKeyDisplayName((int)actionKey.Value));
|
||||
}
|
||||
|
||||
return keyList;
|
||||
}
|
||||
|
||||
private void RemoveExistingModifierVariant(VirtualKey key)
|
||||
{
|
||||
KeyType keyType = (KeyType)KeyboardManagerInterop.GetKeyType((int)key);
|
||||
|
||||
// No need to remove if the key is an action key
|
||||
if (keyType == KeyType.Action)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var existingKey in _currentlyPressedKeys.ToList())
|
||||
{
|
||||
if (existingKey != key)
|
||||
{
|
||||
KeyType existingKeyType = (KeyType)KeyboardManagerInterop.GetKeyType((int)existingKey);
|
||||
|
||||
// Remove the existing key if it is a modifier key and has the same type as the new key
|
||||
if (existingKeyType == keyType)
|
||||
{
|
||||
_currentlyPressedKeys.Remove(existingKey);
|
||||
_keyPressOrder.Remove(existingKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
CleanupHook();
|
||||
_mappingService?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IKeyboardHookTarget
|
||||
{
|
||||
void OnKeyDown(VirtualKey key, List<string> formattedKeys);
|
||||
|
||||
void OnKeyUp(VirtualKey key, List<string> formattedKeys)
|
||||
{
|
||||
}
|
||||
|
||||
void ClearKeys();
|
||||
|
||||
void OnInputLimitReached();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using KeyboardManagerEditorUI.Interop;
|
||||
using ManagedCommon;
|
||||
using Windows.System;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Helpers
|
||||
{
|
||||
public static class RemappingHelper
|
||||
{
|
||||
public static bool SaveMapping(KeyboardMappingService mappingService, List<string> originalKeys, List<string> remappedKeys, bool isAppSpecific, string appName)
|
||||
{
|
||||
if (mappingService == null)
|
||||
{
|
||||
Logger.LogError("Mapping service is null, cannot save mapping");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (originalKeys == null || originalKeys.Count == 0 || remappedKeys == null || remappedKeys.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (originalKeys.Count == 1)
|
||||
{
|
||||
int originalKey = mappingService.GetKeyCodeFromName(originalKeys[0]);
|
||||
if (originalKey != 0)
|
||||
{
|
||||
if (remappedKeys.Count == 1)
|
||||
{
|
||||
int targetKey = mappingService.GetKeyCodeFromName(remappedKeys[0]);
|
||||
if (targetKey != 0)
|
||||
{
|
||||
mappingService.AddSingleKeyMapping(originalKey, targetKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string targetKeys = string.Join(";", remappedKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
mappingService.AddSingleKeyMapping(originalKey, targetKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string originalKeysString = string.Join(";", originalKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
string targetKeysString = string.Join(";", remappedKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
if (isAppSpecific && !string.IsNullOrEmpty(appName))
|
||||
{
|
||||
mappingService.AddShortcutMapping(originalKeysString, targetKeysString, appName);
|
||||
}
|
||||
else
|
||||
{
|
||||
mappingService.AddShortcutMapping(originalKeysString, targetKeysString);
|
||||
}
|
||||
}
|
||||
|
||||
return mappingService.SaveSettings();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error saving mapping: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool DeleteRemapping(KeyboardMappingService mappingService, Remapping remapping)
|
||||
{
|
||||
if (mappingService == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (remapping.OriginalKeys.Count == 1)
|
||||
{
|
||||
// Single key mapping
|
||||
int originalKey = mappingService.GetKeyCodeFromName(remapping.OriginalKeys[0]);
|
||||
if (originalKey != 0)
|
||||
{
|
||||
if (mappingService.DeleteSingleKeyMapping(originalKey))
|
||||
{
|
||||
// Save settings after successful deletion
|
||||
return mappingService.SaveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (remapping.OriginalKeys.Count > 1)
|
||||
{
|
||||
// Shortcut mapping
|
||||
string originalKeysString = string.Join(";", remapping.OriginalKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
bool deleteResult;
|
||||
if (!remapping.IsAllApps && !string.IsNullOrEmpty(remapping.AppName))
|
||||
{
|
||||
// App-specific shortcut key mapping
|
||||
deleteResult = mappingService.DeleteShortcutMapping(originalKeysString, remapping.AppName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Global shortcut key mapping
|
||||
deleteResult = mappingService.DeleteShortcutMapping(originalKeysString);
|
||||
}
|
||||
|
||||
return deleteResult ? mappingService.SaveSettings() : false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error deleting remapping: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsModifierKey(VirtualKey key)
|
||||
{
|
||||
return key == VirtualKey.Control
|
||||
|| key == VirtualKey.LeftControl
|
||||
|| key == VirtualKey.RightControl
|
||||
|| key == VirtualKey.Menu
|
||||
|| key == VirtualKey.LeftMenu
|
||||
|| key == VirtualKey.RightMenu
|
||||
|| key == VirtualKey.Shift
|
||||
|| key == VirtualKey.LeftShift
|
||||
|| key == VirtualKey.RightShift
|
||||
|| key == VirtualKey.LeftWindows
|
||||
|| key == VirtualKey.RightWindows;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Helpers
|
||||
{
|
||||
public class TextMapping
|
||||
{
|
||||
public List<string> Keys { get; set; } = new List<string>();
|
||||
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
public bool IsAllApps { get; set; } = true;
|
||||
|
||||
public string AppName { get; set; } = "All Apps";
|
||||
}
|
||||
}
|
||||
@@ -20,5 +20,6 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
IllegalShortcut,
|
||||
DuplicateMapping,
|
||||
SelfMapping,
|
||||
EmptyTargetText,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
{ ValidationErrorType.IllegalShortcut, ("Reserved System Shortcut", "Win+L and Ctrl+Alt+Delete are reserved system shortcuts and cannot be remapped.") },
|
||||
{ ValidationErrorType.DuplicateMapping, ("Duplicate Remapping", "This key or shortcut is already remapped.") },
|
||||
{ ValidationErrorType.SelfMapping, ("Invalid Remapping", "A key or shortcut cannot be remapped to itself. Please choose a different target.") },
|
||||
{ ValidationErrorType.EmptyTargetText, ("Missing Target Text", "Please enter the text to be inserted when the shortcut is pressed.") },
|
||||
};
|
||||
|
||||
public static ValidationErrorType ValidateKeyMapping(
|
||||
@@ -30,7 +31,9 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
List<string> remappedKeys,
|
||||
bool isAppSpecific,
|
||||
string appName,
|
||||
KeyboardMappingService mappingService)
|
||||
KeyboardMappingService mappingService,
|
||||
bool isEditMode = false,
|
||||
Remapping? editingRemapping = null)
|
||||
{
|
||||
// Check if original keys are empty
|
||||
if (originalKeys == null || originalKeys.Count == 0)
|
||||
@@ -60,7 +63,7 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
// Check if this is a shortcut (multiple keys) and if it's an illegal combination
|
||||
if (originalKeys.Count > 1)
|
||||
{
|
||||
string shortcutKeysString = string.Join(";", originalKeys.Select(k => KeyboardManagerInterop.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
string shortcutKeysString = string.Join(";", originalKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
if (KeyboardManagerInterop.IsShortcutIllegal(shortcutKeysString))
|
||||
{
|
||||
@@ -69,13 +72,13 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
}
|
||||
|
||||
// Check for duplicate mappings
|
||||
if (IsDuplicateMapping(originalKeys, isAppSpecific, appName, mappingService))
|
||||
if (IsDuplicateMapping(originalKeys, isAppSpecific, appName, mappingService, isEditMode, editingRemapping))
|
||||
{
|
||||
return ValidationErrorType.DuplicateMapping;
|
||||
}
|
||||
|
||||
// Check for self-mapping
|
||||
if (IsSelfMapping(originalKeys, remappedKeys))
|
||||
if (IsSelfMapping(originalKeys, remappedKeys, mappingService))
|
||||
{
|
||||
return ValidationErrorType.SelfMapping;
|
||||
}
|
||||
@@ -83,7 +86,59 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
return ValidationErrorType.NoError;
|
||||
}
|
||||
|
||||
public static bool IsDuplicateMapping(List<string> originalKeys, bool isAppSpecific, string appName, KeyboardMappingService mappingService)
|
||||
public static ValidationErrorType ValidateTextMapping(
|
||||
List<string> keys,
|
||||
string textContent,
|
||||
bool isAppSpecific,
|
||||
string appName,
|
||||
KeyboardMappingService mappingService)
|
||||
{
|
||||
// Check if original keys are empty
|
||||
if (keys == null || keys.Count == 0)
|
||||
{
|
||||
return ValidationErrorType.EmptyOriginalKeys;
|
||||
}
|
||||
|
||||
// Check if text content is empty
|
||||
if (string.IsNullOrWhiteSpace(textContent))
|
||||
{
|
||||
return ValidationErrorType.EmptyTargetText;
|
||||
}
|
||||
|
||||
// Check if shortcut contains only modifier keys
|
||||
if (keys.Count > 1 && ContainsOnlyModifierKeys(keys))
|
||||
{
|
||||
return ValidationErrorType.ModifierOnly;
|
||||
}
|
||||
|
||||
// Check if app specific is checked but no app name is provided
|
||||
if (isAppSpecific && string.IsNullOrWhiteSpace(appName))
|
||||
{
|
||||
return ValidationErrorType.EmptyAppName;
|
||||
}
|
||||
|
||||
// Check if this is a shortcut (multiple keys) and if it's an illegal combination
|
||||
if (keys.Count > 1)
|
||||
{
|
||||
string shortcutKeysString = string.Join(";", keys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
if (KeyboardManagerInterop.IsShortcutIllegal(shortcutKeysString))
|
||||
{
|
||||
return ValidationErrorType.IllegalShortcut;
|
||||
}
|
||||
}
|
||||
|
||||
// No errors found
|
||||
return ValidationErrorType.NoError;
|
||||
}
|
||||
|
||||
public static bool IsDuplicateMapping(
|
||||
List<string> originalKeys,
|
||||
bool isAppSpecific,
|
||||
string appName,
|
||||
KeyboardMappingService mappingService,
|
||||
bool isEditMode = false,
|
||||
Remapping? editingRemapping = null)
|
||||
{
|
||||
if (mappingService == null || originalKeys == null || originalKeys.Count == 0)
|
||||
{
|
||||
@@ -93,7 +148,7 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
// For single key remapping
|
||||
if (originalKeys.Count == 1)
|
||||
{
|
||||
int originalKeyCode = KeyboardManagerInterop.GetKeyCodeFromName(originalKeys[0]);
|
||||
int originalKeyCode = mappingService.GetKeyCodeFromName(originalKeys[0]);
|
||||
if (originalKeyCode == 0)
|
||||
{
|
||||
return false;
|
||||
@@ -104,6 +159,14 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
{
|
||||
if (mapping.OriginalKey == originalKeyCode)
|
||||
{
|
||||
// Skip if the remapping is the same as the one being edited
|
||||
if (isEditMode && editingRemapping != null &&
|
||||
editingRemapping.OriginalKeys.Count == 1 &&
|
||||
mappingService.GetKeyCodeFromName(editingRemapping.OriginalKeys[0]) == originalKeyCode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -112,23 +175,50 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
// For shortcut remapping
|
||||
else
|
||||
{
|
||||
string originalKeysString = string.Join(";", originalKeys.Select(k => KeyboardManagerInterop.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
string originalKeysString = string.Join(";", originalKeys.Select(
|
||||
k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
// Don't check for duplicates if the original keys are the same as the remapping being edited
|
||||
bool isEditingExistingRemapping = false;
|
||||
if (isEditMode && editingRemapping != null)
|
||||
{
|
||||
string editingOriginalKeysString = string.Join(";", editingRemapping.OriginalKeys.Select(k =>
|
||||
mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
if (KeyboardManagerInterop.AreShortcutsEqual(originalKeysString, editingOriginalKeysString))
|
||||
{
|
||||
isEditingExistingRemapping = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the shortcut is already remapped in the same app context
|
||||
foreach (var mapping in mappingService.GetShortcutMappingsByType(ShortcutOperationType.RemapShortcut))
|
||||
{
|
||||
// Same shortcut in the same app context
|
||||
if (KeyboardManagerInterop.AreShortcutsEqual(originalKeysString, mapping.OriginalKeys))
|
||||
{
|
||||
// If both are global (all apps)
|
||||
if (!isAppSpecific && string.IsNullOrEmpty(mapping.TargetApp))
|
||||
{
|
||||
// Skip if the remapping is the same as the one being edited
|
||||
if (editingRemapping != null && editingRemapping.OriginalKeys.Count > 1 && editingRemapping.IsAllApps && isEditingExistingRemapping)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If both are for the same specific app
|
||||
else if (isAppSpecific && !string.IsNullOrEmpty(mapping.TargetApp) && string.Equals(mapping.TargetApp, appName, StringComparison.OrdinalIgnoreCase))
|
||||
else if (isAppSpecific && !string.IsNullOrEmpty(mapping.TargetApp)
|
||||
&& string.Equals(mapping.TargetApp, appName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Skip if the remapping is the same as the one being edited
|
||||
if (editingRemapping != null && editingRemapping.OriginalKeys.Count > 1 && !editingRemapping.IsAllApps &&
|
||||
string.Equals(editingRemapping.AppName, appName, StringComparison.OrdinalIgnoreCase) && isEditingExistingRemapping)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -138,8 +228,13 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsSelfMapping(List<string> originalKeys, List<string> remappedKeys)
|
||||
public static bool IsSelfMapping(List<string> originalKeys, List<string> remappedKeys, KeyboardMappingService mappingService)
|
||||
{
|
||||
if (mappingService == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If either list is empty, it's not a self-mapping
|
||||
if (originalKeys == null || remappedKeys == null ||
|
||||
originalKeys.Count == 0 || remappedKeys.Count == 0)
|
||||
@@ -147,8 +242,8 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
return false;
|
||||
}
|
||||
|
||||
string originalKeysString = string.Join(";", originalKeys.Select(k => KeyboardManagerInterop.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
string remappedKeysString = string.Join(";", remappedKeys.Select(k => KeyboardManagerInterop.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
string originalKeysString = string.Join(";", originalKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
string remappedKeysString = string.Join(";", remappedKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
return KeyboardManagerInterop.AreShortcutsEqual(originalKeysString, remappedKeysString);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
{
|
||||
private const string DllName = "Powertoys.KeyboardManagerEditorLibraryWrapper.dll";
|
||||
|
||||
// Configuration Management
|
||||
[DllImport(DllName)]
|
||||
internal static extern IntPtr CreateMappingConfiguration();
|
||||
|
||||
@@ -29,6 +30,7 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool SaveMappingSettings(IntPtr config);
|
||||
|
||||
// Get Mapping Functions
|
||||
[DllImport(DllName)]
|
||||
internal static extern int GetSingleKeyRemapCount(IntPtr config);
|
||||
|
||||
@@ -57,10 +59,15 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool GetShortcutRemapByType(IntPtr config, int operationType, int index, ref ShortcutMapping mapping);
|
||||
|
||||
// Add Mapping Functions
|
||||
[DllImport(DllName)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool AddSingleKeyRemap(IntPtr config, int originalKey, int targetKey);
|
||||
|
||||
[DllImport(DllName)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool AddSingleKeyToTextRemap(IntPtr config, int originalKey, [MarshalAs(UnmanagedType.LPWStr)] string targetText);
|
||||
|
||||
[DllImport(DllName)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool AddSingleKeyToShortcutRemap(IntPtr config, int originalKey, [MarshalAs(UnmanagedType.LPWStr)] string targetKeys);
|
||||
@@ -71,20 +78,31 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
IntPtr config,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string originalKeys,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string targetKeys,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string targetApp);
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string targetApp,
|
||||
int operationType = 0);
|
||||
|
||||
// Delete Mapping Functions
|
||||
[DllImport(DllName)]
|
||||
internal static extern bool DeleteSingleKeyRemap(IntPtr mappingConfiguration, int originalKey);
|
||||
|
||||
[DllImport(DllName)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool DeleteSingleKeyToTextRemap(IntPtr config, int originalKey);
|
||||
|
||||
[DllImport(DllName)]
|
||||
internal static extern bool DeleteShortcutRemap(IntPtr mappingConfiguration, [MarshalAs(UnmanagedType.LPWStr)] string originalKeys, [MarshalAs(UnmanagedType.LPWStr)] string targetApp);
|
||||
|
||||
// Key Utility Functions
|
||||
[DllImport(DllName)]
|
||||
internal static extern int GetKeyCodeFromName([MarshalAs(UnmanagedType.LPWStr)] string keyName);
|
||||
|
||||
[DllImport(DllName, CharSet = CharSet.Unicode)]
|
||||
internal static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
|
||||
|
||||
[DllImport(DllName)]
|
||||
internal static extern void FreeString(IntPtr str);
|
||||
|
||||
[DllImport(DllName)]
|
||||
internal static extern int GetKeyType(int keyCode);
|
||||
|
||||
// Validation Functions
|
||||
[DllImport(DllName)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool IsShortcutIllegal([MarshalAs(UnmanagedType.LPWStr)] string shortcutKeys);
|
||||
@@ -93,11 +111,9 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool AreShortcutsEqual([MarshalAs(UnmanagedType.LPWStr)] string lShort, [MarshalAs(UnmanagedType.LPWStr)] string rShortcut);
|
||||
|
||||
// String Management Functions
|
||||
[DllImport(DllName)]
|
||||
internal static extern bool DeleteSingleKeyRemap(IntPtr mappingConfiguration, int originalKey);
|
||||
|
||||
[DllImport(DllName)]
|
||||
internal static extern bool DeleteShortcutRemap(IntPtr mappingConfiguration, [MarshalAs(UnmanagedType.LPWStr)] string originalKeys, [MarshalAs(UnmanagedType.LPWStr)] string targetApp);
|
||||
internal static extern void FreeString(IntPtr str);
|
||||
|
||||
public static string GetStringAndFree(IntPtr handle)
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Interop
|
||||
{
|
||||
@@ -21,6 +22,7 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
_configHandle = KeyboardManagerInterop.CreateMappingConfiguration();
|
||||
if (_configHandle == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogError("Failed to create mapping configuration");
|
||||
throw new InvalidOperationException("Failed to create mapping configuration");
|
||||
}
|
||||
|
||||
@@ -131,6 +133,16 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
return keyName.ToString();
|
||||
}
|
||||
|
||||
public int GetKeyCodeFromName(string keyName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyName))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return KeyboardManagerInterop.GetKeyCodeFromName(keyName);
|
||||
}
|
||||
|
||||
public bool AddSingleKeyMapping(int originalKey, int targetKey)
|
||||
{
|
||||
return KeyboardManagerInterop.AddSingleKeyRemap(_configHandle, originalKey, targetKey);
|
||||
@@ -153,14 +165,24 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
}
|
||||
}
|
||||
|
||||
public bool AddShortcutMapping(string originalKeys, string targetKeys, string targetApp = "")
|
||||
public bool AddSingleKeyToTextMapping(int originalKey, string targetText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(targetText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return KeyboardManagerInterop.AddSingleKeyToTextRemap(_configHandle, originalKey, targetText);
|
||||
}
|
||||
|
||||
public bool AddShortcutMapping(string originalKeys, string targetKeys, string targetApp = "", ShortcutOperationType operationType = ShortcutOperationType.RemapShortcut)
|
||||
{
|
||||
if (string.IsNullOrEmpty(originalKeys) || string.IsNullOrEmpty(targetKeys))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return KeyboardManagerInterop.AddShortcutRemap(_configHandle, originalKeys, targetKeys, targetApp);
|
||||
return KeyboardManagerInterop.AddShortcutRemap(_configHandle, originalKeys, targetKeys, targetApp, (int)operationType);
|
||||
}
|
||||
|
||||
public bool SaveSettings()
|
||||
@@ -173,6 +195,16 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
return KeyboardManagerInterop.DeleteSingleKeyRemap(_configHandle, originalKey);
|
||||
}
|
||||
|
||||
public bool DeleteSingleKeyToTextMapping(int originalKey)
|
||||
{
|
||||
if (originalKey == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return KeyboardManagerInterop.DeleteSingleKeyToTextRemap(_configHandle, originalKey);
|
||||
}
|
||||
|
||||
public bool DeleteShortcutMapping(string originalKeys, string targetApp = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(originalKeys))
|
||||
|
||||
@@ -15,5 +15,6 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
RemapShortcut = 0,
|
||||
RunProgram = 1,
|
||||
OpenUri = 2,
|
||||
RemapText = 3,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<None Remove="Styles\CommonStyle.xaml" />
|
||||
<None Remove="Styles\InputControl.xaml" />
|
||||
<None Remove="Styles\KeyVisual.xaml" />
|
||||
<None Remove="Styles\TextPageInputControl.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -57,6 +58,11 @@
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\TextPageInputControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\CommonStyle.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -49,16 +49,16 @@
|
||||
<FontIcon Glyph="" />
|
||||
</NavigationViewItem.Icon>
|
||||
</NavigationViewItem>
|
||||
<NavigationViewItem Content="Programs" Tag="Programs">
|
||||
<NavigationViewItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</NavigationViewItem.Icon>
|
||||
</NavigationViewItem>
|
||||
<NavigationViewItem Content="Text" Tag="Text">
|
||||
<NavigationViewItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</NavigationViewItem.Icon>
|
||||
</NavigationViewItem>
|
||||
<NavigationViewItem Content="Programs" Tag="Programs">
|
||||
<NavigationViewItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</NavigationViewItem.Icon>
|
||||
</NavigationViewItem>
|
||||
<NavigationViewItem Content="URLs" Tag="URLs">
|
||||
<NavigationViewItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using KeyboardManagerEditorUI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
@@ -29,12 +30,34 @@ namespace KeyboardManagerEditorUI
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
this.SetTitleBar(titleBar);
|
||||
|
||||
this.Activated += MainWindow_Activated;
|
||||
this.Closed += MainWindow_Closed;
|
||||
|
||||
// Set the default page
|
||||
RootView.SelectedItem = RootView.MenuItems[0];
|
||||
}
|
||||
|
||||
private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
// Release the keyboard hook when the window is deactivated
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
}
|
||||
}
|
||||
|
||||
private void MainWindow_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
KeyboardHookHelper.Instance.Dispose();
|
||||
this.Activated -= MainWindow_Activated;
|
||||
this.Closed -= MainWindow_Closed;
|
||||
}
|
||||
|
||||
private void RootView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
// Cleanup the keyboard hook when the selected page changes
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
|
||||
if (args.SelectedItem is NavigationViewItem selectedItem)
|
||||
{
|
||||
switch ((string)selectedItem.Tag)
|
||||
|
||||
@@ -45,9 +45,6 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
[DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern int GetKeyboardKeysList(bool isShortcut, [Out] KeyNamePair[] keyList, int maxCount);
|
||||
|
||||
[DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern bool CheckIfRemappingsAreValid();
|
||||
|
||||
public List<KeyboardKey> KeysList { get; private set; } = new List<KeyboardKey>();
|
||||
|
||||
public ExistingUI()
|
||||
@@ -81,14 +78,6 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void KeyComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.AddedItems.Count > 0 && e.AddedItems[0] is KeyboardKey key)
|
||||
{
|
||||
Console.WriteLine($"selected key: {key.KeyName} (code: {key.KeyCode})");
|
||||
}
|
||||
}
|
||||
|
||||
private void Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var button = sender as Button;
|
||||
|
||||
@@ -62,6 +62,12 @@
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="Program" />
|
||||
<Rectangle
|
||||
Grid.ColumnSpan="4"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<ListView
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="4"
|
||||
|
||||
@@ -68,7 +68,197 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
{
|
||||
// Make sure we unregister the handler when the page is unloaded
|
||||
UnregisterWindowActivationHandler();
|
||||
RemappingControl.CleanupKeyboardHook();
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
}
|
||||
|
||||
private void RegisterWindowActivationHandler()
|
||||
{
|
||||
// Get the current window that contains this page
|
||||
var app = Application.Current as App;
|
||||
if (app?.GetWindow() is Window window)
|
||||
{
|
||||
// Register for window activation events
|
||||
window.Activated += Dialog_WindowActivated;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterWindowActivationHandler()
|
||||
{
|
||||
var app = Application.Current as App;
|
||||
if (app?.GetWindow() is Window window)
|
||||
{
|
||||
// Unregister to prevent memory leaks
|
||||
window.Activated -= Dialog_WindowActivated;
|
||||
}
|
||||
}
|
||||
|
||||
private void Dialog_WindowActivated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
// When window is deactivated (user switched to another app)
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
// Make sure to cleanup the keyboard hook when the window loses focus
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
|
||||
RemappingControl.ResetToggleButtons();
|
||||
RemappingControl.UpdateAllAppsCheckBoxState();
|
||||
}
|
||||
}
|
||||
|
||||
private async void NewRemappingBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_isEditMode = false;
|
||||
_editingRemapping = null;
|
||||
|
||||
RemappingControl.SetOriginalKeys(new List<string>());
|
||||
RemappingControl.SetRemappedKeys(new List<string>());
|
||||
RemappingControl.SetApp(false, string.Empty);
|
||||
RemappingControl.SetUpToggleButtonInitialStatus();
|
||||
|
||||
RegisterWindowActivationHandler();
|
||||
|
||||
// Show the dialog to add a new remapping
|
||||
KeyDialog.PrimaryButtonClick += KeyDialog_PrimaryButtonClick;
|
||||
await KeyDialog.ShowAsync();
|
||||
KeyDialog.PrimaryButtonClick -= KeyDialog_PrimaryButtonClick;
|
||||
|
||||
UnregisterWindowActivationHandler();
|
||||
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
}
|
||||
|
||||
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is Remapping selectedRemapping && selectedRemapping.IsEnabled)
|
||||
{
|
||||
// Set to edit mode
|
||||
_isEditMode = true;
|
||||
_editingRemapping = selectedRemapping;
|
||||
|
||||
RemappingControl.SetOriginalKeys(selectedRemapping.OriginalKeys);
|
||||
RemappingControl.SetRemappedKeys(selectedRemapping.RemappedKeys);
|
||||
RemappingControl.SetApp(!selectedRemapping.IsAllApps, selectedRemapping.AppName);
|
||||
RemappingControl.SetUpToggleButtonInitialStatus();
|
||||
|
||||
RegisterWindowActivationHandler();
|
||||
|
||||
KeyDialog.PrimaryButtonClick += KeyDialog_PrimaryButtonClick;
|
||||
await KeyDialog.ShowAsync();
|
||||
KeyDialog.PrimaryButtonClick -= KeyDialog_PrimaryButtonClick;
|
||||
|
||||
UnregisterWindowActivationHandler();
|
||||
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
|
||||
// Reset the edit status
|
||||
_isEditMode = false;
|
||||
_editingRemapping = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void KeyDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
List<string> originalKeys = RemappingControl.GetOriginalKeys();
|
||||
List<string> remappedKeys = RemappingControl.GetRemappedKeys();
|
||||
bool isAppSpecific = RemappingControl.GetIsAppSpecific();
|
||||
string appName = RemappingControl.GetAppName();
|
||||
|
||||
// Make sure _mappingService is not null before validating and saving
|
||||
if (_mappingService == null)
|
||||
{
|
||||
Logger.LogError("Mapping service is null, cannot validate mapping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the remapping
|
||||
ValidationErrorType errorType = ValidationHelper.ValidateKeyMapping(
|
||||
originalKeys, remappedKeys, isAppSpecific, appName, _mappingService, _isEditMode, _editingRemapping);
|
||||
|
||||
if (errorType != ValidationErrorType.NoError)
|
||||
{
|
||||
ShowValidationError(errorType, args);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for orphaned keys
|
||||
if (originalKeys.Count == 1 && _mappingService != null)
|
||||
{
|
||||
int originalKeyCode = _mappingService.GetKeyCodeFromName(originalKeys[0]);
|
||||
|
||||
if (IsKeyOrphaned(originalKeyCode, _mappingService))
|
||||
{
|
||||
string keyName = _mappingService.GetKeyDisplayName(originalKeyCode);
|
||||
|
||||
OrphanedKeysTeachingTip.Target = RemappingControl;
|
||||
OrphanedKeysTeachingTip.Subtitle = $"The key {keyName} will become orphaned (inaccessible) after remapping. Please confirm if you want to proceed.";
|
||||
OrphanedKeysTeachingTip.Tag = args;
|
||||
OrphanedKeysTeachingTip.IsOpen = true;
|
||||
|
||||
args.Cancel = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If in edit mode, delete the existing remapping before saving the new one
|
||||
if (_isEditMode && _editingRemapping != null)
|
||||
{
|
||||
if (!RemappingHelper.DeleteRemapping(_mappingService!, _editingRemapping))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If no errors, proceed to save the remapping
|
||||
bool saved = RemappingHelper.SaveMapping(_mappingService!, originalKeys, remappedKeys, isAppSpecific, appName);
|
||||
if (saved)
|
||||
{
|
||||
// Display the remapping in the list after saving
|
||||
LoadMappings();
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button && button.DataContext is Remapping remapping)
|
||||
{
|
||||
if (RemappingHelper.DeleteRemapping(_mappingService!, remapping))
|
||||
{
|
||||
LoadMappings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidationTeachingTip_CloseButtonClick(TeachingTip sender, object args)
|
||||
{
|
||||
sender.IsOpen = false;
|
||||
}
|
||||
|
||||
private void OrphanedKeysTeachingTip_ActionButtonClick(TeachingTip sender, object args)
|
||||
{
|
||||
// User pressed continue anyway button
|
||||
sender.IsOpen = false;
|
||||
|
||||
if (_isEditMode && _editingRemapping != null)
|
||||
{
|
||||
if (!RemappingHelper.DeleteRemapping(_mappingService!, _editingRemapping))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool saved = RemappingHelper.SaveMapping(
|
||||
_mappingService!, RemappingControl.GetOriginalKeys(), RemappingControl.GetRemappedKeys(), RemappingControl.GetIsAppSpecific(), RemappingControl.GetAppName());
|
||||
if (saved)
|
||||
{
|
||||
KeyDialog.Hide();
|
||||
LoadMappings();
|
||||
}
|
||||
}
|
||||
|
||||
private void OrphanedKeysTeachingTip_CloseButtonClick(TeachingTip sender, object args)
|
||||
{
|
||||
// Just close the teaching tip if the user canceled
|
||||
sender.IsOpen = false;
|
||||
}
|
||||
|
||||
private void LoadMappings()
|
||||
@@ -143,6 +333,19 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShowValidationError(ValidationErrorType errorType, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
var (title, message) = ValidationMessages[errorType];
|
||||
|
||||
ValidationTeachingTip.Title = title;
|
||||
ValidationTeachingTip.Subtitle = message;
|
||||
ValidationTeachingTip.Target = RemappingControl;
|
||||
ValidationTeachingTip.Tag = args;
|
||||
ValidationTeachingTip.IsOpen = true;
|
||||
args.Cancel = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
@@ -163,325 +366,5 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterWindowActivationHandler()
|
||||
{
|
||||
// Get the current window that contains this page
|
||||
var app = Application.Current as App;
|
||||
if (app?.GetWindow() is Window window)
|
||||
{
|
||||
// Register for window activation events
|
||||
window.Activated += Dialog_WindowActivated;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterWindowActivationHandler()
|
||||
{
|
||||
var app = Application.Current as App;
|
||||
if (app?.GetWindow() is Window window)
|
||||
{
|
||||
// Unregister to prevent memory leaks
|
||||
window.Activated -= Dialog_WindowActivated;
|
||||
}
|
||||
}
|
||||
|
||||
private void Dialog_WindowActivated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
// When window is deactivated (user switched to another app)
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
// Make sure to cleanup the keyboard hook when the window loses focus
|
||||
RemappingControl.CleanupKeyboardHook();
|
||||
|
||||
RemappingControl.ResetToggleButtons();
|
||||
RemappingControl.UpdateAllAppsCheckBoxState();
|
||||
}
|
||||
}
|
||||
|
||||
private async void NewRemappingBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_isEditMode = false;
|
||||
_editingRemapping = null;
|
||||
|
||||
RemappingControl.SetOriginalKeys(new List<string>());
|
||||
RemappingControl.SetRemappedKeys(new List<string>());
|
||||
RemappingControl.SetApp(false, string.Empty);
|
||||
RemappingControl.SetUpToggleButtonInitialStatus();
|
||||
|
||||
RegisterWindowActivationHandler();
|
||||
|
||||
// Show the dialog to add a new remapping
|
||||
KeyDialog.PrimaryButtonClick += KeyDialog_PrimaryButtonClick;
|
||||
await KeyDialog.ShowAsync();
|
||||
KeyDialog.PrimaryButtonClick -= KeyDialog_PrimaryButtonClick;
|
||||
|
||||
UnregisterWindowActivationHandler();
|
||||
|
||||
RemappingControl.CleanupKeyboardHook();
|
||||
}
|
||||
|
||||
private void KeyDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
List<string> originalKeys = RemappingControl.GetOriginalKeys();
|
||||
List<string> remappedKeys = RemappingControl.GetRemappedKeys();
|
||||
bool isAppSpecific = RemappingControl.GetIsAppSpecific();
|
||||
string appName = RemappingControl.GetAppName();
|
||||
|
||||
// Make sure _mappingService is not null before validating and saving
|
||||
if (_mappingService == null)
|
||||
{
|
||||
Logger.LogError("Mapping service is null, cannot validate mapping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the remapping
|
||||
ValidationErrorType errorType = ValidationHelper.ValidateKeyMapping(
|
||||
originalKeys, remappedKeys, isAppSpecific, appName, _mappingService);
|
||||
|
||||
if (errorType != ValidationErrorType.NoError)
|
||||
{
|
||||
ShowValidationError(errorType, args);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for orphaned keys
|
||||
if (originalKeys.Count == 1 && _mappingService != null)
|
||||
{
|
||||
int originalKeyCode = GetKeyCode(originalKeys[0]);
|
||||
|
||||
if (IsKeyOrphaned(originalKeyCode, _mappingService))
|
||||
{
|
||||
string keyName = _mappingService.GetKeyDisplayName(originalKeyCode);
|
||||
|
||||
OrphanedKeysTeachingTip.Target = RemappingControl;
|
||||
OrphanedKeysTeachingTip.Subtitle = $"The key {keyName} will become orphaned (inaccessible) after remapping. Please confirm if you want to proceed.";
|
||||
OrphanedKeysTeachingTip.Tag = args;
|
||||
OrphanedKeysTeachingTip.IsOpen = true;
|
||||
|
||||
args.Cancel = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If in edit mode, delete the existing remapping before saving the new one
|
||||
if (_isEditMode && _editingRemapping != null)
|
||||
{
|
||||
DeleteRemapping(_editingRemapping);
|
||||
}
|
||||
|
||||
// If no errors, proceed to save the remapping
|
||||
bool saved = SaveCurrentMapping();
|
||||
if (saved)
|
||||
{
|
||||
// Display the remapping in the list after saving
|
||||
LoadMappings();
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShowValidationError(ValidationErrorType errorType, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
var (title, message) = ValidationMessages[errorType];
|
||||
|
||||
ValidationTeachingTip.Title = title;
|
||||
ValidationTeachingTip.Subtitle = message;
|
||||
ValidationTeachingTip.Target = RemappingControl;
|
||||
ValidationTeachingTip.Tag = args;
|
||||
ValidationTeachingTip.IsOpen = true;
|
||||
args.Cancel = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ValidationTeachingTip_CloseButtonClick(TeachingTip sender, object args)
|
||||
{
|
||||
sender.IsOpen = false;
|
||||
}
|
||||
|
||||
private void OrphanedKeysTeachingTip_ActionButtonClick(TeachingTip sender, object args)
|
||||
{
|
||||
// User pressed continue anyway button
|
||||
sender.IsOpen = false;
|
||||
|
||||
if (_isEditMode && _editingRemapping != null)
|
||||
{
|
||||
DeleteRemapping(_editingRemapping);
|
||||
}
|
||||
|
||||
bool saved = SaveCurrentMapping();
|
||||
if (saved)
|
||||
{
|
||||
KeyDialog.Hide();
|
||||
LoadMappings();
|
||||
}
|
||||
}
|
||||
|
||||
private void OrphanedKeysTeachingTip_CloseButtonClick(TeachingTip sender, object args)
|
||||
{
|
||||
// User canceled - just close the teaching tip
|
||||
sender.IsOpen = false;
|
||||
}
|
||||
|
||||
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is Remapping selectedRemapping && selectedRemapping.IsEnabled)
|
||||
{
|
||||
// Set to edit mode
|
||||
_isEditMode = true;
|
||||
_editingRemapping = selectedRemapping;
|
||||
|
||||
RemappingControl.SetOriginalKeys(selectedRemapping.OriginalKeys);
|
||||
RemappingControl.SetRemappedKeys(selectedRemapping.RemappedKeys);
|
||||
RemappingControl.SetApp(!selectedRemapping.IsAllApps, selectedRemapping.AppName);
|
||||
RemappingControl.SetUpToggleButtonInitialStatus();
|
||||
|
||||
RegisterWindowActivationHandler();
|
||||
|
||||
KeyDialog.PrimaryButtonClick += KeyDialog_PrimaryButtonClick;
|
||||
await KeyDialog.ShowAsync();
|
||||
KeyDialog.PrimaryButtonClick -= KeyDialog_PrimaryButtonClick;
|
||||
|
||||
UnregisterWindowActivationHandler();
|
||||
|
||||
RemappingControl.CleanupKeyboardHook();
|
||||
|
||||
// Reset the edit status
|
||||
_isEditMode = false;
|
||||
_editingRemapping = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetKeyCode(string keyName)
|
||||
{
|
||||
return KeyboardManagerInterop.GetKeyCodeFromName(keyName);
|
||||
}
|
||||
|
||||
private bool SaveCurrentMapping()
|
||||
{
|
||||
if (_mappingService == null)
|
||||
{
|
||||
Logger.LogError("Mapping service is null, cannot save mapping");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
List<string> originalKeys = RemappingControl.GetOriginalKeys();
|
||||
List<string> remappedKeys = RemappingControl.GetRemappedKeys();
|
||||
bool isAppSpecific = RemappingControl.GetIsAppSpecific();
|
||||
string appName = RemappingControl.GetAppName();
|
||||
|
||||
if (originalKeys == null || originalKeys.Count == 0 || remappedKeys == null || remappedKeys.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (originalKeys.Count == 1)
|
||||
{
|
||||
int originalKey = GetKeyCode(originalKeys[0]);
|
||||
if (originalKey != 0)
|
||||
{
|
||||
if (remappedKeys.Count == 1)
|
||||
{
|
||||
int targetKey = GetKeyCode(remappedKeys[0]);
|
||||
if (targetKey != 0)
|
||||
{
|
||||
_mappingService.AddSingleKeyMapping(originalKey, targetKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string targetKeys = string.Join(";", remappedKeys.Select(k => GetKeyCode(k).ToString(CultureInfo.InvariantCulture)));
|
||||
_mappingService.AddSingleKeyMapping(originalKey, targetKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string originalKeysString = string.Join(";", originalKeys.Select(k => GetKeyCode(k).ToString(CultureInfo.InvariantCulture)));
|
||||
string targetKeysString = string.Join(";", remappedKeys.Select(k => GetKeyCode(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
if (isAppSpecific && !string.IsNullOrEmpty(appName))
|
||||
{
|
||||
_mappingService.AddShortcutMapping(originalKeysString, targetKeysString, appName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_mappingService.AddShortcutMapping(originalKeysString, targetKeysString);
|
||||
}
|
||||
}
|
||||
|
||||
_mappingService.SaveSettings();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error saving shortcut mapping: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button && button.DataContext is Remapping remapping)
|
||||
{
|
||||
DeleteRemapping(remapping);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteRemapping(Remapping remapping)
|
||||
{
|
||||
if (_mappingService == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Determine the type of remapping to delete
|
||||
if (remapping.OriginalKeys.Count == 1)
|
||||
{
|
||||
// Single key mapping
|
||||
int originalKey = GetKeyCode(remapping.OriginalKeys[0]);
|
||||
if (originalKey != 0)
|
||||
{
|
||||
if (_mappingService.DeleteSingleKeyMapping(originalKey))
|
||||
{
|
||||
// Save settings after successful deletion
|
||||
_mappingService.SaveSettings();
|
||||
|
||||
// Remove from UI
|
||||
RemappingList.Remove(remapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (remapping.OriginalKeys.Count > 1)
|
||||
{
|
||||
// Shortcut key mapping
|
||||
string originalKeysString = string.Join(";", remapping.OriginalKeys.Select(k => GetKeyCode(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
bool deleteResult;
|
||||
if (!remapping.IsAllApps && !string.IsNullOrEmpty(remapping.AppName))
|
||||
{
|
||||
// App-specific shortcut key mapping
|
||||
deleteResult = _mappingService.DeleteShortcutMapping(originalKeysString, remapping.AppName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Global shortcut key mapping
|
||||
deleteResult = _mappingService.DeleteShortcutMapping(originalKeysString);
|
||||
}
|
||||
|
||||
if (deleteResult)
|
||||
{
|
||||
_mappingService.SaveSettings();
|
||||
|
||||
RemappingList.Remove(remapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error deleting remapping: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
x:Class="KeyboardManagerEditorUI.Pages.Text"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helper="using:KeyboardManagerEditorUI.Helpers"
|
||||
xmlns:local="using:KeyboardManagerEditorUI.Pages"
|
||||
@@ -14,7 +15,7 @@
|
||||
<StackPanel Orientation="Vertical" Spacing="12">
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="Text shortcuts allow you to insert text in any inputfield when you use the configured text"
|
||||
Text="Text shortcuts allow you to insert text in any input field when you use the configured keyboard shortcut"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
x:Name="NewShortcutBtn"
|
||||
@@ -40,9 +41,9 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="236" />
|
||||
<ColumnDefinition Width="236" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="120" />
|
||||
<ColumnDefinition Width="84" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
@@ -50,7 +51,7 @@
|
||||
Margin="16,-2,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="Shortcut" />
|
||||
Text="Original key(s)" />
|
||||
<AppBarSeparator
|
||||
Grid.Column="1"
|
||||
Margin="-6,4,0,4"
|
||||
@@ -62,21 +63,42 @@
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="Text" />
|
||||
<AppBarSeparator
|
||||
Grid.Column="2"
|
||||
Margin="-6,4,0,4"
|
||||
HorizontalAlignment="Left" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="12,-2,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="Applicable apps" />
|
||||
|
||||
<Rectangle
|
||||
Grid.Row="0"
|
||||
Grid.ColumnSpan="4"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
|
||||
<ListView
|
||||
x:Name="MappingsList"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="4"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ListView_ItemClick"
|
||||
ItemsSource="{x:Bind Shortcuts}"
|
||||
ItemsSource="{x:Bind TextMappings}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="helper:URLShortcut">
|
||||
<Grid Height="48">
|
||||
<DataTemplate x:DataType="helper:TextMapping">
|
||||
<Grid Height="Auto" MinHeight="48">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="232" />
|
||||
<ColumnDefinition Width="238" />
|
||||
<ColumnDefinition Width="236" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="84" />
|
||||
<ColumnDefinition Width="120" />
|
||||
<ColumnDefinition Width="48" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Rectangle
|
||||
Grid.ColumnSpan="5"
|
||||
@@ -86,13 +108,18 @@
|
||||
VerticalAlignment="Bottom"
|
||||
Fill="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
Opacity="0.8" />
|
||||
|
||||
<!-- Shortcut keys -->
|
||||
<ItemsControl
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
ItemsSource="{x:Bind Shortcut}">
|
||||
ItemsSource="{x:Bind Keys}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
<controls:WrapPanel
|
||||
MaxWidth="230"
|
||||
HorizontalSpacing="4"
|
||||
Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
@@ -104,62 +131,77 @@
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- Text content -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,0,0,0"
|
||||
Margin="-4,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Text="{x:Bind URL}"
|
||||
Text="{x:Bind Text}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<ToggleSwitch
|
||||
Grid.ColumnSpan="4"
|
||||
Margin="0,0,-112,0"
|
||||
|
||||
<!-- App name -->
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="-4,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind AppName}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<!-- Delete button -->
|
||||
<Button
|
||||
Grid.Column="3"
|
||||
Margin="0,0,4,0"
|
||||
Padding="8,4"
|
||||
HorizontalAlignment="Right"
|
||||
IsOn="True"
|
||||
OffContent=""
|
||||
OnContent="" />
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Delete remapping"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Click="DeleteButton_Click"
|
||||
ToolTipService.ToolTip="Delete">
|
||||
<FontIcon
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<ContentDialog
|
||||
x:Name="KeyDialog"
|
||||
Title="New text shortcut"
|
||||
Title="Text Shortcut"
|
||||
Width="480"
|
||||
Height="360"
|
||||
MinWidth="600"
|
||||
MaxWidth="600"
|
||||
PrimaryButtonClick="KeyDialog_PrimaryButtonClick"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
|
||||
PrimaryButtonText="Save"
|
||||
SecondaryButtonText="Cancel">
|
||||
<StackPanel
|
||||
Width="360"
|
||||
Height="360"
|
||||
Orientation="Vertical">
|
||||
<TextBlock Margin="0,12,0,8" Text="Shortcut" />
|
||||
<ToggleButton
|
||||
x:Name="OriginalToggleBtn"
|
||||
Padding="0,24,0,24"
|
||||
HorizontalAlignment="Stretch"
|
||||
Style="{StaticResource CustomShortcutToggleButtonStyle}">
|
||||
<ToggleButton.Content>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<styles:KeyVisual Content="Shift" VisualType="SmallOutline" />
|
||||
<styles:KeyVisual Content="Win" VisualType="SmallOutline" />
|
||||
<styles:KeyVisual Content="M" VisualType="SmallOutline" />
|
||||
</StackPanel>
|
||||
</ToggleButton.Content>
|
||||
</ToggleButton>
|
||||
<TextBox
|
||||
Height="200"
|
||||
Margin="0,24,0,0"
|
||||
Header="Text to insert"
|
||||
Text="https://www.microsoft.com"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Grid>
|
||||
<styles:TextPageInputControl x:Name="TextInputControl" />
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
|
||||
<TeachingTip
|
||||
x:Name="ValidationTip"
|
||||
CloseButtonContent="OK"
|
||||
IsLightDismissEnabled="True"
|
||||
PreferredPlacement="Center">
|
||||
<TeachingTip.IconSource>
|
||||
<SymbolIconSource Symbol="Important" />
|
||||
</TeachingTip.IconSource>
|
||||
</TeachingTip>
|
||||
</Grid>
|
||||
</Page>
|
||||
</Page>
|
||||
@@ -5,46 +5,274 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Threading.Tasks;
|
||||
using KeyboardManagerEditorUI.Helpers;
|
||||
using KeyboardManagerEditorUI.Interop;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class Text : Page
|
||||
public sealed partial class Text : Page, IDisposable
|
||||
{
|
||||
public ObservableCollection<URLShortcut> Shortcuts { get; set; }
|
||||
private KeyboardMappingService? _mappingService;
|
||||
|
||||
// Flag to indicate if the user is editing an existing mapping
|
||||
private bool _isEditMode;
|
||||
private TextMapping? _editingMapping;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
// The list of text mappings
|
||||
public ObservableCollection<TextMapping> TextMappings { get; } = new ObservableCollection<TextMapping>();
|
||||
|
||||
public Text()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
Shortcuts = new ObservableCollection<URLShortcut>();
|
||||
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Alt (Left)", "H" }, URL = "Hello" });
|
||||
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Win (Left)", "P", }, URL = "PowerToys" });
|
||||
try
|
||||
{
|
||||
_mappingService = new KeyboardMappingService();
|
||||
LoadTextMappings();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to initialize KeyboardMappingService: " + ex.Message);
|
||||
}
|
||||
|
||||
this.Unloaded += Text_Unloaded;
|
||||
}
|
||||
|
||||
private void Text_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
private void LoadTextMappings()
|
||||
{
|
||||
if (_mappingService == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TextMappings.Clear();
|
||||
|
||||
// Load key-to-text mappings
|
||||
var keyToTextMappings = _mappingService.GetKeyToTextMappings();
|
||||
foreach (var mapping in keyToTextMappings)
|
||||
{
|
||||
TextMappings.Add(new TextMapping
|
||||
{
|
||||
Keys = new List<string> { _mappingService.GetKeyDisplayName(mapping.OriginalKey) },
|
||||
Text = mapping.TargetText,
|
||||
IsAllApps = true,
|
||||
AppName = "All Apps",
|
||||
});
|
||||
}
|
||||
|
||||
// Load shortcut-to-text mappings
|
||||
foreach (var mapping in _mappingService.GetShortcutMappingsByType(ShortcutOperationType.RemapText))
|
||||
{
|
||||
string[] originalKeyCodes = mapping.OriginalKeys.Split(';');
|
||||
var originalKeyNames = new List<string>();
|
||||
foreach (var keyCode in originalKeyCodes)
|
||||
{
|
||||
if (int.TryParse(keyCode, out int code))
|
||||
{
|
||||
originalKeyNames.Add(_mappingService.GetKeyDisplayName(code));
|
||||
}
|
||||
}
|
||||
|
||||
TextMappings.Add(new TextMapping
|
||||
{
|
||||
Keys = originalKeyNames,
|
||||
Text = mapping.TargetText,
|
||||
IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
|
||||
AppName = string.IsNullOrEmpty(mapping.TargetApp) ? "All Apps" : mapping.TargetApp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async void NewShortcutBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_isEditMode = false;
|
||||
_editingMapping = null;
|
||||
|
||||
TextInputControl.ClearKeys();
|
||||
TextInputControl.SetTextContent(string.Empty);
|
||||
TextInputControl.SetAppSpecific(false, string.Empty);
|
||||
|
||||
await KeyDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
await KeyDialog.ShowAsync();
|
||||
if (e.ClickedItem is TextMapping selectedMapping)
|
||||
{
|
||||
_isEditMode = true;
|
||||
_editingMapping = selectedMapping;
|
||||
|
||||
TextInputControl.SetShortcutKeys(selectedMapping.Keys);
|
||||
TextInputControl.SetTextContent(selectedMapping.Text);
|
||||
TextInputControl.SetAppSpecific(!selectedMapping.IsAllApps, selectedMapping.AppName);
|
||||
|
||||
await KeyDialog.ShowAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void KeyDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
if (_mappingService == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> keys = TextInputControl.GetShortcutKeys();
|
||||
string textContent = TextInputControl.GetTextContent();
|
||||
bool isAppSpecific = TextInputControl.GetIsAppSpecific();
|
||||
string appName = TextInputControl.GetAppName();
|
||||
|
||||
// Validate inputs
|
||||
ValidationErrorType errorType = ValidationHelper.ValidateTextMapping(
|
||||
keys, textContent, isAppSpecific, appName, _mappingService);
|
||||
|
||||
if (errorType != ValidationErrorType.NoError)
|
||||
{
|
||||
ShowValidationError(errorType, args);
|
||||
return;
|
||||
}
|
||||
|
||||
bool saved = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Delete existing mapping if in edit mode
|
||||
if (_isEditMode && _editingMapping != null)
|
||||
{
|
||||
if (_editingMapping.Keys.Count == 1)
|
||||
{
|
||||
int originalKey = _mappingService.GetKeyCodeFromName(_editingMapping.Keys[0]);
|
||||
if (originalKey != 0)
|
||||
{
|
||||
_mappingService.DeleteSingleKeyToTextMapping(originalKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string originalKeys = string.Join(";", _editingMapping.Keys.Select(k => _mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
_mappingService.DeleteShortcutMapping(originalKeys, _editingMapping.IsAllApps ? string.Empty : _editingMapping.AppName);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new mapping
|
||||
if (keys.Count == 1)
|
||||
{
|
||||
// Single key to text mapping
|
||||
int originalKey = _mappingService.GetKeyCodeFromName(keys[0]);
|
||||
if (originalKey != 0)
|
||||
{
|
||||
saved = _mappingService.AddSingleKeyToTextMapping(originalKey, textContent);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Shortcut to text mapping
|
||||
string originalKeysString = string.Join(";", keys.Select(k => _mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
if (isAppSpecific && !string.IsNullOrEmpty(appName))
|
||||
{
|
||||
saved = _mappingService.AddShortcutMapping(originalKeysString, textContent, appName, ShortcutOperationType.RemapText);
|
||||
}
|
||||
else
|
||||
{
|
||||
saved = _mappingService.AddShortcutMapping(originalKeysString, textContent, operationType: ShortcutOperationType.RemapText);
|
||||
}
|
||||
}
|
||||
|
||||
if (saved)
|
||||
{
|
||||
_mappingService.SaveSettings();
|
||||
LoadTextMappings(); // Refresh the list
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error saving text mapping: " + ex.Message);
|
||||
args.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_mappingService == null || !(sender is Button button) || !(button.DataContext is TextMapping mapping))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
bool deleted = false;
|
||||
if (mapping.Keys.Count == 1)
|
||||
{
|
||||
// Single key mapping
|
||||
int originalKey = _mappingService.GetKeyCodeFromName(mapping.Keys[0]);
|
||||
if (originalKey != 0)
|
||||
{
|
||||
deleted = _mappingService.DeleteSingleKeyToTextMapping(originalKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Shortcut mapping
|
||||
string originalKeys = string.Join(";", mapping.Keys.Select(k => _mappingService.GetKeyCodeFromName(k)));
|
||||
deleted = _mappingService.DeleteShortcutMapping(originalKeys, mapping.IsAllApps ? string.Empty : mapping.AppName);
|
||||
}
|
||||
|
||||
if (deleted)
|
||||
{
|
||||
_mappingService.SaveSettings();
|
||||
TextMappings.Remove(mapping);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error deleting text mapping: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowValidationError(ValidationErrorType errorType, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
if (ValidationHelper.ValidationMessages.TryGetValue(errorType, out (string Title, string Message) error))
|
||||
{
|
||||
ValidationTip.Title = error.Title;
|
||||
ValidationTip.Subtitle = error.Message;
|
||||
ValidationTip.IsOpen = true;
|
||||
args.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_mappingService?.Dispose();
|
||||
_mappingService = null;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,12 @@
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="URL" />
|
||||
|
||||
<Rectangle
|
||||
Grid.ColumnSpan="4"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<ListView
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="4"
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="240" />
|
||||
|
||||
@@ -16,38 +16,29 @@ using Windows.System;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Styles
|
||||
{
|
||||
public sealed partial class InputControl : UserControl, IDisposable
|
||||
public sealed partial class InputControl : UserControl, IDisposable, IKeyboardHookTarget
|
||||
{
|
||||
// List to store pressed keys
|
||||
private HashSet<VirtualKey> _currentlyPressedKeys = new HashSet<VirtualKey>();
|
||||
|
||||
// List to store order of remapped keys
|
||||
private List<VirtualKey> _keyPressOrder = new List<VirtualKey>();
|
||||
|
||||
// Collection to store original and remapped keys
|
||||
private ObservableCollection<string> _originalKeys = new ObservableCollection<string>();
|
||||
private ObservableCollection<string> _remappedKeys = new ObservableCollection<string>();
|
||||
|
||||
private HotkeySettingsControlHook? _keyboardHook;
|
||||
|
||||
// TeachingTip for notifications
|
||||
private TeachingTip? currentNotification;
|
||||
private DispatcherTimer? notificationTimer;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
// Define newMode as a DependencyProperty for binding
|
||||
public static readonly DependencyProperty NewModeProperty =
|
||||
public static readonly DependencyProperty InputModeProperty =
|
||||
DependencyProperty.Register(
|
||||
"NewMode",
|
||||
typeof(bool),
|
||||
"InputMode",
|
||||
typeof(KeyInputMode),
|
||||
typeof(InputControl),
|
||||
new PropertyMetadata(false, OnNewModeChanged));
|
||||
new PropertyMetadata(KeyInputMode.OriginalKeys));
|
||||
|
||||
public bool NewMode
|
||||
public KeyInputMode InputMode
|
||||
{
|
||||
get { return (bool)GetValue(NewModeProperty); }
|
||||
set { SetValue(NewModeProperty, value); }
|
||||
get { return (KeyInputMode)GetValue(InputModeProperty); }
|
||||
set { SetValue(InputModeProperty, value); }
|
||||
}
|
||||
|
||||
public InputControl()
|
||||
@@ -66,63 +57,232 @@ namespace KeyboardManagerEditorUI.Styles
|
||||
UpdateAllAppsCheckBoxState();
|
||||
}
|
||||
|
||||
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AllAppsCheckBox.Checked += AllAppsCheckBox_Checked;
|
||||
AllAppsCheckBox.Unchecked += AllAppsCheckBox_Unchecked;
|
||||
|
||||
AppNameTextBox.GotFocus += AppNameTextBox_GotFocus;
|
||||
}
|
||||
|
||||
private void InputControl_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Reset the control when it is unloaded
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void InputControl_KeyDown(int key)
|
||||
public void OnKeyDown(VirtualKey key, List<string> formattedKeys)
|
||||
{
|
||||
VirtualKey virtualKey = (VirtualKey)key;
|
||||
|
||||
if (_currentlyPressedKeys.Contains(virtualKey))
|
||||
if (InputMode == KeyInputMode.RemappedKeys)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// if no keys are pressed, clear the lists when a new key is pressed
|
||||
if (_currentlyPressedKeys.Count == 0)
|
||||
{
|
||||
if (NewMode)
|
||||
_remappedKeys.Clear();
|
||||
foreach (var keyName in formattedKeys)
|
||||
{
|
||||
_remappedKeys.Clear();
|
||||
_remappedKeys.Add(keyName);
|
||||
}
|
||||
else
|
||||
}
|
||||
else
|
||||
{
|
||||
_originalKeys.Clear();
|
||||
foreach (var keyName in formattedKeys)
|
||||
{
|
||||
_originalKeys.Clear();
|
||||
_originalKeys.Add(keyName);
|
||||
}
|
||||
|
||||
_keyPressOrder.Clear();
|
||||
}
|
||||
|
||||
// Count current modifiers
|
||||
int modifierCount = _currentlyPressedKeys.Count(k => IsModifierKey(k));
|
||||
UpdateAllAppsCheckBoxState();
|
||||
}
|
||||
|
||||
// If adding this key would exceed the limits (4 modifiers + 1 action key), don't add it and show notification
|
||||
if ((IsModifierKey(virtualKey) && modifierCount >= 4) ||
|
||||
(!IsModifierKey(virtualKey) && _currentlyPressedKeys.Count >= 5))
|
||||
public void ClearKeys()
|
||||
{
|
||||
if (InputMode == KeyInputMode.RemappedKeys)
|
||||
{
|
||||
ShowNotificationTip("Shortcuts can only have up to 4 modifier keys");
|
||||
return;
|
||||
_remappedKeys.Clear();
|
||||
}
|
||||
|
||||
// Check if this is a different variant of a modifier key already pressed
|
||||
if (IsModifierKey(virtualKey))
|
||||
else
|
||||
{
|
||||
// Remove existing variant of this modifier key if a new one is pressed
|
||||
// This is to ensure that only one variant of a modifier key is displayed at a time
|
||||
RemoveExistingModifierVariant(virtualKey);
|
||||
}
|
||||
|
||||
if (_currentlyPressedKeys.Add(virtualKey))
|
||||
{
|
||||
_keyPressOrder.Add(virtualKey);
|
||||
UpdateKeysDisplay();
|
||||
_originalKeys.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNotificationTip(string message)
|
||||
public void OnInputLimitReached()
|
||||
{
|
||||
ShowNotificationTip("Shortcuts can only have up to 4 modifier keys");
|
||||
}
|
||||
|
||||
public void CleanupKeyboardHook()
|
||||
{
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
}
|
||||
|
||||
private void RemappedToggleBtn_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Only set NewMode to true if RemappedToggleBtn is checked
|
||||
if (RemappedToggleBtn.IsChecked == true)
|
||||
{
|
||||
InputMode = KeyInputMode.RemappedKeys;
|
||||
|
||||
// Make sure OriginalToggleBtn is unchecked
|
||||
if (OriginalToggleBtn.IsChecked == true)
|
||||
{
|
||||
OriginalToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
KeyboardHookHelper.Instance.ActivateHook(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
CleanupKeyboardHook();
|
||||
}
|
||||
}
|
||||
|
||||
private void OriginalToggleBtn_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Only set NewMode to false if OriginalToggleBtn is checked
|
||||
if (OriginalToggleBtn.IsChecked == true)
|
||||
{
|
||||
InputMode = KeyInputMode.OriginalKeys;
|
||||
|
||||
// Make sure RemappedToggleBtn is unchecked
|
||||
if (RemappedToggleBtn.IsChecked == true)
|
||||
{
|
||||
RemappedToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
KeyboardHookHelper.Instance.ActivateHook(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void AllAppsCheckBox_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
|
||||
{
|
||||
RemappedToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked == true)
|
||||
{
|
||||
OriginalToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
CleanupKeyboardHook();
|
||||
|
||||
AppNameTextBox.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void AllAppsCheckBox_Unchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AppNameTextBox.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void AppNameTextBox_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Reset the focus state when the AppNameTextBox is focused
|
||||
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
|
||||
{
|
||||
RemappedToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked == true)
|
||||
{
|
||||
OriginalToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
CleanupKeyboardHook();
|
||||
}
|
||||
|
||||
public void SetRemappedKeys(List<string> keys)
|
||||
{
|
||||
_remappedKeys.Clear();
|
||||
if (keys != null)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
_remappedKeys.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateAllAppsCheckBoxState();
|
||||
}
|
||||
|
||||
public void SetOriginalKeys(List<string> keys)
|
||||
{
|
||||
_originalKeys.Clear();
|
||||
if (keys != null)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
_originalKeys.Add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetApp(bool isSpecificApp, string appName)
|
||||
{
|
||||
if (isSpecificApp)
|
||||
{
|
||||
AllAppsCheckBox.IsChecked = true;
|
||||
AppNameTextBox.Text = appName;
|
||||
AppNameTextBox.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
AllAppsCheckBox.IsChecked = false;
|
||||
AppNameTextBox.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> GetOriginalKeys()
|
||||
{
|
||||
return _originalKeys.ToList();
|
||||
}
|
||||
|
||||
public List<string> GetRemappedKeys()
|
||||
{
|
||||
return _remappedKeys.ToList();
|
||||
}
|
||||
|
||||
public bool GetIsAppSpecific()
|
||||
{
|
||||
return AllAppsCheckBox.IsChecked ?? false;
|
||||
}
|
||||
|
||||
public string GetAppName()
|
||||
{
|
||||
return AppNameTextBox.Text ?? string.Empty;
|
||||
}
|
||||
|
||||
public void SetUpToggleButtonInitialStatus()
|
||||
{
|
||||
// Ensure OriginalToggleBtn is checked
|
||||
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked != true)
|
||||
{
|
||||
OriginalToggleBtn.IsChecked = true;
|
||||
}
|
||||
|
||||
// Make sure RemappedToggleBtn is not checked
|
||||
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
|
||||
{
|
||||
RemappedToggleBtn.IsChecked = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateAllAppsCheckBoxState()
|
||||
{
|
||||
// Only enable app-specific remapping for shortcuts (multiple keys)
|
||||
bool isShortcut = _originalKeys.Count > 1;
|
||||
|
||||
AllAppsCheckBox.IsEnabled = isShortcut;
|
||||
|
||||
// If it's not a shortcut, ensure the checkbox is unchecked and app textbox is hidden
|
||||
if (!isShortcut)
|
||||
{
|
||||
AllAppsCheckBox.IsChecked = false;
|
||||
AppNameTextBox.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowNotificationTip(string message)
|
||||
{
|
||||
// If there's already an active notification, close and remove it first
|
||||
CloseExistingNotification();
|
||||
@@ -139,7 +299,7 @@ namespace KeyboardManagerEditorUI.Styles
|
||||
};
|
||||
|
||||
// Target the toggle button that triggered the notification
|
||||
currentNotification.Target = NewMode ? RemappedToggleBtn : OriginalToggleBtn;
|
||||
currentNotification.Target = InputMode == KeyInputMode.RemappedKeys ? RemappedToggleBtn : OriginalToggleBtn;
|
||||
|
||||
// Add the notification to the root panel and show it
|
||||
if (this.Content is Panel rootPanel)
|
||||
@@ -183,305 +343,6 @@ namespace KeyboardManagerEditorUI.Styles
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveExistingModifierVariant(VirtualKey key)
|
||||
{
|
||||
KeyType keyType = (KeyType)KeyboardManagerInterop.GetKeyType((int)key);
|
||||
|
||||
// No need to remove if the key is an action key
|
||||
if (keyType == KeyType.Action)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var existingKey in _currentlyPressedKeys.ToList())
|
||||
{
|
||||
if (existingKey != key)
|
||||
{
|
||||
KeyType existingKeyType = (KeyType)KeyboardManagerInterop.GetKeyType((int)existingKey);
|
||||
|
||||
// Remove the existing key if it is a modifier key and has the same type as the new key
|
||||
if (existingKeyType == keyType)
|
||||
{
|
||||
_currentlyPressedKeys.Remove(existingKey);
|
||||
_keyPressOrder.Remove(existingKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InputControl_KeyUp(int key)
|
||||
{
|
||||
VirtualKey virtualKey = (VirtualKey)key;
|
||||
|
||||
if (_currentlyPressedKeys.Remove(virtualKey))
|
||||
{
|
||||
_keyPressOrder.Remove(virtualKey);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnNewModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is InputControl control)
|
||||
{
|
||||
// Clear the lists when the mode changes
|
||||
control._currentlyPressedKeys.Clear();
|
||||
control._keyPressOrder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetKeyboardHook()
|
||||
{
|
||||
CleanupKeyboardHook();
|
||||
|
||||
_keyboardHook = new HotkeySettingsControlHook(
|
||||
InputControl_KeyDown,
|
||||
InputControl_KeyUp,
|
||||
() => true,
|
||||
(key, extraInfo) => true);
|
||||
}
|
||||
|
||||
public void CleanupKeyboardHook()
|
||||
{
|
||||
if (_keyboardHook != null)
|
||||
{
|
||||
_keyboardHook.Dispose();
|
||||
_keyboardHook = null;
|
||||
|
||||
_currentlyPressedKeys.Clear();
|
||||
_keyPressOrder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateKeysDisplay()
|
||||
{
|
||||
var formattedKeys = GetFormattedKeyList();
|
||||
|
||||
if (NewMode)
|
||||
{
|
||||
_remappedKeys.Clear();
|
||||
foreach (var key in formattedKeys)
|
||||
{
|
||||
_remappedKeys.Add(key);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_originalKeys.Clear();
|
||||
foreach (var key in formattedKeys)
|
||||
{
|
||||
_originalKeys.Add(key);
|
||||
}
|
||||
|
||||
UpdateAllAppsCheckBoxState();
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> GetFormattedKeyList()
|
||||
{
|
||||
List<string> keyList = new List<string>();
|
||||
List<VirtualKey> modifierKeys = new List<VirtualKey>();
|
||||
VirtualKey? actionKey = null;
|
||||
|
||||
foreach (var key in _keyPressOrder)
|
||||
{
|
||||
if (!_currentlyPressedKeys.Contains(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsModifierKey(key))
|
||||
{
|
||||
if (!modifierKeys.Contains(key))
|
||||
{
|
||||
modifierKeys.Add(key);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
actionKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in modifierKeys)
|
||||
{
|
||||
keyList.Add(GetKeyDisplayName((int)key));
|
||||
}
|
||||
|
||||
if (actionKey.HasValue)
|
||||
{
|
||||
keyList.Add(GetKeyDisplayName((int)actionKey.Value));
|
||||
}
|
||||
|
||||
return keyList;
|
||||
}
|
||||
|
||||
private string GetKeyDisplayName(int keyCode)
|
||||
{
|
||||
var keyName = new System.Text.StringBuilder(64);
|
||||
KeyboardManagerInterop.GetKeyDisplayName(keyCode, keyName, keyName.Capacity);
|
||||
return keyName.ToString();
|
||||
}
|
||||
|
||||
private bool IsModifierKey(VirtualKey key)
|
||||
{
|
||||
return key == VirtualKey.Control
|
||||
|| key == VirtualKey.LeftControl
|
||||
|| key == VirtualKey.RightControl
|
||||
|| key == VirtualKey.Menu
|
||||
|| key == VirtualKey.LeftMenu
|
||||
|| key == VirtualKey.RightMenu
|
||||
|| key == VirtualKey.Shift
|
||||
|| key == VirtualKey.LeftShift
|
||||
|| key == VirtualKey.RightShift
|
||||
|| key == VirtualKey.LeftWindows
|
||||
|| key == VirtualKey.RightWindows;
|
||||
}
|
||||
|
||||
public void SetRemappedKeys(List<string> keys)
|
||||
{
|
||||
_remappedKeys.Clear();
|
||||
if (keys != null)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
_remappedKeys.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateAllAppsCheckBoxState();
|
||||
}
|
||||
|
||||
public void SetOriginalKeys(List<string> keys)
|
||||
{
|
||||
_originalKeys.Clear();
|
||||
if (keys != null)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
_originalKeys.Add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> GetOriginalKeys()
|
||||
{
|
||||
return _originalKeys.ToList();
|
||||
}
|
||||
|
||||
public List<string> GetRemappedKeys()
|
||||
{
|
||||
return _remappedKeys.ToList();
|
||||
}
|
||||
|
||||
public bool GetIsAppSpecific()
|
||||
{
|
||||
return AllAppsCheckBox.IsChecked ?? false;
|
||||
}
|
||||
|
||||
public string GetAppName()
|
||||
{
|
||||
return AppNameTextBox.Text ?? string.Empty;
|
||||
}
|
||||
|
||||
private void RemappedToggleBtn_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Only set NewMode to true if RemappedToggleBtn is checked
|
||||
if (RemappedToggleBtn.IsChecked == true)
|
||||
{
|
||||
NewMode = true;
|
||||
|
||||
// Make sure OriginalToggleBtn is unchecked
|
||||
if (OriginalToggleBtn.IsChecked == true)
|
||||
{
|
||||
OriginalToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
SetKeyboardHook();
|
||||
}
|
||||
else
|
||||
{
|
||||
CleanupKeyboardHook();
|
||||
}
|
||||
}
|
||||
|
||||
private void OriginalToggleBtn_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Only set NewMode to false if OriginalToggleBtn is checked
|
||||
if (OriginalToggleBtn.IsChecked == true)
|
||||
{
|
||||
NewMode = false;
|
||||
|
||||
// Make sure RemappedToggleBtn is unchecked
|
||||
if (RemappedToggleBtn.IsChecked == true)
|
||||
{
|
||||
RemappedToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
SetKeyboardHook();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetApp(bool isSpecificApp, string appName)
|
||||
{
|
||||
if (isSpecificApp)
|
||||
{
|
||||
AllAppsCheckBox.IsChecked = true;
|
||||
AppNameTextBox.Text = appName;
|
||||
AppNameTextBox.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
AllAppsCheckBox.IsChecked = false;
|
||||
AppNameTextBox.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void AllAppsCheckBox_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
|
||||
{
|
||||
RemappedToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked == true)
|
||||
{
|
||||
OriginalToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
CleanupKeyboardHook();
|
||||
|
||||
AppNameTextBox.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void AllAppsCheckBox_Unchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AppNameTextBox.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void AppNameTextBox_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Reset the focus state when the AppNameTextBox is focused
|
||||
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
|
||||
{
|
||||
RemappedToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked == true)
|
||||
{
|
||||
OriginalToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
CleanupKeyboardHook();
|
||||
}
|
||||
|
||||
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AllAppsCheckBox.Checked += AllAppsCheckBox_Checked;
|
||||
AllAppsCheckBox.Unchecked += AllAppsCheckBox_Unchecked;
|
||||
|
||||
AppNameTextBox.GotFocus += AppNameTextBox_GotFocus;
|
||||
}
|
||||
|
||||
public void ResetToggleButtons()
|
||||
{
|
||||
// Reset toggle button status without clearing the key displays
|
||||
@@ -496,35 +357,43 @@ namespace KeyboardManagerEditorUI.Styles
|
||||
}
|
||||
}
|
||||
|
||||
public void SetUpToggleButtonInitialStatus()
|
||||
public void Reset()
|
||||
{
|
||||
// Ensure OriginalToggleBtn is checked
|
||||
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked != true)
|
||||
{
|
||||
OriginalToggleBtn.IsChecked = true;
|
||||
}
|
||||
// Reset displayed keys
|
||||
_originalKeys.Clear();
|
||||
_remappedKeys.Clear();
|
||||
|
||||
// Make sure RemappedToggleBtn is not checked
|
||||
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
|
||||
// Reset toggle button status
|
||||
if (RemappedToggleBtn != null)
|
||||
{
|
||||
RemappedToggleBtn.IsChecked = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateAllAppsCheckBoxState()
|
||||
{
|
||||
// Only enable app-specific remapping for shortcuts (multiple keys)
|
||||
bool isShortcut = _originalKeys.Count > 1;
|
||||
|
||||
AllAppsCheckBox.IsEnabled = isShortcut;
|
||||
|
||||
// If it's not a shortcut, ensure the checkbox is unchecked and app textbox is hidden
|
||||
if (!isShortcut)
|
||||
if (OriginalToggleBtn != null)
|
||||
{
|
||||
AllAppsCheckBox.IsChecked = false;
|
||||
AppNameTextBox.Visibility = Visibility.Collapsed;
|
||||
OriginalToggleBtn.IsChecked = false;
|
||||
}
|
||||
}
|
||||
|
||||
InputMode = KeyInputMode.OriginalKeys;
|
||||
|
||||
// Reset app name text box
|
||||
if (AppNameTextBox != null)
|
||||
{
|
||||
AppNameTextBox.Text = string.Empty;
|
||||
}
|
||||
|
||||
UpdateAllAppsCheckBoxState();
|
||||
|
||||
// Close any existing notifications
|
||||
CloseExistingNotification();
|
||||
|
||||
// Reset the focus status
|
||||
if (this.FocusState != FocusState.Unfocused)
|
||||
{
|
||||
this.IsTabStop = false;
|
||||
this.IsTabStop = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -546,47 +415,5 @@ namespace KeyboardManagerEditorUI.Styles
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
// Reset key status
|
||||
_currentlyPressedKeys.Clear();
|
||||
_keyPressOrder.Clear();
|
||||
|
||||
// Reset displayed keys
|
||||
_originalKeys.Clear();
|
||||
_remappedKeys.Clear();
|
||||
|
||||
// Reset toggle button status
|
||||
if (RemappedToggleBtn != null)
|
||||
{
|
||||
RemappedToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
if (OriginalToggleBtn != null)
|
||||
{
|
||||
OriginalToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
NewMode = false;
|
||||
|
||||
// Reset app name text box
|
||||
if (AppNameTextBox != null)
|
||||
{
|
||||
AppNameTextBox.Text = string.Empty;
|
||||
}
|
||||
|
||||
UpdateAllAppsCheckBoxState();
|
||||
|
||||
// Close any existing notifications
|
||||
CloseExistingNotification();
|
||||
|
||||
// Reset the focus status
|
||||
if (this.FocusState != FocusState.Unfocused)
|
||||
{
|
||||
this.IsTabStop = false;
|
||||
this.IsTabStop = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="KeyboardManagerEditorUI.Styles.TextPageInputControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:KeyboardManagerEditorUI.Styles"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Loaded="UserControl_Loaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel
|
||||
Width="360"
|
||||
Height="360"
|
||||
Orientation="Vertical">
|
||||
<!-- Shortcut section -->
|
||||
<TextBlock
|
||||
Margin="0,12,0,8"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="Shortcut key(s)" />
|
||||
|
||||
<ToggleButton
|
||||
x:Name="ShortcutToggleBtn"
|
||||
Padding="0,24,0,24"
|
||||
HorizontalAlignment="Stretch"
|
||||
Checked="ShortcutToggleBtn_Checked"
|
||||
Style="{StaticResource CustomShortcutToggleButtonStyle}">
|
||||
<ToggleButton.Content>
|
||||
<ItemsControl x:Name="ShortcutKeys">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<controls:WrapPanel HorizontalSpacing="4" Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<local:KeyVisual Content="{Binding}" VisualType="SmallOutline" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ToggleButton.Content>
|
||||
</ToggleButton>
|
||||
|
||||
<!-- Text section -->
|
||||
<TextBox
|
||||
x:Name="TextContentBox"
|
||||
Height="120"
|
||||
Margin="0,8,0,0"
|
||||
AcceptsReturn="True"
|
||||
Background="{ThemeResource TextControlBackgroundFocused}"
|
||||
BorderBrush="{ThemeResource ControlStrokeColorDefaultBrush}"
|
||||
FontSize="13"
|
||||
Header="Text to insert"
|
||||
PlaceholderText="Enter text that will be inserted when the shortcut is pressed"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<!-- App specific section -->
|
||||
<CheckBox
|
||||
x:Name="AllAppsCheckBox"
|
||||
Margin="0,8,0,0"
|
||||
Content="Only apply this text shortcut to a specific application"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
|
||||
<TextBox
|
||||
x:Name="AppNameTextBox"
|
||||
Background="{ThemeResource TextControlBackgroundFocused}"
|
||||
BorderBrush="{ThemeResource ControlStrokeColorDefaultBrush}"
|
||||
Header="Application name"
|
||||
IsEnabled="{Binding ElementName=AllAppsCheckBox, Path=IsChecked}"
|
||||
PlaceholderText="e.g.: outlook.exe" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using KeyboardManagerEditorUI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.System;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Styles
|
||||
{
|
||||
public sealed partial class TextPageInputControl : UserControl, IKeyboardHookTarget
|
||||
{
|
||||
private ObservableCollection<string> _shortcutKeys = new ObservableCollection<string>();
|
||||
private TeachingTip? currentNotification;
|
||||
private DispatcherTimer? notificationTimer;
|
||||
private bool _internalUpdate;
|
||||
|
||||
public TextPageInputControl()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.ShortcutKeys.ItemsSource = _shortcutKeys;
|
||||
|
||||
ShortcutToggleBtn.IsChecked = true;
|
||||
}
|
||||
|
||||
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
KeyboardHookHelper.Instance.ActivateHook(this);
|
||||
TextContentBox.GotFocus += TextContentBox_GotFocus;
|
||||
|
||||
AllAppsCheckBox.Checked += AllAppsCheckBox_Changed;
|
||||
AllAppsCheckBox.Unchecked += AllAppsCheckBox_Changed;
|
||||
AppNameTextBox.GotFocus += AppNameTextBox_GotFocus;
|
||||
|
||||
AppNameTextBox.Visibility = AllAppsCheckBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void ShortcutToggleBtn_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ShortcutToggleBtn.IsChecked == true)
|
||||
{
|
||||
KeyboardHookHelper.Instance.ActivateHook(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnKeyDown(VirtualKey key, List<string> formattedKeys)
|
||||
{
|
||||
_shortcutKeys.Clear();
|
||||
foreach (var keyName in formattedKeys)
|
||||
{
|
||||
_shortcutKeys.Add(keyName);
|
||||
}
|
||||
|
||||
UpdateAllAppsCheckBoxState();
|
||||
}
|
||||
|
||||
private void TextContentBox_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Clean up the keyboard hook when the text box gains focus
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
|
||||
if (ShortcutToggleBtn != null && ShortcutToggleBtn.IsChecked == true)
|
||||
{
|
||||
ShortcutToggleBtn.IsChecked = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnInputLimitReached()
|
||||
{
|
||||
ShowNotificationTip("Shortcuts can only have up to 4 modifier keys");
|
||||
}
|
||||
|
||||
public void UpdateAllAppsCheckBoxState()
|
||||
{
|
||||
// Only enable app-specific remapping for shortcuts (multiple keys)
|
||||
bool isShortcut = _shortcutKeys.Count > 1;
|
||||
|
||||
AllAppsCheckBox.IsEnabled = isShortcut;
|
||||
|
||||
// If it's not a shortcut, ensure the checkbox is unchecked and app textbox is hidden
|
||||
try
|
||||
{
|
||||
if (!isShortcut)
|
||||
{
|
||||
_internalUpdate = true;
|
||||
AllAppsCheckBox.IsChecked = false;
|
||||
AppNameTextBox.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else if (AllAppsCheckBox.IsChecked == true)
|
||||
{
|
||||
AppNameTextBox.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_internalUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AllAppsCheckBox_Changed(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_internalUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
|
||||
if (ShortcutToggleBtn != null && ShortcutToggleBtn.IsChecked == true)
|
||||
{
|
||||
ShortcutToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
AppNameTextBox.Visibility = AllAppsCheckBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void AppNameTextBox_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_internalUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
|
||||
if (ShortcutToggleBtn != null && ShortcutToggleBtn.IsChecked == true)
|
||||
{
|
||||
ShortcutToggleBtn.IsChecked = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowNotificationTip(string message)
|
||||
{
|
||||
CloseExistingNotification();
|
||||
|
||||
currentNotification = new TeachingTip
|
||||
{
|
||||
Title = "Input Limit",
|
||||
Subtitle = message,
|
||||
IsLightDismissEnabled = true,
|
||||
PreferredPlacement = TeachingTipPlacementMode.Top,
|
||||
XamlRoot = this.XamlRoot,
|
||||
IconSource = new SymbolIconSource { Symbol = Symbol.Important },
|
||||
Target = ShortcutToggleBtn,
|
||||
};
|
||||
|
||||
if (this.Content is Panel rootPanel)
|
||||
{
|
||||
rootPanel.Children.Add(currentNotification);
|
||||
currentNotification.IsOpen = true;
|
||||
|
||||
notificationTimer = new DispatcherTimer();
|
||||
notificationTimer.Interval = TimeSpan.FromMilliseconds(EditorConstants.DefaultNotificationTimeout);
|
||||
notificationTimer.Tick += (s, e) =>
|
||||
{
|
||||
CloseExistingNotification();
|
||||
};
|
||||
notificationTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseExistingNotification()
|
||||
{
|
||||
if (notificationTimer != null)
|
||||
{
|
||||
notificationTimer.Stop();
|
||||
notificationTimer = null;
|
||||
}
|
||||
|
||||
if (currentNotification != null && currentNotification.IsOpen)
|
||||
{
|
||||
currentNotification.IsOpen = false;
|
||||
|
||||
if (this.Content is Panel rootPanel && rootPanel.Children.Contains(currentNotification))
|
||||
{
|
||||
rootPanel.Children.Remove(currentNotification);
|
||||
}
|
||||
|
||||
currentNotification = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearKeys()
|
||||
{
|
||||
_shortcutKeys.Clear();
|
||||
UpdateAllAppsCheckBoxState();
|
||||
}
|
||||
|
||||
public List<string> GetShortcutKeys()
|
||||
{
|
||||
List<string> keys = new List<string>();
|
||||
|
||||
foreach (var key in _shortcutKeys)
|
||||
{
|
||||
keys.Add(key);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
public string GetTextContent()
|
||||
{
|
||||
return TextContentBox.Text;
|
||||
}
|
||||
|
||||
public bool GetIsAppSpecific()
|
||||
{
|
||||
return AllAppsCheckBox.IsChecked ?? false;
|
||||
}
|
||||
|
||||
public string GetAppName()
|
||||
{
|
||||
return AllAppsCheckBox.IsChecked == true ? AppNameTextBox.Text : string.Empty;
|
||||
}
|
||||
|
||||
public void SetShortcutKeys(List<string> keys)
|
||||
{
|
||||
if (keys != null)
|
||||
{
|
||||
_shortcutKeys.Clear();
|
||||
foreach (var key in keys)
|
||||
{
|
||||
_shortcutKeys.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateAllAppsCheckBoxState();
|
||||
}
|
||||
|
||||
public void SetTextContent(string text)
|
||||
{
|
||||
TextContentBox.Text = text;
|
||||
}
|
||||
|
||||
public void SetAppSpecific(bool isAppSpecific, string appName)
|
||||
{
|
||||
AllAppsCheckBox.IsChecked = isAppSpecific;
|
||||
if (isAppSpecific)
|
||||
{
|
||||
AppNameTextBox.Text = appName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user