Dreamine.UI.Wpf.Equipment 1.0.1
Dreamine.UI.Wpf.Equipment 사용자 인터페이스 기능과 구성 요소를 제공합니다.
로딩중...
검색중...
일치하는것 없음
DreamineVirtualKeyboard.cs
이 파일의 문서화 페이지로 가기
1using SharpHook;
2using SharpHook.Native;
3using System;
4using System.ComponentModel;
5using System.Diagnostics;
6using System.Linq;
7using System.Threading;
8using System.Threading.Tasks;
9using System.Windows;
10using System.Windows.Controls;
11using System.Windows.Input;
12using System.Windows.Media;
13using System.Windows.Threading;
14using Dreamine.UI.Abstractions.VirtualKeyboard;
15
17
26public class DreamineVirtualKeyboard : UserControl, IDisposable
27{
28 #region Event Handler
29
38 public EventHandler<LanguageCode>? OnKeyboardLanguageChanged { get; set; }
39
40 #endregion
41
42 #region Variable
43
52 private readonly EventSimulator _eventSimulator = new EventSimulator();
61 private readonly HangulComposer _hangulComposer = new();
70 private readonly DispatcherTimer _keyboardStateSyncTimer;
79 private TaskPoolGlobalHook _hook = null!;
80
89 private volatile bool _hookRunning;
90
99 private volatile bool _hookSubscribed;
100
109 protected Panel? _layoutRoot;
118 protected IEnumerable<Key>? _keys;
127 protected Button? _langBtn;
136 protected Button? _inputModeBtn;
137
146 private CancellationTokenSource? _repeatBackspaceCts;
156
165 private DateTime _lastSelfImeToggleAt = DateTime.MinValue;
174 private DateTime _lastSelfLangSwitchAt = DateTime.MinValue;
183 private DateTime _lastSelfShiftAt = DateTime.MinValue;
201 private DateTime _pendingKoreanInputModeUntil = DateTime.MinValue;
210 private static readonly TimeSpan _guard = TimeSpan.FromMilliseconds(350);
219 private bool InImeGuard => (DateTime.UtcNow - _lastSelfImeToggleAt) < _guard;
228 private bool InLangGuard => (DateTime.UtcNow - _lastSelfLangSwitchAt) < _guard;
237 private bool HasPendingKoreanInputMode => _pendingKoreanInputMode.HasValue && DateTime.UtcNow < _pendingKoreanInputModeUntil;
238 // 문자 입력 시 대문자/기호를 위해 스스로 Shift를 눌렀다 떼는 동안, 전역 훅이
239 // 그걸 실제 Shift 토글로 오인해 상태를 뒤집지 않도록 하는 가드.
248 private bool InShiftGuard => (DateTime.UtcNow - _lastSelfShiftAt) < _guard;
249
258 private bool _disposed;
259
260 #endregion
261
262 #region Routed Event
263
272 public static readonly RoutedEvent VirtualKeyDownEvent = EventManager.
273 RegisterRoutedEvent(nameof(VirtualKeyDown), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DreamineVirtualKeyboard));
274
283 public event RoutedEventHandler VirtualKeyDown
284 {
285 add { AddHandler(VirtualKeyDownEvent, value); }
286 remove { RemoveHandler(VirtualKeyDownEvent, value); }
287 }
288
289 #endregion
290
291 #region Dependency Property
292
293 #region Layout
294
303 public static readonly DependencyProperty LayoutProperty =
304 DependencyProperty.Register("Layout", typeof(VkLayout), typeof(DreamineVirtualKeyboard), new PropertyMetadata(VkLayout.Text, OnLayoutChanged));
305
314 public VkLayout Layout
315 {
316 get { return (VkLayout)GetValue(LayoutProperty); }
317 set { SetValue(LayoutProperty, value); }
318 }
319
344 private static void OnLayoutChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
345 {
346 if (d is DreamineVirtualKeyboard vk)
347 {
348 var layout = (VkLayout)e.NewValue;
349 vk._keys = null;
350 vk._langBtn = null;
351 vk._inputModeBtn = null;
352
353 vk.IsPasswordLayout = layout == VkLayout.Password;
354
355 vk.Dispatcher.BeginInvoke(() =>
356 {
357 vk.RefreshKeyboardLayout();
358 vk.EnsurePreviewFocus();
359 vk.TryTogglePreviewVisuals();
360 }, DispatcherPriority.Loaded);
361 }
362 }
363
364 #endregion
365
366 #region IsPasswordLayout (Readonly DP)
367
376 private static readonly DependencyPropertyKey IsPasswordLayoutPropertyKey =
377 DependencyProperty.RegisterReadOnly(nameof(IsPasswordLayout), typeof(bool), typeof(DreamineVirtualKeyboard), new PropertyMetadata(false, OnIsPasswordLayoutChanged));
378
387 public static readonly DependencyProperty IsPasswordLayoutProperty = IsPasswordLayoutPropertyKey.DependencyProperty;
388
398 {
399 get => (bool)GetValue(IsPasswordLayoutProperty);
400 private set => SetValue(IsPasswordLayoutPropertyKey, value);
401 }
402
427 private static void OnIsPasswordLayoutChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
428 {
429 if (d is DreamineVirtualKeyboard vk)
430 {
431 vk.TryTogglePreviewVisuals();
432 }
433 }
434
435 #endregion
436
437 #endregion
438
439 #region Property
440
449 public LanguageCode CurrentLang { get; private set; }
458 public bool IsPressedShift { get; private set; }
459
476 public bool IsPressedCapsLock => Keyboard.IsKeyToggled(System.Windows.Input.Key.CapsLock);
477
486 public bool IsPressedCtrl { get; set; }
495 public bool ImeMode { get; private set; }
496
497 #endregion
498
499 #region Constructor
500
510 {
511 _keyboardStateSyncTimer = new DispatcherTimer(DispatcherPriority.Background)
512 {
513 Interval = TimeSpan.FromMilliseconds(160),
514 };
515 _keyboardStateSyncTimer.Tick += SynchronizeKeyboardState;
516
519 IsVisibleChanged += KeyboardUserControl_IsVisibleChanged;
520 AddHandler(PreviewMouseLeftButtonUpEvent, (MouseButtonEventHandler)BackspacePointerReleased, true);
521 AddHandler(PreviewTouchUpEvent, (EventHandler<TouchEventArgs>)BackspacePointerReleased, true);
522 AddHandler(LostMouseCaptureEvent, (MouseEventHandler)BackspaceMouseCaptureLost, true);
523 InputLanguageManager.Current.InputLanguageChanged += OnInputLanguageChanged;
524
526 }
527
528 #endregion
529
530 #region Private Method
531
540 private void FindElements()
541 {
542 if (_layoutRoot is null && FindName("KeyboardLayoutRoot") is Panel layoutRoot)
543 {
544 _layoutRoot = layoutRoot;
545 }
546
547 if (_layoutRoot != null && _langBtn is null && _layoutRoot.FindFirstVisualChild<Button>("_langBtn") is { } langBtn)
548 {
549 _langBtn = langBtn;
550 }
551
552 if (_layoutRoot != null && _inputModeBtn is null && _layoutRoot.FindFirstVisualChild<Button>("_inputModeBtn") is { } inputModeBtn)
553 {
554 _inputModeBtn = inputModeBtn;
555 }
556
557 if (_layoutRoot != null && (_keys is null || !_keys.Any()))
558 {
559 _keys = _layoutRoot.FindVisualChildren<Key>();
560 }
561 }
562
579 private async Task RegisterHookEventsAsync()
580 {
581 await Task.Yield();
582 if (_hookRunning)
583 return;
584
585 _hook ??= new TaskPoolGlobalHook();
586
587 if (!_hookSubscribed)
588 {
589 _hook.KeyPressed += Hook_KeyPressed;
590 _hook.KeyReleased += Hook_KeyReleased;
591 _hook.MouseReleased += Hook_MouseReleased;
592 _hookSubscribed = true;
593 }
594
595 _hookRunning = true;
596 _ = Task.Run(async () =>
597 {
598 try
599 {
600 await _hook.RunAsync().ConfigureAwait(false);
601 }
602 catch (Exception ex)
603 {
604 Debug.WriteLine($"[VK Hook] RunAsync failed: {ex.Message}");
605 }
606 finally
607 {
608 _hookRunning = false;
609 }
610 });
611 }
612
621 private void UnregisterHookEvents()
622 {
623 if (_hook is null)
624 return;
625
626 // 실행 중이면 중지
627 if (_hookRunning)
628 {
629 _hook.Dispose();
630 _hookRunning = false;
631 }
632
633 // 이벤트 구독 해제 (필요 시 완전 해제)
634 if (_hookSubscribed)
635 {
636 _hook.KeyPressed -= Hook_KeyPressed;
637 _hook.KeyReleased -= Hook_KeyReleased;
638 _hook.MouseReleased -= Hook_MouseReleased;
639 _hookSubscribed = false;
640 }
641
642 //_hook = null; // 재사용 의도가 없으면 해제
643 }
644
653 protected void UpdateKeys()
654 {
656
657 if (!InImeGuard)
659
660 if (_keys != null && _keys.Any())
661 {
662 foreach (var keyButton in _keys)
663 {
664 var useShift = Layout == VkLayout.Text || Layout == VkLayout.Password;
665 keyButton.UpdateKey(useShift ? IsPressedShift : false, IsPressedCapsLock, CurrentLang, ImeMode);
666 }
667 }
668 }
669
678 private void EnsurePreviewFocus()
679 {
680 if (_layoutRoot == null)
681 _layoutRoot = FindName("KeyboardLayoutRoot") as Panel;
682
683 if (Layout == VkLayout.Password)
684 {
685 if (_layoutRoot?.FindFirstVisualChild<PasswordBox>("VkbPasswordBox") is { } pb && !pb.IsKeyboardFocusWithin)
686 {
687 pb.Focus();
688 }
689 }
690 else
691 {
692 if (_layoutRoot?.FindFirstVisualChild<TextBox>("VkbTextBox") is { } tb && !tb.IsKeyboardFocusWithin)
693 {
694 tb.Focus();
695 tb.CaretIndex = tb.Text?.Length ?? 0;
696 }
697 }
698 }
699
708 protected void UpdateLangKey()
709 {
710 if (_langBtn != null)
711 {
712 _langBtn.Visibility = Visibility.Collapsed;
713 }
714 }
715
740 protected bool UpdateInputModeKey(bool readSystemIme = true)
741 {
742 var imeModeChanged = false;
743
744 if (readSystemIme && !InImeGuard)
745 {
746 var imeMode = ImeHelper.GetImeMode();
747 if (ImeMode != imeMode)
748 {
749 ImeMode = imeMode;
750 imeModeChanged = true;
751 }
752 }
753
754 if (_inputModeBtn != null)
755 {
756 _inputModeBtn.Content = CurrentLang == LanguageCode.ko_KR && ImeMode ? "가" : "abc";
757 }
758
759 return imeModeChanged;
760 }
761
778 protected void RefreshKeyboardLayout(string lang = "")
779 {
780 SetLanguage(lang);
781 FindElements();
783 UpdateKeys();
784 }
785
810 private void SynchronizeKeyboardState(object? sender, EventArgs e)
811 {
812 if (!IsVisible || DesignerProperties.GetIsInDesignMode(this))
813 return;
814
816 {
818 return;
819 }
820
822
823 var previousLang = CurrentLang;
824 var previousImeMode = ImeMode;
825
826 if (!InLangGuard)
827 SetLanguage(InputLanguageManager.Current.CurrentInputLanguage.Name);
828
830
831 if (previousLang != CurrentLang || previousImeMode != ImeMode)
832 UpdateKeys();
833 }
834
860 {
861 if (Layout != VkLayout.Text && Layout != VkLayout.Password)
862 return false;
863
864 if (key.KeyCode < KeyCode.VcA || key.KeyCode > KeyCode.VcZ)
865 return false;
866
868 if (!HangulComposer.IsComposableJamo(text))
869 return false;
870
871 var edit = _hangulComposer.Input(text, GetTextBeforeCaret());
872 ReplaceTextTail(edit.ReplaceCount, edit.Text);
873 return true;
874 }
875
892 private void InsertRawText(string text)
893 {
894 _hangulComposer.Reset();
895 ReplaceTextTail(0, text);
896 }
897
922 private void ReplaceTextTail(int replaceCount, string text)
923 {
924 if (Layout == VkLayout.Password)
925 {
926 var password = VkbPasswordBoxFromTree()?.Password ?? string.Empty;
927 var keep = Math.Max(0, password.Length - replaceCount);
928 SetPassword(password[..keep] + text);
929 return;
930 }
931
932 if (VkbTextBoxFromTree() is not { } textBox)
933 return;
934
935 var currentText = textBox.Text ?? string.Empty;
936 var selectionStart = Math.Clamp(textBox.SelectionStart, 0, currentText.Length);
937 var selectionLength = Math.Clamp(textBox.SelectionLength, 0, currentText.Length - selectionStart);
938
939 if (selectionLength > 0)
940 {
941 textBox.SelectedText = text;
942 textBox.CaretIndex = selectionStart + text.Length;
943 }
944 else
945 {
946 var removeStart = Math.Max(0, selectionStart - replaceCount);
947 removeStart = Math.Min(removeStart, currentText.Length);
948 var removeLength = Math.Clamp(selectionStart - removeStart, 0, currentText.Length - removeStart);
949 textBox.Text = currentText.Remove(removeStart, removeLength).Insert(removeStart, text);
950 textBox.CaretIndex = removeStart + text.Length;
951 }
952
953 textBox.GetBindingExpression(TextBox.TextProperty)?.UpdateSource();
954 }
955
972 private string GetTextBeforeCaret()
973 {
974 if (Layout == VkLayout.Password)
975 return VkbPasswordBoxFromTree()?.Password ?? string.Empty;
976
977 if (VkbTextBoxFromTree() is not { } textBox)
978 return string.Empty;
979
980 var caret = Math.Clamp(textBox.CaretIndex, 0, textBox.Text.Length);
981 return textBox.Text[..caret];
982 }
983
1000 private void SetPassword(string password)
1001 {
1002 if (VkbPasswordBoxFromTree() is { } passwordBox)
1003 passwordBox.Password = password;
1004 }
1005
1022 private TextBox? VkbTextBoxFromTree()
1023 {
1024 var window = Window.GetWindow(this) ?? this as DependencyObject;
1025 return window == null ? null : FindByNameInVisualTree<TextBox>(window, "VkbTextBox");
1026 }
1027
1044 private PasswordBox? VkbPasswordBoxFromTree()
1045 {
1046 var window = Window.GetWindow(this) ?? this as DependencyObject;
1047 return window == null ? null : FindByNameInVisualTree<PasswordBox>(window, "VkbPasswordBox");
1048 }
1049
1066 private void SimulateCharKey(Key key)
1067 {
1068 var kc = key.KeyCode;
1069
1070 // 대문자/기호는 '가상 Shift 토글'로만 결정한다.
1071 // CapsLock(영문 대문자화)은 OS가 주입된 문자 키에 자동 적용하므로 여기서 감싸지 않는다.
1072 var useShift = IsPressedShift;
1073
1074 if (useShift)
1075 {
1076 _lastSelfShiftAt = DateTime.UtcNow;
1077 _eventSimulator.SimulateKeyPress(KeyCode.VcLeftShift);
1078 _eventSimulator.SimulateKeyPress(kc);
1079 _eventSimulator.SimulateKeyRelease(kc);
1080 _eventSimulator.SimulateKeyRelease(KeyCode.VcLeftShift);
1081 _lastSelfShiftAt = DateTime.UtcNow;
1082 }
1083 else
1084 {
1085 _eventSimulator.SimulateKeyPress(kc);
1086 _eventSimulator.SimulateKeyRelease(kc);
1087 }
1088 }
1089
1099 {
1100 if (!Dispatcher.CheckAccess())
1101 {
1102 Dispatcher.BeginInvoke(new Action(ReleaseBackspaceRepeat), DispatcherPriority.Send);
1103 return;
1104 }
1105
1106 if (_repeatBackspaceCts != null)
1107 {
1108 _repeatBackspaceCts.Cancel();
1109 _repeatBackspaceCts.Dispose();
1110 _repeatBackspaceCts = null;
1111 }
1112
1113 if (_repeatBackspaceKey?.IsMouseCaptured == true)
1114 _repeatBackspaceKey.ReleaseMouseCapture();
1115
1116 _repeatBackspaceKey = null;
1117 _eventSimulator.SimulateKeyRelease(KeyCode.VcBackspace);
1118 }
1119
1136 private void StartBackspaceRepeat(Key key)
1137 {
1138 if (_repeatBackspaceCts != null)
1139 return;
1140
1141 _repeatBackspaceKey = key;
1142 key.CaptureMouse();
1144
1145 _repeatBackspaceCts = new CancellationTokenSource();
1146 var token = _repeatBackspaceCts.Token;
1147
1148 _ = Task.Run(async () =>
1149 {
1150 try
1151 {
1152 await Task.Delay(400, token).ConfigureAwait(false);
1153 while (!token.IsCancellationRequested)
1154 {
1156 await Task.Delay(50, token).ConfigureAwait(false);
1157 }
1158 }
1159 catch (OperationCanceledException)
1160 {
1161 }
1162 }, token);
1163 }
1164
1174 {
1175 _eventSimulator.SimulateKeyPress(KeyCode.VcBackspace);
1176 _eventSimulator.SimulateKeyRelease(KeyCode.VcBackspace);
1177 }
1178
1203 private string NormalizeLanguageString(string lang)
1204 {
1205 if (lang == null)
1206 return "";
1207
1208 char[] hiddenChars =
1209 {
1210 '\u200B', // zero width space
1211 '\u200C',
1212 '\u200D',
1213 '\u2060',
1214 '\uFEFF', // BOM
1215 '\u00A0' // NBSP
1216 };
1217
1218 return new string(lang.Where(c => !hiddenChars.Contains(c)).ToArray()).Trim();
1219 }
1220
1237 private void SetLanguage(string lang)
1238 {
1239 if (string.IsNullOrEmpty(lang))
1240 {
1241 lang = InputLanguageManager.Current.CurrentInputLanguage.Name;
1242 }
1243
1244 lang = NormalizeLanguageString(lang);
1245
1246 var currentLang = lang switch
1247 {
1248 "ko-KR" => LanguageCode.ko_KR,
1249 "vi-VN" => LanguageCode.vi_VN,
1250 "zh-CN" => LanguageCode.zh_CN,
1251 _ => LanguageCode.en_US
1252 };
1253
1254 if (CurrentLang == currentLang)
1255 return;
1256
1257 CurrentLang = currentLang;
1259 }
1260
1285 private bool SetKoreanEnglishMode(bool useKorean)
1286 {
1287 _lastSelfLangSwitchAt = DateTime.UtcNow;
1288 _lastSelfImeToggleAt = DateTime.UtcNow;
1289 _pendingKoreanInputMode = useKorean;
1290 _pendingKoreanInputModeUntil = DateTime.UtcNow + TimeSpan.FromMilliseconds(900);
1291
1292 var targetName = useKorean ? "ko-KR" : "en-US";
1293 try
1294 {
1295 var available = InputLanguageManager.Current.AvailableInputLanguages;
1296 var match = available?
1297 .Cast<System.Globalization.CultureInfo>()
1298 .FirstOrDefault(c => string.Equals(c.Name, targetName, StringComparison.OrdinalIgnoreCase));
1299
1300 if (match != null)
1301 {
1302 InputLanguageManager.Current.CurrentInputLanguage = match;
1303 SetLanguage(match.Name);
1304 }
1305 else
1306 {
1307 SetLanguage(targetName);
1308 }
1309
1311 ApplyDesiredImeMode(useKorean);
1313 RecheckKeyboardStateSoon(useKorean);
1314 return true;
1315 }
1316 catch (Exception ex)
1317 {
1318 Debug.WriteLine($"[VK] SetKoreanEnglishMode failed: {ex.Message}");
1319 }
1320
1321 return false;
1322 }
1323
1340 private void ApplyDesiredImeMode(bool useKorean)
1341 {
1342 var currentImeMode = ImeHelper.GetImeMode();
1343 if (currentImeMode != useKorean)
1344 {
1345 if (!ImeHelper.SetImeMode(useKorean))
1346 {
1347 switch (CurrentLang)
1348 {
1349 case LanguageCode.ko_KR:
1350 _eventSimulator.SimulateKeyPress(KeyCode.VcHangul);
1351 _eventSimulator.SimulateKeyRelease(KeyCode.VcHangul);
1352 break;
1353 case LanguageCode.zh_CN:
1354 _eventSimulator.SimulateKeyPress(KeyCode.VcLeftShift);
1355 _eventSimulator.SimulateKeyRelease(KeyCode.VcLeftShift);
1356 break;
1357 }
1358 }
1359 }
1360
1361 ImeMode = useKorean;
1362 }
1363
1380 private bool IsKoreanInputActive()
1381 {
1383 return _pendingKoreanInputMode == true;
1384
1385 var currentLanguage = InputLanguageManager.Current.CurrentInputLanguage.Name;
1386 var isKoreanLanguage = string.Equals(
1387 NormalizeLanguageString(currentLanguage),
1388 "ko-KR",
1389 StringComparison.OrdinalIgnoreCase);
1390
1391 return isKoreanLanguage && ImeHelper.GetImeMode();
1392 }
1393
1410 private void RecheckKeyboardStateSoon(bool useKorean)
1411 {
1412 var remaining = 5;
1413 DispatcherTimer timer = new() { Interval = TimeSpan.FromMilliseconds(100) };
1414 timer.Tick += (_, __) =>
1415 {
1417 ApplyDesiredImeMode(useKorean);
1419
1420 if (--remaining <= 0)
1421 {
1422 timer.Stop();
1425 }
1426 };
1427 timer.Start();
1428 }
1429
1446 private void RefreshKeyboardVisualState(bool readSystemIme = true)
1447 {
1449 {
1451 readSystemIme = false;
1452 }
1453
1454 UpdateInputModeKey(readSystemIme);
1455 UpdateKeys();
1457 }
1458
1475 private bool IsNumLockOn()
1476 {
1477 return Convert.ToBoolean(Win32Api.GetKeyState((int)KeyCode.VcNumLock) & 0x0001);
1478 }
1479
1489 {
1490 var window = Window.GetWindow(this) ?? this as DependencyObject;
1491 if (window == null) return;
1492
1493 var vkbText = FindByNameInVisualTree<TextBox>(window, "VkbTextBox");
1494 var vkbPwd = FindByNameInVisualTree<PasswordBox>(window, "VkbPasswordBox");
1495
1496 if (vkbText != null)
1497 vkbText.Visibility = IsPasswordLayout ? Visibility.Collapsed : Visibility.Visible;
1498
1499 if (vkbPwd != null)
1500 vkbPwd.Visibility = IsPasswordLayout ? Visibility.Visible : Visibility.Collapsed;
1501 }
1502
1543 private static T? FindByNameInVisualTree<T>(DependencyObject root, string name) where T : FrameworkElement
1544 {
1545 if (root is T fe && fe.Name == name)
1546 return fe;
1547
1548 int count = VisualTreeHelper.GetChildrenCount(root);
1549 for (int i = 0; i < count; i++)
1550 {
1551 var child = VisualTreeHelper.GetChild(root, i);
1552 var found = FindByNameInVisualTree<T>(child, name);
1553 if (found != null) return found;
1554 }
1555 return null;
1556 }
1557
1558#endregion
1559
1560 #region Event Handler
1561
1586 private void OnInputLanguageChanged(object? sender, InputLanguageEventArgs e)
1587 {
1588 _hangulComposer.Reset();
1589
1590 if (InLangGuard)
1591 {
1592 SetLanguage(e.NewLanguage.Name);
1594 UpdateKeys();
1596 return;
1597 }
1598
1599 RefreshKeyboardLayout(e.NewLanguage.Name);
1601 }
1602
1627 private void KeyboardUserControl_Loaded(object? sender, RoutedEventArgs e)
1628 {
1629 if (DesignerProperties.GetIsInDesignMode(this))
1630 return;
1631
1632 RemoveHandler(Key.ClickEvent, (RoutedEventHandler)KeyClick);
1633 AddHandler(Key.ClickEvent, (RoutedEventHandler)KeyClick);
1634
1635 Dispatcher.BeginInvoke(() =>
1636 {
1641 }, DispatcherPriority.SystemIdle);
1642 }
1643
1668 private void KeyboardUserControl_Unloaded(object? sender, RoutedEventArgs e)
1669 {
1671 Dispose(); // 안전
1672 }
1673
1698 private void KeyboardUserControl_IsVisibleChanged(object? sender, DependencyPropertyChangedEventArgs e)
1699 {
1700 if (e.NewValue is not bool isVisible) return;
1701
1702 if (!isVisible)
1703 {
1705
1706 if (IsPressedShift)
1707 {
1708 IsPressedShift = false;
1709 _eventSimulator.SimulateKeyRelease(KeyCode.VcLeftShift);
1710 }
1711 // CapsLock은 OS 상태를 그대로 두고 건드리지 않는다(단일 소스).
1712 }
1713 else if (IsLoaded)
1714 {
1715 SynchronizeKeyboardState(this, EventArgs.Empty);
1717 }
1718 }
1719
1744 private void Hook_KeyPressed(object? sender, KeyboardHookEventArgs e)
1745 {
1746 var keyCode = e.Data.KeyCode;
1747 Dispatcher.BeginInvoke(() =>
1748 {
1749 var refreshLayout = false;
1750 if (UpdateInputModeKey())
1751 refreshLayout = true;
1752
1753 switch (keyCode)
1754 {
1755 case KeyCode.VcLeftShift:
1756 case KeyCode.VcRightShift:
1757 // 문자 입력용 자체 Shift 시뮬레이션은 무시(토글 상태 보존).
1758 if (!InShiftGuard)
1759 {
1760 if (Layout == VkLayout.Text || Layout == VkLayout.Password)
1761 IsPressedShift = true;
1762 if (_keys?.FirstOrDefault(k => k.KeyCode == KeyCode.VcLeftShift) is { } shiftKey)
1763 shiftKey.IsPressed = true;
1764 refreshLayout = true;
1765 }
1766 break;
1767 case KeyCode.VcCapsLock:
1768 // OS CapsLock 토글 결과를 표시에 반영(상태 자체는 OS가 소유).
1769 if (_keys?.FirstOrDefault(k => k.KeyCode == KeyCode.VcCapsLock) is { } capsLockKey)
1770 capsLockKey.IsPressed = IsPressedCapsLock;
1771 refreshLayout = true;
1772 break;
1773 case KeyCode.VcLeftControl:
1774 case KeyCode.VcRightControl:
1775 if (Layout == VkLayout.Text || Layout == VkLayout.Password)
1776 IsPressedCtrl = true;
1777 if (_keys?.FirstOrDefault(k => k.KeyCode == KeyCode.VcLeftControl) is { } ctrlKey)
1778 ctrlKey.IsPressed = true;
1779 break;
1780 default:
1781 if (keyCode >= KeyCode.VcA && keyCode <= KeyCode.VcZ)
1782 _hangulComposer.Reset();
1783
1784 if (_keys?.FirstOrDefault(k => k.KeyCode == keyCode) is { } keyBtn)
1785 keyBtn.IsPressed = true;
1786 break;
1787 }
1788
1789 if (refreshLayout)
1790 UpdateKeys();
1791 }, DispatcherPriority.ApplicationIdle);
1792 }
1793
1818 private void Hook_KeyReleased(object? sender, KeyboardHookEventArgs e)
1819 {
1820 var keyCode = e.Data.KeyCode;
1821 Dispatcher.BeginInvoke(() =>
1822 {
1823 switch (keyCode)
1824 {
1825 case KeyCode.VcLeftShift:
1826 case KeyCode.VcRightShift:
1827 if (!InShiftGuard)
1828 {
1829 IsPressedShift = false;
1830 if (_keys?.FirstOrDefault(k => k.KeyCode == KeyCode.VcLeftShift) is { } shiftKey)
1831 shiftKey.IsPressed = false;
1832 UpdateKeys();
1833 }
1834 break;
1835 case KeyCode.VcCapsLock:
1836 if (_keys?.FirstOrDefault(k => k.KeyCode == KeyCode.VcCapsLock) is { } capsLockKey)
1837 capsLockKey.IsPressed = IsPressedCapsLock;
1838 UpdateKeys();
1839 break;
1840 case KeyCode.VcLeftControl:
1841 case KeyCode.VcRightControl:
1842 IsPressedCtrl = false;
1843 if (_keys?.FirstOrDefault(k => k.KeyCode == KeyCode.VcLeftControl) is { } ctrlKey)
1844 ctrlKey.IsPressed = false;
1845 break;
1846 default:
1847 if (_keys?.FirstOrDefault(k => k.KeyCode == keyCode) is { } keyBtn)
1848 keyBtn.IsPressed = false;
1849 break;
1850 }
1851 }, DispatcherPriority.ApplicationIdle);
1852 }
1853
1878 private void Hook_MouseReleased(object? sender, MouseHookEventArgs e)
1879 {
1881 }
1882
1907 private void BackspacePointerReleased(object? sender, InputEventArgs e)
1908 {
1910 }
1911
1936 private void BackspaceMouseCaptureLost(object? sender, MouseEventArgs e)
1937 {
1938 if (_repeatBackspaceCts != null && _repeatBackspaceKey?.IsMouseCaptured != true)
1940 }
1941
1966 private void KeyClick(object? sender, RoutedEventArgs e)
1967 {
1968 if (Layout != VkLayout.Text && Layout != VkLayout.Password)
1969 {
1970 _eventSimulator.SimulateKeyRelease(KeyCode.VcLeftShift);
1971 _eventSimulator.SimulateKeyRelease(KeyCode.VcRightShift);
1972
1973 if (!IsNumLockOn())
1974 {
1975 _eventSimulator.SimulateKeyPress(KeyCode.VcNumLock);
1976 _eventSimulator.SimulateKeyRelease(KeyCode.VcNumLock);
1977 }
1978 }
1979
1980 if (e.OriginalSource is Button { Name: "_langBtn" })
1981 {
1982 _hangulComposer.Reset();
1984 return;
1985 }
1986
1987 if (e.OriginalSource is Button { Name: "_inputModeBtn" })
1988 {
1989 _hangulComposer.Reset();
1991 return;
1992 }
1993
1994 if (e.OriginalSource is not Key key)
1995 return;
1996
1997 var keyCode = key.KeyCode;
1998
1999 switch (keyCode)
2000 {
2001 // Shift는 토글(lock). 물리 Shift를 눌러두지 않고 상태만 들고 있다가,
2002 // 문자 입력 때 SimulateCharKey에서 한 글자씩 감싼다. → 전역 훅 없이도 동작.
2003 case KeyCode.VcLeftShift:
2004 case KeyCode.VcRightShift:
2006 key.IsPressed = IsPressedShift;
2007 UpdateKeys();
2008 break;
2009
2010 // CapsLock: 실제 OS CapsLock을 토글하고, 반영은 OS 상태를 다시 읽어서 한다.
2011 case KeyCode.VcCapsLock:
2012 _eventSimulator.SimulateKeyPress(keyCode);
2013 _eventSimulator.SimulateKeyRelease(keyCode);
2014 DispatcherTimer tCaps = new() { Interval = TimeSpan.FromMilliseconds(120) };
2015 tCaps.Tick += (_, __) =>
2016 {
2017 tCaps.Stop();
2018 key.IsPressed = IsPressedCapsLock;
2019 UpdateKeys();
2020 };
2021 tCaps.Start();
2022 break;
2023
2024 case KeyCode.VcLeftControl:
2026 key.IsPressed = IsPressedCtrl;
2027 if (IsPressedCtrl) _eventSimulator.SimulateKeyPress(keyCode);
2028 else _eventSimulator.SimulateKeyRelease(keyCode);
2029 break;
2030
2031 case KeyCode.VcBackspace:
2032 _hangulComposer.Reset();
2034 break;
2035
2036 case KeyCode.VcEnter:
2037 _hangulComposer.Reset();
2038 _eventSimulator.SimulateKeyPress(keyCode);
2039 _eventSimulator.SimulateKeyRelease(keyCode);
2040 break;
2041
2042 case KeyCode.VcTab:
2043 _hangulComposer.Reset();
2044 _eventSimulator.SimulateTextEntry(" ");
2045 break;
2046
2047 case KeyCode.VcSpace:
2048 InsertRawText(" ");
2049 break;
2050
2051 default:
2052 if (IsPressedCtrl)
2053 {
2054 _hangulComposer.Reset();
2055 // Ctrl 조합(단축키)은 한 번만 적용하고 해제(one-shot).
2056 _eventSimulator.SimulateKeyPress(keyCode);
2057 _eventSimulator.SimulateKeyRelease(keyCode);
2058 _eventSimulator.SimulateKeyRelease(KeyCode.VcLeftControl);
2059 IsPressedCtrl = false;
2060 if (_keys?.FirstOrDefault(k => k.KeyCode == KeyCode.VcLeftControl) is { } ctrlKey)
2061 ctrlKey.IsPressed = false;
2062 }
2063 else
2064 {
2065 if (!TryHandleComposedTextKey(key))
2066 {
2067 _hangulComposer.Reset();
2068 SimulateCharKey(key);
2069 }
2070 }
2071 break;
2072 }
2073
2074 RaiseEvent(new RoutedEventArgs(VirtualKeyDownEvent, key.KeyCode));
2075 }
2076
2077 #endregion
2078
2079 #region Disposable
2080
2089 public void Dispose() => Dispose(true);
2090
2107 protected virtual void Dispose(bool disposing)
2108 {
2109 if (_disposed) return;
2110 _disposed = true;
2111
2112 try
2113 {
2114 // 공통: 눌린 키/반복/훅 정리
2118 UnregisterHookEvents(); // _hook.Dispose() 포함
2119
2120 if (disposing)
2121 {
2122 _keyboardStateSyncTimer.Tick -= SynchronizeKeyboardState;
2123
2124 // 관리 리소스/이벤트 해제
2126 Unloaded -= KeyboardUserControl_Unloaded;
2127 IsVisibleChanged -= KeyboardUserControl_IsVisibleChanged;
2128 InputLanguageManager.Current.InputLanguageChanged -= OnInputLanguageChanged;
2129
2130 // RoutedEvent 핸들러 해제
2131 RemoveHandler(Key.ClickEvent, (RoutedEventHandler)KeyClick);
2132
2133 // SharpHook 자체 해제
2134 _hook?.Dispose();
2135 _hook = null!;
2136
2137 // 참조 끊기(시각 트리 객체들)
2138 _layoutRoot = null;
2139 _keys = null;
2140 _langBtn = null;
2141 _inputModeBtn = null;
2142 }
2143 }
2144 catch (Exception ex) { Debug.WriteLine($"[VK] Dispose error: {ex.Message}"); }
2145 finally
2146 {
2147 GC.SuppressFinalize(this);
2148 }
2149 }
2150
2160
2161
2171 {
2173
2175 _eventSimulator.SimulateKeyRelease(KeyCode.VcCapsLock);
2176
2177 if (IsPressedShift)
2178 {
2179 _eventSimulator.SimulateKeyRelease(KeyCode.VcLeftShift);
2180 _eventSimulator.SimulateKeyRelease(KeyCode.VcRightShift);
2181 }
2182
2183 if (IsPressedCtrl)
2184 {
2185 _eventSimulator.SimulateKeyRelease(KeyCode.VcLeftControl);
2186 _eventSimulator.SimulateKeyRelease(KeyCode.VcRightControl);
2187 }
2188 }
2189
2190 #endregion
2191}
void KeyboardUserControl_IsVisibleChanged(object? sender, DependencyPropertyChangedEventArgs e)
static void OnLayoutChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
static void OnIsPasswordLayoutChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
string GetDisplayText(bool shift, bool capsLock, LanguageCode languageCode, bool imeMode)
Definition Key.cs:189