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