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 }