openMSX
ImGuiOsdIcons.cc
Go to the documentation of this file.
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
20using namespace std::literals;
21
22
23namespace openmsx {
24
26 : ImGuiPart(manager_)
27{
28 // Usually immediately overridden by loading imgui.ini
29 // But nevertheless required for the initial state.
30 setDefaultIcons();
31}
32
33void 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
48{
49 iconInfo.clear();
50}
51
52void 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
81{
82 if (iconInfo.empty()) setDefaultIcons();
83 iconInfoDirty = true;
84}
85
86void 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
102void 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
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::Image(ic.tex.getImGui(), gl::vec2(ic.size),
212 {0.0f, 0.0f}, {1.0f, 1.0f}, {1.0f, 1.0f, 1.0f, alpha});
213 }
214
215 // draw outline
216 if (showConfigureIcons) {
217 ImGui::SetCursorPos(cursor);
218 gl::vec2 rectMin = ImGui::GetCursorScreenPos();
219 gl::vec2 rectMax = rectMin + gl::vec2(maxIconSize);
220 ImGui::GetWindowDrawList()->AddRect(rectMin, rectMax, IM_COL32(255, 0, 0, 128), 0.0f, 0, 1.0f);
221 }
222
223 // advance to next icon position
224 if (++col == columns) {
225 col = 0;
226 cursor = gl::vec2(topLeft.x, cursor.y + style.ItemSpacing.y + maxIconSize.y);
227 } else {
228 cursor = gl::vec2(cursor.x + style.ItemSpacing.x + maxIconSize.x, cursor.y);
229 }
230 }
231
232 if (hideTitle && ImGui::IsWindowFocused()) {
233 ImGui::SetWindowFocus(nullptr); // give-up focus
234 }
235 });
236}
237
238void ImGuiOsdIcons::paintConfigureIcons()
239{
240 ImGui::SetNextWindowSize(gl::vec2{37, 17} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
241 im::Window("Configure Icons", &showConfigureIcons, [&]{
242 ImGui::Checkbox("Show OSD icons", &showIcons);
243 ImGui::Separator();
244
245 if (ImGui::Checkbox("Hide Title", &hideTitle)) {
246 // reset fade-out-delay (on hiding the title)
247 for (auto& icon : iconInfo) {
248 icon.time = 0.0f;
249 }
250 }
251 HelpMarker("When you want the icons inside the MSX window, you might want to hide the window title.\n"
252 "To further hide the icons, it's possible to make them fade-out after some time.");
253 im::Indent([&]{
254 im::Disabled(!hideTitle, [&]{
255 ImGui::Checkbox("Allow move", &allowMove);
256 HelpMarker("When the icons are in the MSX window (without title bar), you might want "
257 "to lock them in place, to prevent them from being moved by accident.\n"
258 "Move by click and drag the icon box.\n");
259 auto width = ImGui::GetFontSize() * 10.0f;
260 ImGui::SetNextItemWidth(width);
261 ImGui::SliderFloat("Fade-out delay", &fadeDelay, 0.0f, 30.0f, "%.1f");
262 HelpMarker("After some delay, fade-out icons that haven't changed status for a while.\n"
263 "Note: by default some icons are configured to never fade-out.");
264 ImGui::SetNextItemWidth(width);
265 ImGui::SliderFloat("Fade-out duration", &fadeDuration, 0.0f, 30.0f, "%.1f");
266 HelpMarker("Configure the fade-out speed.");
267 });
268 });
269 ImGui::Separator();
270
271 im::TreeNode("Configure individual icons", [&]{
272 HelpMarker("Change the order and properties of the icons (for advanced users).\n"
273 "Right-click in a row to reorder, insert, delete that row.\n"
274 "Right-click on an icon to remove it.");
275 int flags = ImGuiTableFlags_RowBg |
276 ImGuiTableFlags_BordersOuterH |
277 ImGuiTableFlags_BordersInnerH |
278 ImGuiTableFlags_BordersV |
279 ImGuiTableFlags_BordersOuter |
280 ImGuiTableFlags_Resizable;
281 im::Table("table", 5, flags, [&]{
282 ImGui::TableSetupColumn("Enabled", ImGuiTableColumnFlags_WidthFixed);
283 ImGui::TableSetupColumn("Fade-out", ImGuiTableColumnFlags_WidthFixed);
284 ImGui::TableSetupColumn("True-image", ImGuiTableColumnFlags_WidthFixed);
285 ImGui::TableSetupColumn("False-image", ImGuiTableColumnFlags_WidthFixed);
286 ImGui::TableSetupColumn("Expression");
287 ImGui::TableHeadersRow();
288
289 enum class Cmd { MOVE_FRONT, MOVE_FWD, MOVE_BWD, MOVE_BACK, INSERT, DELETE };
290 using enum Cmd;
291 std::pair<int, Cmd> cmd(-1, MOVE_FRONT);
292 auto lastRow = narrow<int>(iconInfo.size()) - 1;
293 im::ID_for_range(iconInfo.size(), [&](int row) {
294 auto& icon = iconInfo[row];
295 if (ImGui::TableNextColumn()) { // enabled
296 auto pos = ImGui::GetCursorPos();
297 const auto& style = ImGui::GetStyle();
298 auto textHeight = ImGui::GetTextLineHeight();
299 float rowHeight = std::max({
300 2.0f * style.FramePadding.y + textHeight,
301 float(icon.on.size.y),
302 float(icon.off.size.y)});
303 ImGui::Selectable("##row", false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap, ImVec2(0, rowHeight));
304 if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) {
305 ImGui::OpenPopup("config-icon-context");
306 }
307 im::Popup("config-icon-context", [&]{
308 if (lastRow >= 1) { // at least 2 rows
309 if (row != 0) {
310 if (ImGui::MenuItem("Move to front")) cmd = {row, MOVE_FRONT};
311 if (ImGui::MenuItem("Move forwards")) cmd = {row, MOVE_FWD};
312 }
313 if (row != lastRow) {
314 if (ImGui::MenuItem("Move backwards"))cmd = {row, MOVE_BWD};
315 if (ImGui::MenuItem("Move to back")) cmd = {row, MOVE_BACK};
316 }
317 ImGui::Separator();
318 }
319 if (ImGui::MenuItem("Insert new row")) cmd = {row, INSERT};
320 if (ImGui::MenuItem("Delete current row")) cmd = {row, DELETE};
321 });
322
323 ImGui::SetCursorPos(pos);
324 if (ImGui::Checkbox("##enabled", &icon.enable)) {
325 iconInfoDirty = true;
326 }
327 }
328 if (ImGui::TableNextColumn()) { // fade-out
329 im::Disabled(!hideTitle, [&]{
330 if (ImGui::Checkbox("##fade-out", &icon.fade)) {
331 iconInfoDirty = true;
332 }
333 });
334 }
335
336 auto image = [&](IconInfo::Icon& ic, const char* id) {
337 if (ic.tex.get()) {
338 ImGui::Image(ic.tex.getImGui(), gl::vec2(ic.size));
339 im::PopupContextItem(id, [&]{
340 if (ImGui::MenuItem("Remove image")) {
341 ic.filename.clear();
342 iconInfoDirty = true;
343 ImGui::CloseCurrentPopup();
344 }
345 });
346 } else {
347 ImGui::Button("Select ...");
348 }
349 if (ImGui::IsItemClicked()) {
350 manager.openFile->selectFile(
351 "Select image for icon", "PNG (*.png){.png}",
352 [this, &ic](const std::string& filename) {
353 ic.filename = filename;
354 iconInfoDirty = true;
355 });
356 }
357 };
358 if (ImGui::TableNextColumn()) { // true-image
359 image(icon.on, "##on");
360 }
361 if (ImGui::TableNextColumn()) { // false-image
362 image(icon.off, "##off");
363 }
364 if (ImGui::TableNextColumn()) { // expression
365 ImGui::SetNextItemWidth(-FLT_MIN);
366 bool valid = manager.getInterpreter().validExpression(icon.expr.getString());
367 im::StyleColor(!valid, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
368 auto expr = std::string(icon.expr.getString());
369 if (ImGui::InputText("##expr", &expr)) {
370 icon.expr = expr;
371 iconInfoDirty = true;
372 }
373 });
374 }
375 });
376 if (int row = cmd.first; row != -1) {
377 switch (cmd.second) {
378 case MOVE_FRONT:
379 assert(row >= 1);
380 std::rotate(&iconInfo[0], &iconInfo[row], &iconInfo[row + 1]);
381 break;
382 case MOVE_FWD:
383 assert(row >= 1);
384 std::swap(iconInfo[row], iconInfo[row - 1]);
385 break;
386 case MOVE_BWD:
387 assert(row < narrow<int>(iconInfo.size() - 1));
388 std::swap(iconInfo[row], iconInfo[row + 1]);
389 break;
390 case MOVE_BACK:
391 assert(row < narrow<int>(iconInfo.size() - 1));
392 std::rotate(&iconInfo[row], &iconInfo[row + 1], &iconInfo[lastRow + 1]);
393 break;
394 case INSERT:
395 iconInfo.emplace(iconInfo.begin() + row, TclObject("true"), "", "", true);
396 break;
397 case DELETE:
398 iconInfo.erase(iconInfo.begin() + row);
399 break;
400 }
401 iconInfoDirty = true;
402 }
403 });
404
405 if (ImGui::Button("Restore default")) {
406 setDefaultIcons();
407 }
408 });
409 });
410}
411
412} // namespace openmsx
std::string image
Definition HDImageCLI.cc:16
uintptr_t id
TclObject t
bool loadLine(std::string_view name, zstring_view value)
void save(ImGuiTextBuffer &buf)
Interpreter & getInterpreter()
std::unique_ptr< ImGuiOpenFile > openFile
void save(ImGuiTextBuffer &buf) override
void loadEnd() override
void loadLine(std::string_view name, zstring_view value) override
void paint(MSXMotherBoard *motherBoard) override
ImGuiOsdIcons(ImGuiManager &manager_)
void loadStart() override
ImGuiManager & manager
Definition ImGuiPart.hh:30
bool validExpression(std::string_view expression)
Like std::string_view, but with the extra guarantee that it refers to a zero-terminated string.
constexpr auto enumerate(Iterable &&iterable)
Heavily inspired by Nathan Reed's blog post: Python-Like enumerate() In C++17 http://reedbeta....
Definition enumerate.hh:28
bool stringToBool(string_view str)
Definition StringOp.cc:16
std::pair< string_view, string_view > splitOnFirst(string_view str, string_view chars)
Definition StringOp.cc:95
vecN< 2, float > vec2
Definition gl_vec.hh:382
constexpr vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
Definition gl_vec.hh:449
void Table(const char *str_id, int column, ImGuiTableFlags flags, const ImVec2 &outer_size, float inner_width, std::invocable<> auto next)
Definition ImGuiCpp.hh:455
void Window(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:63
void PopupContextItem(const char *str_id, ImGuiPopupFlags popup_flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:421
bool TreeNode(const char *label, ImGuiTreeNodeFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:302
void StyleColor(bool active, Args &&...args)
Definition ImGuiCpp.hh:175
void Disabled(bool b, std::invocable<> auto next)
Definition ImGuiCpp.hh:506
void Indent(float indent_w, std::invocable<> auto next)
Definition ImGuiCpp.hh:224
void ID_for_range(std::integral auto count, std::invocable< int > auto next)
Definition ImGuiCpp.hh:281
SDLSurfacePtr load(const std::string &filename, bool want32bpp)
Load the given PNG file in a SDL_Surface.
Definition PNG.cc:106
This file implemented 3 utility functions:
Definition Autofire.cc:11
const FileContext & systemFileContext()
bool loadOnePersistent(std::string_view name, zstring_view value, C &c, const std::tuple< Elements... > &tup)
void savePersistent(ImGuiTextBuffer &buf, C &c, const std::tuple< Elements... > &tup)
void HelpMarker(std::string_view desc)
Definition ImGuiUtils.cc:23
ImU32 getColor(imColor col)