OXYGENHUB
◆ main openmsx / src / imgui / ImGuiOsdIcons.cc
13.8 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "ImGuiOsdIcons.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiManager.hh"
5
#include "ImGuiOpenFile.hh"
6
#include "ImGuiUtils.hh"
7
8
#include "CommandException.hh"
9
#include "FileContext.hh"
10
#include "GLImage.hh"
11
#include "Interpreter.hh"
12
#include "StringOp.hh"
13
#include "enumerate.hh"
14
15
#include <imgui.h>
16
#include <imgui_stdlib.h>
17
18
#include <cstdlib>
19
20
using namespace std::literals;
21
22
23
namespace openmsx {
24
25
ImGuiOsdIcons::ImGuiOsdIcons(ImGuiManager& manager_)
26
	: ImGuiPart(manager_)
27
{
28
	// Usually immediately overridden by loading imgui.ini
29
	// But nevertheless required for the initial state.
30
	setDefaultIcons();
31
}
32
33
void ImGuiOsdIcons::save(ImGuiTextBuffer& buf)
34
{
35
	savePersistent(buf, *this, persistentElements);
36
	adjust.save(buf);
37
	for (const auto& [i, icon] : enumerate(iconInfo)) {
38
		auto n = narrow<int>(i + 1);
39
		buf.appendf("icon.%d.enabled=%d\n",   n, icon.enable);
40
		buf.appendf("icon.%d.fade=%d\n",      n, icon.fade);
41
		buf.appendf("icon.%d.on-image=%s\n",  n, icon.on.filename.c_str());
42
		buf.appendf("icon.%d.off-image=%s\n", n, icon.off.filename.c_str());
43
		buf.appendf("icon.%d.expr=%s\n",      n, icon.expr.getString().c_str());
44
	}
45
}
46
47
void ImGuiOsdIcons::loadStart()
48
{
49
	iconInfo.clear();
50
}
51
52
void ImGuiOsdIcons::loadLine(std::string_view name, zstring_view value)
53
{
54
	if (loadOnePersistent(name, value, *this, persistentElements)) {
55
		// already handled
56
	} else if (adjust.loadLine(name, value)) {
57
		// already handled
58
	} else if (name.starts_with("icon.")) {
59
		auto [numStr, suffix] = StringOp::splitOnFirst(name.substr(5), '.');
60
		auto n = StringOp::stringTo<size_t>(numStr);
61
		if (!n || *n == 0) return;
62
		while (iconInfo.size() < *n) {
63
			iconInfo.emplace_back();
64
		}
65
		auto& info = iconInfo[*n - 1];
66
		if (suffix == "enabled") {
67
			info.enable = StringOp::stringToBool(value);
68
		} else if (suffix == "fade") {
69
			info.fade = StringOp::stringToBool(value);
70
		} else if (suffix == "on-image") {
71
			info.on.filename = value;
72
		} else if (suffix == "off-image") {
73
			info.off.filename = value;
74
		} else if (suffix == "expr") {
75
			info.expr = value;
76
		}
77
	}
78
}
79
80
void ImGuiOsdIcons::loadEnd()
81
{
82
	if (iconInfo.empty()) setDefaultIcons();
83
	iconInfoDirty = true;
84
}
85
86
void ImGuiOsdIcons::setDefaultIcons()
87
{
88
	iconInfo.clear();
89
	iconInfo.emplace_back(TclObject("$led_power"), "skins/set1/power-on.png", "skins/set1/power-off.png", true);
90
	iconInfo.emplace_back(TclObject("$led_caps" ), "skins/set1/caps-on.png",  "skins/set1/caps-off.png", true);
91
	iconInfo.emplace_back(TclObject("$led_kana" ), "skins/set1/kana-on.png",  "skins/set1/kana-off.png", true);
92
	iconInfo.emplace_back(TclObject("$led_pause"), "skins/set1/pause-on.png", "skins/set1/pause-off.png", true);
93
	iconInfo.emplace_back(TclObject("$led_turbo"), "skins/set1/turbo-on.png", "skins/set1/turbo-off.png", true);
94
	iconInfo.emplace_back(TclObject("$led_FDD"  ), "skins/set1/fdd-on.png",   "skins/set1/fdd-off.png", true);
95
	iconInfo.emplace_back(TclObject("$pause"    ), "skins/set1/pause.png",    "", false);
96
	iconInfo.emplace_back(TclObject("!$throttle || $fastforward"), "skins/set1/throttle.png", "", false);
97
	iconInfo.emplace_back(TclObject("$mute"     ), "skins/set1/mute.png",     "", false);
98
	iconInfo.emplace_back(TclObject("$breaked"  ), "skins/set1/breaked.png",  "", false);
99
	iconInfoDirty = true;
100
}
101
102
void ImGuiOsdIcons::loadIcons()
103
{
104
	maxIconSize = gl::vec2();
105
	numIcons = 0;
106
	FileContext context = systemFileContext();
107
	for (auto& icon : iconInfo) {
108
		if (!icon.enable) continue;
109
110
		auto load = [&](IconInfo::Icon& i) {
111
			try {
112
				if (!i.filename.empty()) {
113
					i.tex = loadTexture(context.resolve(i.filename), i.size);
114
					return;
115
				}
116
			} catch (...) {
117
				// nothing
118
			}
119
			i.tex.reset();
120
			i.size = {};
121
		};
122
		load(icon.on);
123
		load(icon.off);
124
125
		auto m = max(icon.on.size, icon.off.size);
126
		maxIconSize = max(maxIconSize, gl::vec2(m));
127
128
		++numIcons;
129
	}
130
	iconInfoDirty = false;
131
}
132
133
void ImGuiOsdIcons::paint(MSXMotherBoard* /*motherBoard*/)
134
{
135
	if (iconInfoDirty) loadIcons();
136
	if (showConfigureIcons) paintConfigureIcons();
137
	if (!showIcons) return;
138
139
	// default placement: bottom left
140
	const auto* mainViewPort = ImGui::GetMainViewport();
141
	ImGui::SetNextWindowPos(gl::vec2(mainViewPort->Pos) + gl::vec2{10.0f, mainViewPort->WorkSize.y - 10.0f},
142
	                        ImGuiCond_FirstUseEver,
143
	                        {0.0f, 1.0f}); // pivot = bottom-left
144
	int flags = hideTitle ? ImGuiWindowFlags_NoTitleBar |
145
	                        ImGuiWindowFlags_NoResize |
146
	                        ImGuiWindowFlags_NoScrollbar |
147
	                        ImGuiWindowFlags_NoScrollWithMouse |
148
	                        ImGuiWindowFlags_NoCollapse |
149
	                        ImGuiWindowFlags_NoBackground |
150
	                        ImGuiWindowFlags_NoFocusOnAppearing |
151
	                        ImGuiWindowFlags_NoNav |
152
	                        (allowMove ? 0 : ImGuiWindowFlags_NoMove)
153
	                      : 0;
154
	adjust.pre();
155
	im::Window("Icons", &showIcons, flags | ImGuiWindowFlags_HorizontalScrollbar, [&]{
156
		bool isOnMainViewPort = adjust.post();
157
		gl::vec2 topLeft = ImGui::GetCursorPos();
158
159
		const auto& style = ImGui::GetStyle();
160
		const auto& io = ImGui::GetIO();
161
162
		auto availableSize = ImGui::GetContentRegionAvail();
163
		auto columns = std::max(int(floor((availableSize.x + style.ItemSpacing.x) / (maxIconSize.x + style.ItemSpacing.x))), 1);
164
		auto rows = (numIcons + columns - 1) / columns; // round up
165
166
		// cover full canvas, both for context menu and to set dimensions (SetCursorPos() can't extend canvas)
167
		ImGui::Dummy(gl::vec2(float(columns) * maxIconSize.x + float(columns - 1) * style.ItemSpacing.x,
168
		                      float(rows   ) * maxIconSize.y + float(rows    - 1) * style.ItemSpacing.y));
169
		if (allowMove) {
170
			im::PopupContextItem("icons context menu", [&]{
171
				if (ImGui::MenuItem("Configure icons")) {
172
					showConfigureIcons = true;
173
				}
174
			});
175
		}
176
177
		bool fade = hideTitle && !ImGui::IsWindowDocked() && isOnMainViewPort;
178
179
		auto cursor = topLeft;
180
		int col = 0;
181
		for (auto& icon : iconInfo) {
182
			if (!icon.enable) continue;
183
184
			// is the icon on or off?
185
			bool state = [&] {
186
				try {
187
					return icon.expr.evalBool(manager.getInterpreter());
188
				} catch (CommandException&) {
189
					return false; // TODO report warning??
190
				}
191
			}();
192
			if (state != icon.lastState) {
193
				icon.lastState = state;
194
				icon.time = 0.0f;
195
			}
196
			icon.time += io.DeltaTime;
197
198
			// calculate fade status
199
			float alpha = [&] {
200
				if (!fade || !icon.fade) return 1.0f;
201
				auto t = icon.time - fadeDelay;
202
				if (t <= 0.0f) return 1.0f;
203
				if (t >= fadeDuration) return 0.0f;
204
				return 1.0f - (t / fadeDuration);
205
			}();
206
207
			// draw icon
208
			const auto& ic = state ? icon.on : icon.off;
209
			if (alpha > 0.0f && ic.tex.get()) {
210
				ImGui::SetCursorPos(cursor);
211
				ImGui::ImageWithBg(ic.tex.getImGui(), gl::vec2(ic.size),
212
				                   {0.0f, 0.0f}, {1.0f, 1.0f},
213
				                   {}, {1.0f, 1.0f, 1.0f, alpha});
214
			}
215
216
			// draw outline
217
			if (showConfigureIcons) {
218
				ImGui::SetCursorPos(cursor);
219
				gl::vec2 rectMin = ImGui::GetCursorScreenPos();
220
				gl::vec2 rectMax = rectMin + gl::vec2(maxIconSize);
221
				ImGui::GetWindowDrawList()->AddRect(rectMin, rectMax, IM_COL32(255, 0, 0, 128), 0.0f, 1.0f);
222
			}
223
224
			// advance to next icon position
225
			if (++col == columns) {
226
				col = 0;
227
				cursor = gl::vec2(topLeft.x, cursor.y + style.ItemSpacing.y + maxIconSize.y);
228
			} else {
229
				cursor = gl::vec2(cursor.x + style.ItemSpacing.x + maxIconSize.x, cursor.y);
230
			}
231
		}
232
233
		if (hideTitle && ImGui::IsWindowFocused()) {
234
			ImGui::SetWindowFocus(nullptr); // give-up focus
235
		}
236
	});
237
}
238
239
void ImGuiOsdIcons::paintConfigureIcons()
240
{
241
	ImGui::SetNextWindowSize(gl::vec2{37, 17} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
242
	im::Window("Configure Icons", &showConfigureIcons, [&]{
243
		ImGui::Checkbox("Show OSD icons", &showIcons);
244
		ImGui::Separator();
245
246
		if (ImGui::Checkbox("Hide Title", &hideTitle)) {
247
			// reset fade-out-delay (on hiding the title)
248
			for (auto& icon : iconInfo) {
249
				icon.time = 0.0f;
250
			}
251
		}
252
		HelpMarker("When you want the icons inside the MSX window, you might want to hide the window title.\n"
253
		           "To further hide the icons, it's possible to make them fade-out after some time.");
254
		im::Indent([&]{
255
			im::Disabled(!hideTitle, [&]{
256
				ImGui::Checkbox("Allow move", &allowMove);
257
				HelpMarker("When the icons are in the MSX window (without title bar), you might want "
258
				           "to lock them in place, to prevent them from being moved by accident.\n"
259
				           "Move by click and drag the icon box.\n");
260
				auto width = ImGui::GetFontSize() * 10.0f;
261
				ImGui::SetNextItemWidth(width);
262
				ImGui::SliderFloat("Fade-out delay",    &fadeDelay,    0.0f, 30.0f, "%.1f");
263
				HelpMarker("After some delay, fade-out icons that haven't changed status for a while.\n"
264
				           "Note: by default some icons are configured to never fade-out.");
265
				ImGui::SetNextItemWidth(width);
266
				ImGui::SliderFloat("Fade-out duration", &fadeDuration, 0.0f, 30.0f, "%.1f");
267
				HelpMarker("Configure the fade-out speed.");
268
			});
269
		});
270
		ImGui::Separator();
271
272
		im::TreeNode("Configure individual icons", [&]{
273
			HelpMarker("Change the order and properties of the icons (for advanced users).\n"
274
			           "Right-click in a row to reorder, insert, delete that row.\n"
275
			           "Right-click on an icon to remove it.");
276
			int flags = ImGuiTableFlags_RowBg |
277
					ImGuiTableFlags_BordersOuterH |
278
					ImGuiTableFlags_BordersInnerH |
279
					ImGuiTableFlags_BordersV |
280
					ImGuiTableFlags_BordersOuter |
281
					ImGuiTableFlags_Resizable;
282
			im::Table("table", 5, flags, [&]{
283
				ImGui::TableSetupColumn("Enabled", ImGuiTableColumnFlags_WidthFixed);
284
				ImGui::TableSetupColumn("Fade-out", ImGuiTableColumnFlags_WidthFixed);
285
				ImGui::TableSetupColumn("True-image", ImGuiTableColumnFlags_WidthFixed);
286
				ImGui::TableSetupColumn("False-image", ImGuiTableColumnFlags_WidthFixed);
287
				ImGui::TableSetupColumn("Expression");
288
				ImGui::TableHeadersRow();
289
290
				enum class Cmd : uint8_t { MOVE_FRONT, MOVE_FWD, MOVE_BWD, MOVE_BACK, INSERT, DELETE };
291
				using enum Cmd;
292
				std::pair<int, Cmd> cmd(-1, MOVE_FRONT);
293
				auto lastRow = narrow<int>(iconInfo.size()) - 1;
294
				im::ID_for_range(iconInfo.size(), [&](int row) {
295
					auto& icon = iconInfo[row];
296
					if (ImGui::TableNextColumn()) { // enabled
297
						auto pos = ImGui::GetCursorPos();
298
						const auto& style = ImGui::GetStyle();
299
						auto textHeight = ImGui::GetTextLineHeight();
300
						float rowHeight = std::max({
301
							2.0f * style.FramePadding.y + textHeight,
302
							float(icon.on.size.y),
303
							float(icon.off.size.y)});
304
						ImGui::Selectable("##row", false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap, ImVec2(0, rowHeight));
305
						if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) {
306
							ImGui::OpenPopup("config-icon-context");
307
						}
308
						im::Popup("config-icon-context", [&]{
309
							if (lastRow >= 1) { // at least 2 rows
310
								if (row != 0) {
311
									if (ImGui::MenuItem("Move to front")) cmd = {row, MOVE_FRONT};
312
									if (ImGui::MenuItem("Move forwards")) cmd = {row, MOVE_FWD};
313
								}
314
								if (row != lastRow) {
315
									if (ImGui::MenuItem("Move backwards"))cmd = {row, MOVE_BWD};
316
									if (ImGui::MenuItem("Move to back"))  cmd = {row, MOVE_BACK};
317
								}
318
								ImGui::Separator();
319
							}
320
							if (ImGui::MenuItem("Insert new row"))     cmd = {row, INSERT};
321
							if (ImGui::MenuItem("Delete current row")) cmd = {row, DELETE};
322
						});
323
324
						ImGui::SetCursorPos(pos);
325
						if (ImGui::Checkbox("##enabled", &icon.enable)) {
326
							iconInfoDirty = true;
327
						}
328
					}
329
					if (ImGui::TableNextColumn()) { // fade-out
330
						im::Disabled(!hideTitle, [&]{
331
							if (ImGui::Checkbox("##fade-out", &icon.fade)) {
332
								iconInfoDirty = true;
333
							}
334
						});
335
					}
336
337
					auto image = [&](IconInfo::Icon& ic, const char* id) {
338
						if (ic.tex.get()) {
339
							ImGui::Image(ic.tex.getImGui(), gl::vec2(ic.size));
340
							im::PopupContextItem(id, [&]{
341
								if (ImGui::MenuItem("Remove image")) {
342
									ic.filename.clear();
343
									iconInfoDirty = true;
344
									ImGui::CloseCurrentPopup();
345
								}
346
							});
347
						} else {
348
							ImGui::Button("Select...");
349
						}
350
						if (ImGui::IsItemClicked()) {
351
							manager.openFile->selectFile(
352
								"Select image for icon", "PNG (*.png){.png}",
353
								[this, &ic](const std::string& filename) {
354
									ic.filename = filename;
355
									iconInfoDirty = true;
356
								});
357
						}
358
					};
359
					if (ImGui::TableNextColumn()) { // true-image
360
						image(icon.on, "##on");
361
					}
362
					if (ImGui::TableNextColumn()) { // false-image
363
						image(icon.off, "##off");
364
					}
365
					if (ImGui::TableNextColumn()) { // expression
366
						ImGui::SetNextItemWidth(-FLT_MIN);
367
						bool valid = manager.getInterpreter().validExpression(icon.expr.getString());
368
						im::StyleColor(!valid, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
369
							auto expr = std::string(icon.expr.getString());
370
							if (ImGui::InputText("##expr", &expr)) {
371
								icon.expr = expr;
372
								iconInfoDirty = true;
373
							}
374
						});
375
					}
376
				});
377
				if (int row = cmd.first; row != -1) {
378
					switch (cmd.second) {
379
					case MOVE_FRONT:
380
						assert(row >= 1);
381
						std::rotate(&iconInfo[0], &iconInfo[row], &iconInfo[row + 1]);
382
						break;
383
					case MOVE_FWD:
384
						assert(row >= 1);
385
						std::swap(iconInfo[row], iconInfo[row - 1]);
386
						break;
387
					case MOVE_BWD:
388
						assert(row < narrow<int>(iconInfo.size() - 1));
389
						std::swap(iconInfo[row], iconInfo[row + 1]);
390
						break;
391
					case MOVE_BACK:
392
						assert(row < narrow<int>(iconInfo.size() - 1));
393
						std::rotate(&iconInfo[row], &iconInfo[row + 1], &iconInfo[lastRow + 1]);
394
						break;
395
					case INSERT:
396
						iconInfo.emplace(iconInfo.begin() + row, TclObject("true"), "", "", true);
397
						break;
398
					case DELETE:
399
						iconInfo.erase(iconInfo.begin() + row);
400
						break;
401
					}
402
					iconInfoDirty = true;
403
				}
404
			});
405
406
			if (ImGui::Button("Restore default")) {
407
				setDefaultIcons();
408
			}
409
		});
410
	});
411
}
412
413
} // namespace openmsx