1 /**
2  * Deft framework demo — a localized widget gallery.
3  *
4  * Exercises every control type the framework provides: menus and accelerators,
5  * a status bar, a tab control whose pages hold labels, buttons, check boxes, a
6  * radio group, single- and multi-line text boxes, a list view, a tree view, a
7  * list box and a combo box, plus a one-second timer, a tray icon, a modal
8  * dialog and a message box.
9  *
10  * It also demonstrates **localization**: every user-facing string is marked with
11  * Deft's `tr()` seam, the `Language` menu switches the UI language at runtime,
12  * and translations are loaded from gettext `.mo` catalogs (compiled from the
13  * `.po` files under `locale/`) via the `mofile` package, installed through
14  * `setTranslator`. The standard dialog buttons localize themselves from the OS.
15  *
16  * Each tab page is a framework `Panel` — a container that lays its children out
17  * with a sizer and forwards their notifications.
18  */
19 module app;
20 
21 import std.conv : to;
22 import std.file : thisExePath, exists;
23 import std.format : format;
24 import std.path : buildPath, dirName;
25 
26 import core.sys.windows.windows;
27 
28 import mofile;
29 
30 import deft;
31 
32 /// Command ids for the menu bar.
33 enum : int
34 {
35 	idNew = 40_001,
36 	idInput = 40_002,
37 	idExit = 40_003,
38 	idPreview = 40_010,
39 	idAbout = 40_020,
40 	idTrayShow = 40_030,
41 	idTrayExit = 40_031,
42 	idLangEn = 40_040,
43 	idLangFr = 40_041,
44 	idLangDe = 40_042,
45 	idLangRu = 40_043,
46 	idLangUk = 40_044,
47 }
48 
49 int main()
50 {
51 	auto app = Application.instance;
52 	app.initialize();
53 
54 	auto window = new Window("Deft Widget Gallery", 900, 640);
55 	window.setIcon(loadIcon(1));
56 	window.setMinimumSize(640, 480);
57 
58 	auto status = new StatusBar(window);
59 	window.setStatusBar(status);
60 
61 	// --- Tab control with two pages, each a Panel laid out with a VBox. ---
62 	auto tabs = new TabControl(window);
63 
64 	auto root = new VBox();
65 	root.add(tabs).proportion(1).pad(Padding.all(8));
66 	window.setSizer(root);
67 
68 	// Page 1: basic controls.
69 	auto basics = new Panel(window);
70 	auto basicsBox = new VBox();
71 
72 	auto hello = new Label(basics, tr("Hello from Deft"));
73 	basicsBox.add(hello).pad(Padding.all(6));
74 
75 	auto clock = new Label(basics, "");
76 	basicsBox.add(clock).pad(Padding.all(6));
77 
78 	auto dialogButton = new Button(basics, tr("&Open Dialog..."));
79 	basicsBox.add(dialogButton).pad(Padding.all(6)).alignH(HAlign.center);
80 
81 	auto feature = new CheckBox(basics, tr("Enable &feature"));
82 	basicsBox.add(feature).pad(Padding.all(6));
83 
84 	auto optionA = new RadioButton(basics, tr("Option &A"), true);
85 	auto optionB = new RadioButton(basics, tr("Option &B"));
86 	optionA.setChecked(true);
87 	basicsBox.add(optionA).pad(Padding.all(6));
88 	basicsBox.add(optionB).pad(Padding.all(6));
89 
90 	auto search = new TextBox(basics, "", TextBoxStyle.singleLine);
91 	basicsBox.add(search).pad(Padding.all(6));
92 
93 	auto notes = new TextBox(basics, tr("Multi-line text..."), TextBoxStyle.multiLine);
94 	basicsBox.add(notes).proportion(1).pad(Padding.all(6));
95 
96 	basics.setSizer(basicsBox);
97 	tabs.addPage(tr("Basics"), basics);
98 
99 	// Page 2: list-style controls.
100 	auto lists = new Panel(window);
101 	auto listsBox = new VBox();
102 
103 	auto listView = new ListView(lists);
104 	listView.addColumn(tr("Title"), 260);
105 	listView.addColumn(tr("Updated"), 140);
106 	listView.addColumn(tr("Created"), 140);
107 	listView.addItem(["My first note", "2026-05-01", "2026-04-15"]);
108 	listView.addItem(["Shopping list", "2026-04-30", "2026-04-20"]);
109 	listView.addItem(["Meeting agenda", "2026-04-28", "2026-04-10"]);
110 	listView.autoSizeColumn(0, ColumnAutoSize.content);
111 	listView.autoSizeColumn(1, ColumnAutoSize.content);
112 	listView.autoSizeColumn(2, ColumnAutoSize.header);
113 	setAccessibleName(listView, tr("Notes list"));
114 	listsBox.add(listView).proportion(2).pad(Padding.all(6));
115 
116 	auto tree = new TreeView(lists);
117 	auto work = tree.addRoot("Work");
118 	tree.addChild(work, "Project Alpha");
119 	tree.addChild(work, "Project Beta");
120 	auto personal = tree.addRoot("Personal");
121 	tree.addChild(personal, "Shopping");
122 	tree.addChild(personal, "Travel");
123 	tree.expandItem(work);
124 	setAccessibleName(tree, tr("Categories"));
125 	listsBox.add(tree).proportion(2).pad(Padding.all(6));
126 
127 	auto listBox = new ListBox(lists);
128 	listBox.addItem("Apples");
129 	listBox.addItem("Oranges");
130 	listBox.addItem("Pears");
131 	setAccessibleName(listBox, tr("Fruit list"));
132 	listsBox.add(listBox).proportion(1).pad(Padding.all(6));
133 
134 	auto checks = new CheckListBox(lists);
135 	checks.addItem("Bold");
136 	checks.addItem("Italic");
137 	checks.addItem("Underline");
138 	checks.setChecked(0, true);
139 	setAccessibleName(checks, tr("Text style"));
140 	checks.onItemChecked ~= (int index) {
141 		status.setText(format(checks.isChecked(index) ? tr("Checked: %s") : tr("Unchecked: %s"),
142 			checks.getItemText(index)));
143 	};
144 	listsBox.add(checks).proportion(1).pad(Padding.all(6));
145 
146 	auto combo = new ComboBox(lists);
147 	combo.addItem("Small");
148 	combo.addItem("Medium");
149 	combo.addItem("Large");
150 	combo.setSelectedIndex(1);
151 	setAccessibleName(combo, tr("Size"));
152 	listsBox.add(combo).pad(Padding.all(6));
153 
154 	lists.setSizer(listsBox);
155 	tabs.addPage(tr("Lists"), lists);
156 
157 	// --- Localization state and machinery. ---
158 	int elapsed = 0;
159 	MoFile catalog;
160 	bool haveCatalog = false;
161 	MenuBar menuBar; // assigned once the menu is built; used by retranslate()
162 
163 	/// Refresh the clock label in the current language.
164 	void updateClock()
165 	{
166 		clock.setText(format(tr("Elapsed: %d s"), elapsed));
167 	}
168 
169 	/// Re-apply every translatable caption in the active language. Controls
170 	/// created on demand (dialogs, message boxes) read `tr()` when shown, so they
171 	/// need no retranslation here.
172 	void retranslate()
173 	{
174 		window.setTitle(tr("Deft Widget Gallery"));
175 		status.setText(tr("Ready"));
176 
177 		tabs.setTabTitle(0, tr("Basics"));
178 		tabs.setTabTitle(1, tr("Lists"));
179 
180 		hello.setText(tr("Hello from Deft"));
181 		updateClock();
182 		dialogButton.setText(tr("&Open Dialog..."));
183 		feature.setText(tr("Enable &feature"));
184 		optionA.setText(tr("Option &A"));
185 		optionB.setText(tr("Option &B"));
186 
187 		listView.setColumnTitle(0, tr("Title"));
188 		listView.setColumnTitle(1, tr("Updated"));
189 		listView.setColumnTitle(2, tr("Created"));
190 
191 		if (menuBar !is null)
192 		{
193 			menuBar.setMenuTitle(0, tr("&File"));
194 			menuBar.setMenuTitle(1, tr("&Edit"));
195 			menuBar.setMenuTitle(2, tr("&Language"));
196 			menuBar.setMenuTitle(3, tr("&Help"));
197 			menuBar.setItemText(idNew, tr("&New Note..."));
198 			menuBar.setItemText(idInput, tr("&Input..."));
199 			menuBar.setItemText(idExit, tr("E&xit"));
200 			menuBar.setItemText(idPreview, tr("Show &Preview"));
201 			menuBar.setItemText(idAbout, tr("&About"));
202 			if (window.handle)
203 				DrawMenuBar(window.handle);
204 		}
205 	}
206 
207 	/// Load `code` ("en" for the untranslated source language, otherwise a locale
208 	/// directory under `locale/`) and re-translate the whole UI.
209 	void loadLanguage(string code)
210 	{
211 		if (code == "en")
212 		{
213 			haveCatalog = false;
214 			setTranslator(null);
215 		}
216 		else
217 		{
218 			auto path = buildPath(dirName(thisExePath()), "locale", code, "deft-demo.mo");
219 			if (exists(path))
220 			{
221 				try
222 				{
223 					catalog = MoFile(path);
224 					haveCatalog = true;
225 					setTranslator((string key) => catalog.gettext(key));
226 				}
227 				catch (Exception)
228 				{
229 					haveCatalog = false;
230 					setTranslator(null);
231 				}
232 			}
233 			else
234 			{
235 				haveCatalog = false;
236 				setTranslator(null);
237 			}
238 		}
239 
240 		if (menuBar !is null)
241 		{
242 			menuBar.setChecked(idLangEn, code == "en");
243 			menuBar.setChecked(idLangFr, code == "fr");
244 			menuBar.setChecked(idLangDe, code == "de");
245 			menuBar.setChecked(idLangRu, code == "ru");
246 			menuBar.setChecked(idLangUk, code == "uk");
247 		}
248 
249 		retranslate();
250 	}
251 
252 	// --- Cross-control wiring. ---
253 	search.onTextChanged ~= (string text) {
254 		status.setText(format(tr("Search: %s"), text));
255 	};
256 
257 	listView.onSelectionChanged ~= (int index) {
258 		status.setText(format(tr("Selected: %s"), listView.getItemText(index, 0)));
259 	};
260 	listView.onItemActivated ~= (int index) {
261 		showMessageBox(window,
262 			format(tr("You activated: %s"), listView.getItemText(index, 0)),
263 			tr("Note"), MessageBoxStyle.info);
264 	};
265 
266 	// Right-click context menu on the list (coordinates are screen-relative).
267 	auto listMenu = new Menu();
268 	auto ctxView = MenuItem(0, tr("&View"));
269 	ctxView.onClicked ~= {
270 		int sel = listView.getSelectedIndex();
271 		if (sel >= 0)
272 			status.setText(format(tr("View: %s"), listView.getItemText(sel, 0)));
273 	};
274 	listMenu.append(ctxView);
275 	listView.onContextMenu ~= (int index, MouseEventArgs m) {
276 		if (index >= 0)
277 			listView.setSelectedIndex(index);
278 		showPopupMenu(listMenu, window, m.x, m.y);
279 	};
280 
281 	tree.onSelectionChanged ~= (TreeItem item) {
282 		status.setText(format(tr("Category: %s"), tree.getItemText(item)));
283 	};
284 
285 	// Context menu on the tree — works from right-click and the Apps key / Shift+F10.
286 	auto treeMenu = new Menu();
287 	auto ctxExpand = MenuItem(0, tr("&Expand"));
288 	ctxExpand.onClicked ~= {
289 		auto sel = tree.getSelectedItem();
290 		if (!sel.isNull)
291 			tree.expandItem(sel);
292 	};
293 	treeMenu.append(ctxExpand);
294 	tree.onContextMenu ~= (TreeItem item, MouseEventArgs m) {
295 		if (!item.isNull)
296 			tree.setSelectedItem(item);
297 		showPopupMenu(treeMenu, window, m.x, m.y);
298 	};
299 
300 	listBox.onSelectionChanged ~= (int index) {
301 		status.setText(format(tr("Fruit: %s"), listBox.getItemText(index)));
302 	};
303 
304 	feature.onToggled ~= {
305 		status.setText(feature.isChecked() ? tr("Feature on") : tr("Feature off"));
306 	};
307 
308 	dialogButton.onClicked ~= { openEditDialog(window, status); };
309 
310 	// --- Menu bar with accelerators. ---
311 	auto fileMenu = new Menu();
312 	auto newItem = MenuItem(idNew, tr("&New Note..."), "Ctrl+N");
313 	newItem.onClicked ~= { status.setText(tr("New note")); };
314 	fileMenu.append(newItem);
315 
316 	auto inputItem = MenuItem(idInput, tr("&Input..."), "Ctrl+I");
317 	inputItem.onClicked ~= {
318 		auto answer = showInputDialog(window, tr("Your name"), tr("Enter your &name:"));
319 		if (answer !is null)
320 			status.setText(format(tr("Hello, %s"), answer));
321 	};
322 	fileMenu.append(inputItem);
323 
324 	fileMenu.appendSeparator();
325 
326 	auto exitItem = MenuItem(idExit, tr("E&xit"), "Ctrl+Q");
327 	exitItem.onClicked ~= { window.close(); };
328 	fileMenu.append(exitItem);
329 
330 	auto editMenu = new Menu();
331 	auto previewItem = MenuItem(idPreview, tr("Show &Preview"), "", MenuItemKind.checkable);
332 	previewItem.onClicked ~= {
333 		bool now = !editMenu.findItem(idPreview).checked;
334 		editMenu.setChecked(idPreview, now);
335 		status.setText(now ? tr("Preview on") : tr("Preview off"));
336 	};
337 	editMenu.append(previewItem);
338 
339 	// Language menu: item labels are autonyms, shown in their own language, so
340 	// they are intentionally NOT passed through tr().
341 	auto langMenu = new Menu();
342 	auto langEn = MenuItem(idLangEn, "English", "", MenuItemKind.checkable);
343 	langEn.onClicked ~= { loadLanguage("en"); };
344 	langMenu.append(langEn);
345 	auto langFr = MenuItem(idLangFr, "Français", "", MenuItemKind.checkable);
346 	langFr.onClicked ~= { loadLanguage("fr"); };
347 	langMenu.append(langFr);
348 	auto langDe = MenuItem(idLangDe, "Deutsch", "", MenuItemKind.checkable);
349 	langDe.onClicked ~= { loadLanguage("de"); };
350 	langMenu.append(langDe);
351 	auto langRu = MenuItem(idLangRu, "Русский", "", MenuItemKind.checkable);
352 	langRu.onClicked ~= { loadLanguage("ru"); };
353 	langMenu.append(langRu);
354 	auto langUk = MenuItem(idLangUk, "Українська", "", MenuItemKind.checkable);
355 	langUk.onClicked ~= { loadLanguage("uk"); };
356 	langMenu.append(langUk);
357 
358 	auto helpMenu = new Menu();
359 	auto aboutItem = MenuItem(idAbout, tr("&About"), "F1");
360 	aboutItem.onClicked ~= {
361 		showMessageBox(window,
362 			tr("Deft Widget Gallery\nA native UI framework for D."),
363 			tr("About"), MessageBoxStyle.info);
364 	};
365 	helpMenu.append(aboutItem);
366 
367 	menuBar = new MenuBar();
368 	menuBar.append(fileMenu, tr("&File"));
369 	menuBar.append(editMenu, tr("&Edit"));
370 	menuBar.append(langMenu, tr("&Language"));
371 	menuBar.append(helpMenu, tr("&Help"));
372 	window.setMenuBar(menuBar);
373 
374 	// --- One-second timer updating the clock label. ---
375 	auto timer = new Timer(window);
376 	timer.onTick ~= {
377 		++elapsed;
378 		updateClock();
379 	};
380 	timer.start(1000);
381 
382 	// --- Tray icon with a context menu. ---
383 	auto trayMenu = new Menu();
384 	auto trayShow = MenuItem(idTrayShow, tr("&Show"));
385 	trayShow.onClicked ~= {
386 		window.show();
387 		SetForegroundWindow(window.handle);
388 	};
389 	trayMenu.append(trayShow);
390 	trayMenu.appendSeparator();
391 	auto trayExit = MenuItem(idTrayExit, tr("E&xit"));
392 	auto tray = new TrayIcon(window, tr("Deft Widget Gallery"));
393 	trayExit.onClicked ~= {
394 		tray.remove();
395 		app.quit();
396 	};
397 	trayMenu.append(trayExit);
398 
399 	tray.setIcon(LoadIconW(null, IDI_APPLICATION));
400 	tray.setContextMenu(trayMenu);
401 	tray.onDoubleClicked ~= {
402 		window.show();
403 		SetForegroundWindow(window.handle);
404 	};
405 
406 	// Remove the tray icon before the window is destroyed.
407 	window.onClose ~= (CloseEventArgs* args) { tray.remove(); };
408 
409 	// Start in the source language (English); checks the English menu item and
410 	// fills in the dynamic clock/status captions.
411 	loadLanguage("en");
412 
413 	window.show();
414 	return app.run();
415 }
416 
417 /// Open a modal note editor and report the result to the status bar.
418 void openEditDialog(Window parent, StatusBar status)
419 {
420 	auto dialog = new Dialog(parent, tr("Edit Note"), 480, 360);
421 	scope (exit)
422 		dialog.dispose();
423 
424 	// A table layout: a fixed label column with right-aligned labels, and a
425 	// stretching field column; the title row sizes to the field, the content row
426 	// fills the rest.
427 	auto grid = new Grid(2, 2);
428 	grid.setColumn(0, GridTrack.pixels(90));
429 	grid.setColumn(1, GridTrack.percent(100));
430 	grid.setRow(0, GridTrack.autoSize);
431 	grid.setRow(1, GridTrack.percent(100));
432 	grid.setSpacing(8, 8);
433 
434 	auto titleLabel = new Label(dialog, tr("&Title:"));
435 	auto titleInput = new TextBox(dialog, tr("Untitled"));
436 	auto bodyLabel = new Label(dialog, tr("&Content:"));
437 	auto bodyInput = new TextBox(dialog, "", TextBoxStyle.multiLine);
438 
439 	grid.add(titleLabel, 0, 0).aligned(HAlign.right, VAlign.middle).pad(Padding.all(4));
440 	grid.add(titleInput, 1, 0).pad(Padding.all(4));
441 	grid.add(bodyLabel, 0, 1).aligned(HAlign.right, VAlign.top).pad(Padding.all(4));
442 	grid.add(bodyInput, 1, 1).pad(Padding.all(4));
443 
444 	dialog.setSizer(grid);
445 	dialog.addStandardButtons(ButtonSet.okCancel);
446 
447 	if (dialog.showModal() == DialogResult.ok)
448 		status.setText(format(tr("Saved: %s"), titleInput.getText()));
449 	else
450 		status.setText(tr("Edit canceled"));
451 }