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 gl::vec2 cursor = ImGui::GetCursorPos();
201 ImGui::Image(ic.tex.getImGui(), gl::vec2(ic.size),
202 {0.0f, 0.0f}, {1.0f, 1.0f}, {1.0f, 1.0f, 1.0f, alpha});
203
204 ImGui::SetCursorPos(cursor);
205 auto size = gl::vec2(max(icon.on.size, icon.off.size));
206 (iconsHorizontal ? size.x : size.y) += spacing;
207 ImGui::Dummy(size);
208 if (iconsHorizontal) ImGui::SameLine();
209 }
210
211 ImGui::SetCursorPos(cursor0); // cover full window for context menu
212 ImGui::Dummy(availableSize);
213 if (iconsAllowMove) {
214 im::PopupContextItem("icons context menu", [&]{
215 if (ImGui::MenuItem("Configure icons ...")) {
216 showConfigureIcons = true;
217 }
218 });
219 }
220 });
221}
222
223void ImGuiOsdIcons::paintConfigureIcons()
224{
225 ImGui::SetNextWindowSize(gl::vec2{37, 17} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
226 im::Window("Configure Icons", &showConfigureIcons, [&]{
227 ImGui::Checkbox("Show OSD icons", &showIcons);
228 ImGui::TextUnformatted("Layout:"sv);
229 ImGui::SameLine();
230 ImGui::RadioButton("Horizontal", &iconsHorizontal, 1);
231 ImGui::SameLine();
232 ImGui::RadioButton("Vertical", &iconsHorizontal, 0);
233 ImGui::Separator();
234
235 if (ImGui::Checkbox("Hide Title", &iconsHideTitle)) {
236 // reset fade-out-delay (on hiding the title)
237 for (auto& icon : iconInfo) {
238 icon.time = 0.0f;
239 }
240 }
241 HelpMarker("When you want the icons inside the MSX window, you might want to hide the window title.\n"
242 "To further hide the icons, it's possible to make them fade-out after some time.");
243 im::Indent([&]{
244 im::Disabled(!iconsHideTitle, [&]{
245 ImGui::Checkbox("Allow move", &iconsAllowMove);
246 HelpMarker("When the icons are in the MSX window (without title bar), you might want "
247 "to lock them in place, to prevent them from being moved by accident.\n"
248 "Move by click and drag the icon box.\n");
249 auto width = ImGui::GetFontSize() * 10.0f;
250 ImGui::SetNextItemWidth(width);
251 ImGui::SliderFloat("Fade-out delay", &iconsFadeDelay, 0.0f, 30.0f, "%.1f");
252 HelpMarker("After some delay, fade-out icons that haven't changed status for a while.\n"
253 "Note: by default some icons are configured to never fade-out.");
254 ImGui::SetNextItemWidth(width);
255 ImGui::SliderFloat("Fade-out duration", &iconsFadeDuration, 0.0f, 30.0f, "%.1f");
256 HelpMarker("Configure the fade-out speed.");
257 });
258 });
259 ImGui::Separator();
260
261 im::TreeNode("Configure individual icons", [&]{
262 HelpMarker("Change the order and properties of the icons (for advanced users).\n"
263 "Right-click in a row to reorder, insert, delete that row.\n"
264 "Right-click on an icon to remove it.");
265 int flags = ImGuiTableFlags_RowBg |
266 ImGuiTableFlags_BordersOuterH |
267 ImGuiTableFlags_BordersInnerH |
268 ImGuiTableFlags_BordersV |
269 ImGuiTableFlags_BordersOuter |
270 ImGuiTableFlags_Resizable;
271 im::Table("table", 5, flags, [&]{
272 ImGui::TableSetupColumn("Enabled", ImGuiTableColumnFlags_WidthFixed);
273 ImGui::TableSetupColumn("Fade-out", ImGuiTableColumnFlags_WidthFixed);
274 ImGui::TableSetupColumn("True-image", ImGuiTableColumnFlags_WidthFixed);
275 ImGui::TableSetupColumn("False-image", ImGuiTableColumnFlags_WidthFixed);
276 ImGui::TableSetupColumn("Expression");
277 ImGui::TableHeadersRow();
278
279 enum class Cmd { MOVE_FRONT, MOVE_FWD, MOVE_BWD, MOVE_BACK, INSERT, DELETE };
280 using enum Cmd;
281 std::pair<int, Cmd> cmd(-1, MOVE_FRONT);
282 auto lastRow = narrow<int>(iconInfo.size()) - 1;
283 im::ID_for_range(iconInfo.size(), [&](int row) {
284 auto& icon = iconInfo[row];
285 if (ImGui::TableNextColumn()) { // enabled
286 auto pos = ImGui::GetCursorPos();
287 const auto& style = ImGui::GetStyle();
288 auto textHeight = ImGui::GetTextLineHeight();
289 float rowHeight = std::max(2.0f * style.FramePadding.y + textHeight,
290 std::max(float(icon.on.size.y), float(icon.off.size.y)));
291 ImGui::Selectable("##row", false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap, ImVec2(0, rowHeight));
292 if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) {
293 ImGui::OpenPopup("config-icon-context");
294 }
295 im::Popup("config-icon-context", [&]{
296 if (lastRow >= 1) { // at least 2 rows
297 if (row != 0) {
298 if (ImGui::MenuItem("Move to front")) cmd = {row, MOVE_FRONT};
299 if (ImGui::MenuItem("Move forwards")) cmd = {row, MOVE_FWD};
300 }
301 if (row != lastRow) {
302 if (ImGui::MenuItem("Move backwards"))cmd = {row, MOVE_BWD};
303 if (ImGui::MenuItem("Move to back")) cmd = {row, MOVE_BACK};
304 }
305 ImGui::Separator();
306 }
307 if (ImGui::MenuItem("Insert new row")) cmd = {row, INSERT};
308 if (ImGui::MenuItem("Delete current row")) cmd = {row, DELETE};
309 });
310
311 ImGui::SetCursorPos(pos);
312 if (ImGui::Checkbox("##enabled", &icon.enable)) {
313 iconInfoDirty = true;
314 }
315 }
316 if (ImGui::TableNextColumn()) { // fade-out
317 im::Disabled(!iconsHideTitle, [&]{
318 if (ImGui::Checkbox("##fade-out", &icon.fade)) {
319 iconInfoDirty = true;
320 }
321 });
322 }
323
324 auto image = [&](IconInfo::Icon& ic, const char* id) {
325 if (ic.tex.get()) {
326 ImGui::Image(ic.tex.getImGui(), gl::vec2(ic.size));
327 im::PopupContextItem(id, [&]{
328 if (ImGui::MenuItem("Remove image")) {
329 ic.filename.clear();
330 iconInfoDirty = true;
331 ImGui::CloseCurrentPopup();
332 }
333 });
334 } else {
335 ImGui::Button("Select ...");
336 }
337 if (ImGui::IsItemClicked()) {
338 manager.openFile->selectFile(
339 "Select image for icon", "PNG (*.png){.png}",
340 [this, &ic](const std::string& filename) {
341 ic.filename = filename;
342 iconInfoDirty = true;
343 });
344 }
345 };
346 if (ImGui::TableNextColumn()) { // true-image
347 image(icon.on, "##on");
348 }
349 if (ImGui::TableNextColumn()) { // false-image
350 image(icon.off, "##off");
351 }
352 if (ImGui::TableNextColumn()) { // expression
353 ImGui::SetNextItemWidth(-FLT_MIN);
354 bool valid = manager.getInterpreter().validExpression(icon.expr.getString());
355 im::StyleColor(!valid, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
356 auto expr = std::string(icon.expr.getString());
357 if (ImGui::InputText("##expr", &expr)) {
358 icon.expr = expr;
359 iconInfoDirty = true;
360 }
361 });
362 }
363 });
364 if (int row = cmd.first; row != -1) {
365 switch (cmd.second) {
366 case MOVE_FRONT:
367 assert(row >= 1);
368 std::rotate(&iconInfo[0], &iconInfo[row], &iconInfo[row + 1]);
369 break;
370 case MOVE_FWD:
371 assert(row >= 1);
372 std::swap(iconInfo[row], iconInfo[row - 1]);
373 break;
374 case MOVE_BWD:
375 assert(row < narrow<int>(iconInfo.size() - 1));
376 std::swap(iconInfo[row], iconInfo[row + 1]);
377 break;
378 case MOVE_BACK:
379 assert(row < narrow<int>(iconInfo.size() - 1));
380 std::rotate(&iconInfo[row], &iconInfo[row + 1], &iconInfo[lastRow + 1]);
381 break;
382 case INSERT:
383 iconInfo.emplace(iconInfo.begin() + row, TclObject("true"), "", "", true);
384 break;
385 case DELETE:
386 iconInfo.erase(iconInfo.begin() + row);
387 break;
388 }
389 iconInfoDirty = true;
390 }
391 });
392
393 if (ImGui::Button("Restore default")) {
394 setDefaultIcons();
395 }
396 });
397 });
398}
399
400} // 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:459
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:425
bool TreeNode(const char *label, ImGuiTreeNodeFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:306
void StyleColor(bool active, Args &&...args)
Definition ImGuiCpp.hh:175
void Disabled(bool b, std::invocable<> auto next)
Definition ImGuiCpp.hh:510
void Indent(float indent_w, std::invocable<> auto next)
Definition ImGuiCpp.hh:224
void ID_for_range(int 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)