1 /** 2 * Single- and multi-line text entry control. 3 * 4 * `TextBox` wraps the native Win32 `"EDIT"` control. It supports single-line and 5 * multi-line variants, an optional read-only flag, selection helpers, and a 6 * delegate-based change/keyboard event surface. Because it is a real native 7 * control, it carries MSAA accessibility for free. 8 */ 9 module deft.controls.textbox; 10 11 version (Windows): 12 13 import core.sys.windows.windows; 14 15 import deft.controls.control; 16 import deft.events; 17 import deft.util.strings; 18 import deft.widget; 19 20 /// The flavor of text box to create. 21 enum TextBoxStyle 22 { 23 /// One line of text, no wrapping. 24 singleLine, 25 /// Multiple lines with vertical scrolling and Enter inserting newlines. 26 multiLine, 27 /// Single line, not user-editable. 28 singleLineReadOnly, 29 /// Multiple lines, not user-editable. 30 multiLineReadOnly, 31 } 32 33 /// A native text entry field built on the Win32 `EDIT` control. 34 class TextBox : Control 35 { 36 private bool multiline_; 37 38 /// Fired when the text changes (`EN_CHANGE`); carries the new text. 39 Event!(string) onTextChanged; 40 41 /// Fired on a key press while the control has focus. 42 Event!(KeyEventArgs) onKeyDown; 43 44 /** 45 * Create a text box. 46 * 47 * `initialText` is placed in the control if non-empty. `style` selects the 48 * single/multi-line and read-only behavior. 49 */ 50 this(Widget parent, string initialText = "", TextBoxStyle style = TextBoxStyle.singleLine) 51 { 52 multiline_ = (style == TextBoxStyle.multiLine 53 || style == TextBoxStyle.multiLineReadOnly); 54 55 DWORD editStyle = WS_TABSTOP | WS_BORDER; 56 if (multiline_) 57 editStyle |= ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL; 58 else 59 editStyle |= ES_AUTOHSCROLL; 60 61 if (style == TextBoxStyle.singleLineReadOnly 62 || style == TextBoxStyle.multiLineReadOnly) 63 editStyle |= ES_READONLY; 64 65 super(parent, "EDIT", editStyle); 66 67 if (initialText.length != 0) 68 setText(initialText); 69 70 subclass(); 71 } 72 73 /// Toggle the read-only state of the control. 74 void setReadOnly(bool ro) 75 { 76 SendMessageW(handle, EM_SETREADONLY, ro ? TRUE : FALSE, 0); 77 } 78 79 /// Select all the text in the control. 80 void selectAll() 81 { 82 SendMessageW(handle, EM_SETSEL, 0, -1); 83 } 84 85 /// Return the current selection as `[start, end]` character offsets. 86 int[2] getSelectionRange() 87 { 88 DWORD start; 89 DWORD end; 90 SendMessageW(handle, EM_GETSEL, cast(WPARAM)&start, cast(LPARAM)&end); 91 return [cast(int) start, cast(int) end]; 92 } 93 94 /// Append text at the end of the control, moving the caret there first. 95 void appendText(string text) 96 { 97 int len = cast(int) SendMessageW(handle, WM_GETTEXTLENGTH, 0, 0); 98 SendMessageW(handle, EM_SETSEL, len, len); 99 SendMessageW(handle, EM_REPLACESEL, FALSE, cast(LPARAM) text.toWStringz); 100 } 101 102 /// Fire `onTextChanged` on `EN_CHANGE` notifications. 103 override bool processCommand(ushort notificationCode) 104 { 105 if (notificationCode == EN_CHANGE) 106 { 107 onTextChanged.fire(getText()); 108 return true; 109 } 110 return false; 111 } 112 113 /// Intercept `WM_KEYDOWN` to surface `onKeyDown` and allow suppression. 114 override bool processSubclassed(UINT msg, WPARAM wParam, LPARAM lParam, ref LRESULT result) 115 { 116 if (msg == WM_KEYDOWN) 117 { 118 // A multi-line edit reports DLGC_WANTALLKEYS, so the dialog manager 119 // hands it the Tab key and it inserts a literal tab — a focus trap for 120 // keyboard users. Intercept plain Tab and move focus like a dialog 121 // would; Ctrl+Tab still falls through to insert a real tab character. 122 if (multiline_ && wParam == VK_TAB 123 && (GetKeyState(VK_CONTROL) & 0x8000) == 0) 124 { 125 bool back = (GetKeyState(VK_SHIFT) & 0x8000) != 0; 126 HWND root = GetAncestor(handle, GA_ROOT); 127 if (root !is null) 128 { 129 HWND next = GetNextDlgTabItem(root, handle, back ? TRUE : FALSE); 130 if (next !is null && next !is handle) 131 SetFocus(next); 132 } 133 result = 0; 134 return true; 135 } 136 137 KeyEventArgs args; 138 args.keyCode = cast(uint) wParam; 139 args.ctrl = (GetKeyState(VK_CONTROL) & 0x8000) != 0; 140 args.shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0; 141 args.alt = (GetKeyState(VK_MENU) & 0x8000) != 0; 142 143 onKeyDown.fire(args); 144 145 if (args.handled) 146 { 147 result = 0; 148 return true; 149 } 150 } 151 return false; 152 } 153 154 /// Preferred size: compact for single-line, taller for multi-line. 155 override Size getPreferredSize() 156 { 157 return multiline_ ? Size(160, 80) : Size(160, 24); 158 } 159 }