2using System.Windows.Forms;
15internal sealed class DreamineVirtualKeyboardForm : Form
25 private static readonly Color KeyBackground = Color.FromArgb(0xF1, 0x68, 0x5E);
34 private static readonly Color KeySelectedBackground = Color.FromArgb(0xF5, 0x95, 0x84);
43 private const int KeyHeight = 54;
52 private const int KeyMargin = 2;
61 private const int RowHeight = 58;
70 private const int TextKeyboardWidth = 928;
80 private readonly DreamineTextBox _target;
98 private readonly HangulComposer _hangulComposer =
new();
107 private readonly List<KeyButton> _keyButtons = [];
116 private readonly System.Windows.Forms.Timer _stateTimer;
135 private bool _physicalShift;
144 private bool _korean;
153 private DreamineButton? _shiftButton;
162 private DreamineButton? _capsButton;
171 private DreamineButton? _languageButton;
180 private int _contentWidth;
189 private int _contentHeight;
223 public DreamineVirtualKeyboardForm(DreamineTextBox target,
VkLayout layout)
227 _korean = IsKoreanInputMode();
229 FormBorderStyle = FormBorderStyle.None;
230 StartPosition = FormStartPosition.Manual;
231 ShowInTaskbar =
false;
233 BackColor = Color.Black;
234 Padding =
new Padding(18, 14, 18, 14);
236 Controls.Add(layout ==
VkLayout.Numeric ? BuildNumericLayout() : BuildTextLayout());
238 _stateTimer =
new System.Windows.Forms.Timer { Interval = 180 };
239 _stateTimer.Tick += (_, _) => SyncPhysicalKeyboardState();
244 var contentWidth = _contentWidth == 0 ? 900 : _contentWidth;
245 var contentHeight = _contentHeight == 0 ? (layout == VkLayout.Numeric ? 4 : 5) * RowHeight : _contentHeight;
246 ClientSize =
new Size(contentWidth + Padding.Horizontal, contentHeight + Padding.Vertical);
265 private Control BuildNumericLayout()
267 var root = CreateRootPanel();
268 AddRow(root, [KeySpec.Text(
"1"), KeySpec.Text(
"2"), KeySpec.Text(
"3"), KeySpec.Command(
"Backspace", 104, Backspace)]);
269 AddRow(root, [KeySpec.Text(
"4"), KeySpec.Text(
"5"), KeySpec.Text(
"6"), KeySpec.Command(
"Enter", 104, Close)]);
270 AddRow(root, [KeySpec.Text(
"7"), KeySpec.Text(
"8"), KeySpec.Text(
"9"), KeySpec.Text(
".")]);
271 AddRow(root, [KeySpec.Text(
"0", 148), KeySpec.Command(
"Clear", 148, Clear)]);
291 private Control BuildTextLayout()
293 var root = CreateRootPanel();
297 KeySpec.Command(
"Esc", 54, Close),
298 KeySpec.Text(
"`"), KeySpec.Text(
"1"), KeySpec.Text(
"2"), KeySpec.Text(
"3"), KeySpec.Text(
"4"),
299 KeySpec.Text(
"5"), KeySpec.Text(
"6"), KeySpec.Text(
"7"), KeySpec.Text(
"8"), KeySpec.Text(
"9"),
300 KeySpec.Text(
"0"), KeySpec.Text(
"-"), KeySpec.Text(
"="),
301 KeySpec.Command(
"Backspace", 116, Backspace)
302 ], TextKeyboardWidth, 14);
306 KeySpec.Command(
"Tab", 100, () => InsertRaw(
" ")),
307 KeySpec.Text(
"q"), KeySpec.Text(
"w"), KeySpec.Text(
"e"), KeySpec.Text(
"r"), KeySpec.Text(
"t"),
308 KeySpec.Text(
"y"), KeySpec.Text(
"u"), KeySpec.Text(
"i"), KeySpec.Text(
"o"), KeySpec.Text(
"p"),
309 KeySpec.Text(
"["), KeySpec.Text(
"]"), KeySpec.Text(
"\\", 100)
310 ], TextKeyboardWidth, 13);
314 KeySpec.Command(
"Caps Lock", 116, ToggleCapsLock),
315 KeySpec.Text(
"a"), KeySpec.Text(
"s"), KeySpec.Text(
"d"), KeySpec.Text(
"f"), KeySpec.Text(
"g"),
316 KeySpec.Text(
"h"), KeySpec.Text(
"j"), KeySpec.Text(
"k"), KeySpec.Text(
"l"),
317 KeySpec.Text(
";"), KeySpec.Text(
"'"),
318 KeySpec.Command(
"Enter", 116, Close)
319 ], TextKeyboardWidth, 0, 12);
323 KeySpec.Command(
"Shift", 138, ToggleShift),
324 KeySpec.Text(
"z"), KeySpec.Text(
"x"), KeySpec.Text(
"c"), KeySpec.Text(
"v"), KeySpec.Text(
"b"),
325 KeySpec.Text(
"n"), KeySpec.Text(
"m"), KeySpec.Text(
","), KeySpec.Text(
"."), KeySpec.Text(
"/"),
326 KeySpec.Command(
"◀", 58, MoveLeft),
327 KeySpec.Command(
"▶", 58, MoveRight)
328 ], TextKeyboardWidth, 0, 11, 12);
332 KeySpec.Command(
"Ctrl", 124, () => { }),
333 KeySpec.Command(
"Space", 696, () => InsertRaw(
" ")),
334 KeySpec.Command(
"abc", 72, ToggleLanguage)
335 ], TextKeyboardWidth, 1);
356 private FlowLayoutPanel CreateRootPanel()
358 return new FlowLayoutPanel
360 Dock = DockStyle.Fill,
361 FlowDirection = FlowDirection.TopDown,
362 WrapContents =
false,
363 BackColor = Color.Black,
364 Margin = Padding.Empty,
365 Padding = Padding.Empty,
410 private void AddRow(FlowLayoutPanel root, IReadOnlyList<KeySpec> specs,
int? targetWidth =
null, params
int[] expandIndexes)
412 var arrangedSpecs = specs.ToArray();
413 var rowWidth = arrangedSpecs.Sum(spec => spec.Width + (KeyMargin * 2));
414 if (targetWidth is { } width && expandIndexes.Length > 0 && rowWidth < width)
416 var validIndexes = expandIndexes
417 .Where(index => index >= 0 && index < arrangedSpecs.Length)
421 if (validIndexes.Length > 0)
423 var extra = width - rowWidth;
424 var share = extra / validIndexes.Length;
425 var remainder = extra % validIndexes.Length;
427 for (var i = 0; i < validIndexes.Length; i++)
429 var index = validIndexes[i];
430 var spec = arrangedSpecs[index];
431 arrangedSpecs[index] = spec with { Width = spec.Width + share + (i == validIndexes.Length - 1 ? remainder : 0) };
438 var row =
new FlowLayoutPanel
442 FlowDirection = FlowDirection.LeftToRight,
443 WrapContents =
false,
444 BackColor = Color.Black,
445 Margin = Padding.Empty,
446 Padding = Padding.Empty,
449 foreach (var spec
in arrangedSpecs)
451 var button = MakeKey(spec);
452 row.Controls.Add(button);
455 root.Controls.Add(row);
456 _contentWidth = Math.Max(_contentWidth, rowWidth);
457 _contentHeight += RowHeight;
484 private DreamineButton MakeKey(KeySpec spec)
486 var button =
new DreamineButton
488 Content = spec.Label,
492 Margin =
new Padding(KeyMargin),
493 BackColor = KeyBackground,
494 ForeColor = Color.White,
495 BorderColor = KeyBackground,
496 Font =
new Font(
"Segoe UI", spec.Width > 90 ? 12f : 14f, FontStyle.Regular, GraphicsUnit.Point)
499 if (spec.Kind == KeyKind.Text)
500 _keyButtons.Add(
new KeyButton(button, spec.Label));
502 if (spec.Label ==
"Shift")
503 _shiftButton = button;
504 else if (spec.Label ==
"Caps Lock")
505 _capsButton = button;
506 else if (spec.Label is
"abc" or
"가")
507 _languageButton = button;
509 button.MouseUp += (_, e) =>
511 if (e.Button != MouseButtons.Left || !button.ClientRectangle.Contains(e.Location))
514 if (_target.IsDisposed)
521 if (spec.Kind == KeyKind.Text)
522 InsertKey(spec.Label);
524 spec.Action?.Invoke();
546 private void InsertKey(
string key)
548 var text = GetKeyText(key);
549 if (_korean && HangulComposer.IsComposableJamo(text))
551 var edit = _hangulComposer.Input(text, _target.GetTextBeforeCaret());
552 _target.ReplaceTextTail(edit.ReplaceCount, edit.Text);
556 _hangulComposer.Reset();
557 _target.InsertText(text);
583 private void InsertRaw(
string text)
585 _hangulComposer.Reset();
586 _target.InsertText(text);
597 private void Backspace()
599 _hangulComposer.Reset();
613 _hangulComposer.Reset();
614 _target.Text =
string.Empty;
625 private void ToggleShift()
639 private void ToggleCapsLock()
641 _hangulComposer.Reset();
642 SendKeys.SendWait(
"{CAPSLOCK}");
654 private void ToggleLanguage()
656 _hangulComposer.Reset();
658 ApplyInputLanguage(_korean);
659 ImeHelper.SetNativeMode(_target.TextBoxHandle, _korean);
671 private void MoveLeft()
673 _target.SelectionStart = Math.Max(0, _target.SelectionStart - 1);
684 private void MoveRight()
686 _target.SelectionStart = Math.Min(_target.Text.Length, _target.SelectionStart + 1);
697 private void SyncPhysicalKeyboardState()
702 var caps = Control.IsKeyLocked(Keys.CapsLock);
703 _physicalShift = (ModifierKeys & Keys.Shift) == Keys.Shift;
704 _korean = IsKoreanInputMode();
705 if (_capsButton !=
null)
706 _capsButton.IsSelected = caps;
719 private void RefreshKeys()
721 var caps = Control.IsKeyLocked(Keys.CapsLock);
723 foreach (var key
in _keyButtons)
724 key.Button.Content = GetKeyText(key.BaseText);
726 if (_shiftButton !=
null)
728 _shiftButton.IsSelected = EffectiveShift;
729 _shiftButton.BackColor = EffectiveShift ? KeySelectedBackground : KeyBackground;
732 if (_capsButton !=
null)
734 _capsButton.IsSelected = caps;
735 _capsButton.BackColor = caps ? KeySelectedBackground : KeyBackground;
738 if (_languageButton !=
null)
740 _languageButton.Content = _korean ?
"가" :
"abc";
741 _languageButton.IsSelected = _korean;
742 _languageButton.BackColor = _korean ? KeySelectedBackground : KeyBackground;
778 private string GetKeyText(
string key)
780 if (_korean && KoreanKeys.TryGetValue(key, out var korean))
781 return EffectiveShift && KoreanShiftKeys.TryGetValue(key, out var koreanShift) ? koreanShift : korean;
783 if (ShiftKeys.TryGetValue(key, out var shifted) && EffectiveShift)
786 if (key.Length == 1 &&
char.IsLetter(key[0]))
788 var upper = EffectiveShift ^ Control.IsKeyLocked(Keys.CapsLock);
789 return upper ? key.ToUpperInvariant() : key.ToLowerInvariant();
811 protected override void OnFormClosed(FormClosedEventArgs e)
814 _stateTimer.Dispose();
815 base.OnFormClosed(e);
834 protected override void OnPaint(PaintEventArgs e)
837 using var pen =
new Pen(Color.Gray, 4f);
838 e.Graphics.DrawRectangle(pen, 1, 1, Width - 3, Height - 3);
849 protected override bool ShowWithoutActivation =>
true;
859 private bool EffectiveShift => _shift || _physicalShift;
877 private bool IsKoreanInputMode()
879 return InputLanguage.CurrentInputLanguage.Culture.Name.Equals(
"ko-KR", StringComparison.OrdinalIgnoreCase) &&
880 ImeHelper.IsNativeMode(_target.TextBoxHandle);
899 private static void ApplyInputLanguage(
bool korean)
901 var cultureName = korean ?
"ko-KR" :
"en-US";
902 foreach (InputLanguage language
in InputLanguage.InstalledInputLanguages)
904 if (language.Culture.Name.Equals(cultureName, StringComparison.OrdinalIgnoreCase))
906 InputLanguage.CurrentInputLanguage = language;
920 private static readonly Dictionary<string, string> ShiftKeys =
new()
922 [
"`"] =
"~", [
"1"] =
"!", [
"2"] =
"@", [
"3"] =
"#", [
"4"] =
"$",
923 [
"5"] =
"%", [
"6"] =
"^", [
"7"] =
"&", [
"8"] =
"*", [
"9"] =
"(",
924 [
"0"] =
")", [
"-"] =
"_", [
"="] =
"+", [
"["] =
"{", [
"]"] =
"}",
925 [
"\\"] =
"|", [
";"] =
":", [
"'"] =
"\"", [
","] =
"<", [
"."] =
">",
937 private static readonly Dictionary<string, string> KoreanKeys =
new()
939 [
"q"] =
"ㅂ", [
"w"] =
"ㅈ", [
"e"] =
"ㄷ", [
"r"] =
"ㄱ", [
"t"] =
"ㅅ",
940 [
"y"] =
"ㅛ", [
"u"] =
"ㅕ", [
"i"] =
"ㅑ", [
"o"] =
"ㅐ", [
"p"] =
"ㅔ",
941 [
"a"] =
"ㅁ", [
"s"] =
"ㄴ", [
"d"] =
"ㅇ", [
"f"] =
"ㄹ", [
"g"] =
"ㅎ",
942 [
"h"] =
"ㅗ", [
"j"] =
"ㅓ", [
"k"] =
"ㅏ", [
"l"] =
"ㅣ",
943 [
"z"] =
"ㅋ", [
"x"] =
"ㅌ", [
"c"] =
"ㅊ", [
"v"] =
"ㅍ", [
"b"] =
"ㅠ",
944 [
"n"] =
"ㅜ", [
"m"] =
"ㅡ",
955 private static readonly Dictionary<string, string> KoreanShiftKeys =
new()
957 [
"q"] =
"ㅃ", [
"w"] =
"ㅉ", [
"e"] =
"ㄸ", [
"r"] =
"ㄲ", [
"t"] =
"ㅆ",
958 [
"o"] =
"ㅒ", [
"p"] =
"ㅖ",
985 private sealed record KeyButton(DreamineButton Button,
string BaseText);
1027 private sealed record KeySpec(KeyKind Kind,
string Label,
int Width, Action? Action)
1061 public static KeySpec Text(
string label,
int width = 54) =>
new(KeyKind.Text, label, width,
null);
1103 public static KeySpec Command(
string label,
int width, Action action) =>
new(KeyKind.Command, label, width, action);
1114 private enum KeyKind