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 iconsTotalSize = gl::ivec2();
105 iconsMaxSize = gl::ivec2();
106 iconsNumEnabled = 0;
107 FileContext context = systemFileContext();
108 for (auto& icon : iconInfo) {
109 auto load = [&](IconInfo::Icon& i) {
110 try {
111 if (!i.filename.empty()) {
112 auto r = context.resolve(i.filename);
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 if (icon.enable) {
125 ++iconsNumEnabled;
126 auto m = max(icon.on.size, icon.off.size);
127 iconsTotalSize += m;
128 iconsMaxSize = max(iconsMaxSize, m);
129 }
130 }
131 iconInfoDirty = false;
132}
133
135{
136 if (iconInfoDirty) loadIcons();
137 if (showConfigureIcons) paintConfigureIcons();
138 if (!showIcons) return;
139
140 const auto& style = ImGui::GetStyle();
141 auto windowPadding = 2.0f * gl::vec2(style.WindowPadding);
142 auto totalSize = windowPadding + gl::vec2(iconsTotalSize) + float(iconsNumEnabled) * gl::vec2(style.ItemSpacing);
143 auto minSize = iconsHorizontal
144 ? gl::vec2(totalSize.x, float(iconsMaxSize.y) + windowPadding.y)
145 : gl::vec2(float(iconsMaxSize.x) + windowPadding.x, totalSize.y);
146 if (!iconsHideTitle) {
147 minSize.y += 2.0f * style.FramePadding.y + ImGui::GetTextLineHeight();
148 }
149 auto maxSize = iconsHorizontal
150 ? gl::vec2(FLT_MAX, minSize.y)
151 : gl::vec2(minSize.x, FLT_MAX);
152 ImGui::SetNextWindowSizeConstraints(minSize, maxSize);
153
154 // default placement: bottom left
155 const auto* mainViewPort = ImGui::GetMainViewport();
156 ImGui::SetNextWindowPos(gl::vec2(mainViewPort->Pos) + gl::vec2{10.0f, mainViewPort->WorkSize.y - 10.0f},
157 ImGuiCond_FirstUseEver,
158 {0.0f, 1.0f}); // pivot = bottom-left
159 int flags = iconsHideTitle ? ImGuiWindowFlags_NoTitleBar |
160 ImGuiWindowFlags_NoResize |
161 ImGuiWindowFlags_NoScrollbar |
162 ImGuiWindowFlags_NoScrollWithMouse |
163 ImGuiWindowFlags_NoCollapse |
164 ImGuiWindowFlags_NoBackground |
165 ImGuiWindowFlags_NoFocusOnAppearing |
166 ImGuiWindowFlags_NoNav |
167 (iconsAllowMove ? 0 : ImGuiWindowFlags_NoMove)
168 : 0;
169 adjust.pre();
170 im::Window("Icons", &showIcons, flags | ImGuiWindowFlags_HorizontalScrollbar, [&]{
171 bool isOnMainViewPort = adjust.post();
172 auto cursor0 = ImGui::GetCursorPos();
173 auto availableSize = ImGui::GetContentRegionAvail();
174 float slack = iconsHorizontal ? (availableSize.x - totalSize.x)
175 : (availableSize.y - totalSize.y);
176 float spacing = (iconsNumEnabled >= 2) ? (std::max(0.0f, slack) / float(iconsNumEnabled)) : 0.0f;
177
178 bool fade = iconsHideTitle && !ImGui::IsWindowDocked() && isOnMainViewPort;
179 for (auto& icon : iconInfo) {
180 if (!icon.enable) continue;
181
182 bool state = [&] {
183 try {
184 return icon.expr.evalBool(manager.getInterpreter());
185 } catch (CommandException&) {
186 return false; // TODO report warning??
187 }
188 }();
189 if (state != icon.lastState) {
190 icon.lastState = state;
191 icon.time = 0.0f;
192 }
193 const auto& io = ImGui::GetIO();
194 icon.time += io.DeltaTime;
195 float alpha = [&] {
196 if (!fade || !icon.fade) return 1.0f;
197 auto t = icon.time - iconsFadeDelay;
198 if (t <= 0.0f) return 1.0f;
199 if (t >= iconsFadeDuration) return 0.0f;
200 return 1.0f - (t / iconsFadeDuration);
201 }();
202
203 const auto& ic = state ? icon.on : icon.off;
204 if (ic.tex.get()) {
205 gl::vec2 cursor = ImGui::GetCursorPos();
206 ImGui::Image(ic.tex.getImGui(), gl::vec2(ic.size),
207 {0.0f, 0.0f}, {1.0f, 1.0f}, {1.0f, 1.0f, 1.0f, alpha});
208 ImGui::SetCursorPos(cursor);
209 }
210
211 auto size = gl::vec2(max(icon.on.size, icon.off.size));
212 (iconsHorizontal ? size.x : size.y) += spacing;
213 ImGui::Dummy(size);
214 if (iconsHorizontal) ImGui::SameLine();
215 }
216
217 ImGui::SetCursorPos(cursor0); // cover full window for context menu
218 ImGui::Dummy(availableSize);
219 if (iconsAllowMove) {
220 im::PopupContextItem("icons context menu", [&]{
221 if (ImGui::MenuItem("Configure icons ...")) {
222 showConfigureIcons = true;
223 }
224 });
225 }
226
227 if (iconsHideTitle && ImGui::IsWindowFocused()) {
228 ImGui::SetWindowFocus(nullptr); // give-up focus
229 }
230 });
231}
232
233void ImGuiOsdIcons::paintConfigureIcons()
234{
235 ImGui::SetNextWindowSize(gl::vec2{37, 17} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
236 im::Window("Configure Icons", &showConfigureIcons, [&]{
237 ImGui::Checkbox("Show OSD icons", &showIcons);
238 ImGui::TextUnformatted("Layout:"sv);
239 ImGui::SameLine();
240 ImGui::RadioButton("Horizontal", &iconsHorizontal, 1);
241 ImGui::SameLine();
242 ImGui::RadioButton("Vertical", &iconsHorizontal, 0);
243 ImGui::Separator();
244
245 if (ImGui::Checkbox("Hide Title", &iconsHideTitle)) {
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(!iconsHideTitle, [&]{
255 ImGui::Checkbox("Allow move", &iconsAllowMove);
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", &iconsFadeDelay, 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", &iconsFadeDuration, 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(2.0f * style.FramePadding.y + textHeight,
300 std::max(float(icon.on.size.y), float(icon.off.size.y)));
301 ImGui::Selectable("##row", false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap, ImVec2(0, rowHeight));
302 if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) {
303 ImGui::OpenPopup("config-icon-context");
304 }
305 im::Popup("config-icon-context", [&]{
306 if (lastRow >= 1) { // at least 2 rows
307 if (row != 0) {
308 if (ImGui::MenuItem("Move to front")) cmd = {row, MOVE_FRONT};
309 if (ImGui::MenuItem("Move forwards")) cmd = {row, MOVE_FWD};
310 }
311 if (row != lastRow) {
312 if (ImGui::MenuItem("Move backwards"))cmd = {row, MOVE_BWD};
313 if (ImGui::MenuItem("Move to back")) cmd = {row, MOVE_BACK};
314 }
315 ImGui::Separator();
316 }
317 if (ImGui::MenuItem("Insert new row")) cmd = {row, INSERT};
318 if (ImGui::MenuItem("Delete current row")) cmd = {row, DELETE};
319 });
320
321 ImGui::SetCursorPos(pos);
322 if (ImGui::Checkbox("##enabled", &icon.enable)) {
323 iconInfoDirty = true;
324 }
325 }
326 if (ImGui::TableNextColumn()) { // fade-out
327 im::Disabled(!iconsHideTitle, [&]{
328 if (ImGui::Checkbox("##fade-out", &icon.fade)) {
329 iconInfoDirty = true;
330 }
331 });
332 }
333
334 auto image = [&](IconInfo::Icon& ic, const char* id) {
335 if (ic.tex.get()) {
336 ImGui::Image(ic.tex.getImGui(), gl::vec2(ic.size));
337 im::PopupContextItem(id, [&]{
338 if (ImGui::MenuItem("Remove image")) {
339 ic.filename.clear();
340 iconInfoDirty = true;
341 ImGui::CloseCurrentPopup();
342 }
343 });
344 } else {
345 ImGui::Button("Select ...");
346 }
347 if (ImGui::IsItemClicked()) {
348 manager.openFile->selectFile(
349 "Select image for icon", "PNG (*.png){.png}",
350 [this, &ic](const std::string& filename) {
351 ic.filename = filename;
352 iconInfoDirty = true;
353 });
354 }
355 };
356 if (ImGui::TableNextColumn()) { // true-image
357 image(icon.on, "##on");
358 }
359 if (ImGui::TableNextColumn()) { // false-image
360 image(icon.off, "##off");
361 }
362 if (ImGui::TableNextColumn()) { // expression
363 ImGui::SetNextItemWidth(-FLT_MIN);
364 bool valid = manager.getInterpreter().validExpression(icon.expr.getString());
365 im::StyleColor(!valid, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
366 auto expr = std::string(icon.expr.getString());
367 if (ImGui::InputText("##expr", &expr)) {
368 icon.expr = expr;
369 iconInfoDirty = true;
370 }
371 });
372 }
373 });
374 if (int row = cmd.first; row != -1) {
375 switch (cmd.second) {
376 case MOVE_FRONT:
377 assert(row >= 1);
378 std::rotate(&iconInfo[0], &iconInfo[row], &iconInfo[row + 1]);
379 break;
380 case MOVE_FWD:
381 assert(row >= 1);
382 std::swap(iconInfo[row], iconInfo[row - 1]);
383 break;
384 case MOVE_BWD:
385 assert(row < narrow<int>(iconInfo.size() - 1));
386 std::swap(iconInfo[row], iconInfo[row + 1]);
387 break;
388 case MOVE_BACK:
389 assert(row < narrow<int>(iconInfo.size() - 1));
390 std::rotate(&iconInfo[row], &iconInfo[row + 1], &iconInfo[lastRow + 1]);
391 break;
392 case INSERT:
393 iconInfo.emplace(iconInfo.begin() + row, TclObject("true"), "", "", true);
394 break;
395 case DELETE:
396 iconInfo.erase(iconInfo.begin() + row);
397 break;
398 }
399 iconInfoDirty = true;
400 }
401 });
402
403 if (ImGui::Button("Restore default")) {
404 setDefaultIcons();
405 }
406 });
407 });
408}
409
410} // 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
void TextUnformatted(const std::string &str)
Definition ImGuiUtils.hh:24
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, int > ivec2
Definition gl_vec.hh:181
vecN< 2, float > vec2
Definition gl_vec.hh:178
constexpr vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
Definition gl_vec.hh:320
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)