openMSX
ImGuiMessages.cc
Go to the documentation of this file.
1#include "ImGuiMessages.hh"
2
3#include "CustomFont.h"
4#include "ImGuiCpp.hh"
5#include "ImGuiManager.hh"
6#include "ImGuiUtils.hh"
7
8#include "GlobalCliComm.hh"
9#include "PixelOperations.hh"
10#include "Reactor.hh"
11
12#include "stl.hh"
13
14#include <imgui_stdlib.h>
15#include <imgui.h>
16
17#include <cassert>
18#include <concepts>
19
20namespace openmsx {
21
22using namespace std::literals;
23
25 : ImGuiPart(manager_)
26 , modalMessages(10)
27 , popupMessages(10)
28 , allMessages(10)
29{
30 popupAction[CliComm::INFO] = NO_POPUP;
31 popupAction[CliComm::WARNING] = POPUP;
32 popupAction[CliComm::LOGLEVEL_ERROR] = MODAL_POPUP;
33 popupAction[CliComm::PROGRESS] = NO_POPUP;
34 openLogAction[CliComm::INFO] = NO_OPEN_LOG;
35 openLogAction[CliComm::WARNING] = NO_OPEN_LOG;
36 openLogAction[CliComm::LOGLEVEL_ERROR] = NO_OPEN_LOG;
37 openLogAction[CliComm::PROGRESS] = NO_OPEN_LOG;
38 osdAction[CliComm::INFO] = SHOW_OSD;
39 osdAction[CliComm::WARNING] = NO_OSD;
40 osdAction[CliComm::LOGLEVEL_ERROR] = NO_OSD;
41 osdAction[CliComm::PROGRESS] = NO_OSD;
42
43 auto& reactor = manager.getReactor();
44 auto& cliComm = reactor.getGlobalCliComm();
45 listenerHandle = cliComm.addListener(std::make_unique<Listener>(*this));
46}
47
49{
50 auto& reactor = manager.getReactor();
51 auto& cliComm = reactor.getGlobalCliComm();
52 cliComm.removeListener(*listenerHandle);
53}
54
55void ImGuiMessages::save(ImGuiTextBuffer& buf)
56{
57 savePersistent(buf, *this, persistentElements);
58 buf.appendf("popupActions=[%d %d %d]\n",
59 popupAction[CliComm::LOGLEVEL_ERROR],
60 popupAction[CliComm::WARNING],
61 popupAction[CliComm::INFO]);
62 buf.appendf("openLogActions=[%d %d %d]\n",
63 openLogAction[CliComm::LOGLEVEL_ERROR],
64 openLogAction[CliComm::WARNING],
65 openLogAction[CliComm::INFO]);
66 buf.appendf("osdActions=[%d %d %d]\n",
67 osdAction[CliComm::LOGLEVEL_ERROR],
68 osdAction[CliComm::WARNING],
69 osdAction[CliComm::INFO]);
70 buf.appendf("fadeOutDuration=[%f %f %f]\n",
71 double(colorSequence[CliComm::LOGLEVEL_ERROR][2].start), // note: cast to double only to silence warnings
72 double(colorSequence[CliComm::WARNING][2].start),
73 double(colorSequence[CliComm::INFO][2].start));
74}
75
76void ImGuiMessages::loadLine(std::string_view name, zstring_view value)
77{
78 if (loadOnePersistent(name, value, *this, persistentElements)) {
79 // already handled
80 } else if (name == "popupActions"sv) {
81 std::array<int, 3> a = {};
82 if (sscanf(value.c_str(), "[%d %d %d]", &a[0], &a[1], &a[2]) == 3) {
83 popupAction[CliComm::LOGLEVEL_ERROR] = PopupAction(a[0]);
84 popupAction[CliComm::WARNING] = PopupAction(a[1]);
85 popupAction[CliComm::INFO] = PopupAction(a[2]);
86 }
87 } else if (name == "openLogActions"sv) {
88 std::array<int, 3> a = {};
89 if (sscanf(value.c_str(), "[%d %d %d]", &a[0], &a[1], &a[2]) == 3) {
90 openLogAction[CliComm::LOGLEVEL_ERROR] = OpenLogAction(a[0]);
91 openLogAction[CliComm::WARNING] = OpenLogAction(a[1]);
92 openLogAction[CliComm::INFO] = OpenLogAction(a[2]);
93 }
94 } else if (name == "osdActions"sv) {
95 std::array<int, 3> a = {};
96 if (sscanf(value.c_str(), "[%d %d %d]", &a[0], &a[1], &a[2]) == 3) {
97 osdAction[CliComm::LOGLEVEL_ERROR] = OsdAction(a[0]);
98 osdAction[CliComm::WARNING] = OsdAction(a[1]);
99 osdAction[CliComm::INFO] = OsdAction(a[2]);
100 }
101 } else if (name == "fadeOutDuration"sv) {
102 std::array<float, 3> a = {};
103 if (sscanf(value.c_str(), "[%f %f %f]", &a[0], &a[1], &a[2]) == 3) {
104 colorSequence[CliComm::LOGLEVEL_ERROR][2].start = a[0];
105 colorSequence[CliComm::WARNING][2].start = a[1];
106 colorSequence[CliComm::INFO][2].start = a[2];
107 }
108 }
109}
110
112{
113 paintModal();
114 paintPopup();
115 paintProgress();
116 paintOSD();
117 if (logWindow.open) paintLog();
118 if (configureWindow.open) paintConfigure();
119}
120
121template<std::predicate<std::string_view, std::string_view> Filter = always_true>
122static void printMessages(const circular_buffer<ImGuiMessages::Message>& messages, Filter filter = {})
123{
124 im::TextWrapPos(ImGui::GetFontSize() * 50.0f, [&]{
125 for (const auto& message : messages) {
126 auto [color, prefix_] = [&]() -> std::pair<ImU32, std::string_view> {
127 switch (message.level) {
128 case CliComm::LOGLEVEL_ERROR: return {getColor(imColor::ERROR), "Error:"};
129 case CliComm::WARNING: return {getColor(imColor::WARNING), "Warning:"};
130 case CliComm::INFO: return {getColor(imColor::TEXT), "Info:"};
131 default: assert(false); return {getColor(imColor::TEXT), "Unknown:"};
132 }
133 }();
134 auto prefix = prefix_; // clang workaround
135 if (std::invoke(filter, prefix, message.text)) {
136 im::StyleColor(ImGuiCol_Text, color, [&]{
138 ImGui::SameLine();
139 ImGui::TextUnformatted(message.text);
140 });
141 }
142 }
143 });
144}
145
146bool ImGuiMessages::paintButtons()
147{
148 ImGui::SetCursorPosX(40.0f);
149 bool close = ImGui::Button("Ok");
150 ImGui::SameLine(0.0f, 30.0f);
151 if (ImGui::SmallButton("Configure...")) {
152 close = true;
154 }
155 return close;
156}
157
158void ImGuiMessages::paintModal()
159{
160 if (doOpenModal) {
161 doOpenModal = false;
162 ImGui::OpenPopup("Message");
163 }
164
165 bool open = true;
166 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, {0.5f, 0.5f});
167 im::PopupModal("Message", &open, ImGuiWindowFlags_AlwaysAutoResize, [&]{
168 printMessages(modalMessages);
169 bool close = paintButtons();
170 if (!open || close) {
171 modalMessages.clear();
172 ImGui::CloseCurrentPopup();
173 }
174 });
175}
176
177void ImGuiMessages::paintPopup()
178{
179 // TODO automatically fade-out and close
180 if (doOpenPopup) {
181 // on first open, clear old messages
182 if (!ImGui::IsPopupOpen("popup-message")) {
183 while (popupMessages.size() > doOpenPopup) {
184 popupMessages.pop_back();
185 }
186 }
187 doOpenPopup = 0;
188 ImGui::OpenPopup("popup-message");
189 }
190
191 im::Popup("popup-message", [&]{
192 printMessages(popupMessages);
193 bool close = paintButtons();
194 if (close) ImGui::CloseCurrentPopup();
195 });
196}
197
198void ImGuiMessages::paintProgress()
199{
200 if (doOpenProgress) {
201 doOpenProgress = false;
202 ImGui::OpenPopup("popup-progress");
203 }
204
205 im::Popup("popup-progress", [&]{
206 if (progressFraction >= 1.0f) {
207 ImGui::CloseCurrentPopup();
208 } else {
209 ImGui::TextUnformatted(progressMessage);
210 if (progressFraction >= 0.0f) {
211 ImGui::ProgressBar(progressFraction);
212 } else {
213 // unknown fraction, animate progress bar, no label
214 progressTime = fmodf(progressTime + ImGui::GetIO().DeltaTime, 2.0f);
215 float fraction = (progressTime < 1.0f) ? progressTime : (2.0f - progressTime);
216 ImGui::ProgressBar(fraction, {}, "");
217 }
218 }
219 });
220}
221
222void ImGuiMessages::paintOSD()
223{
224 auto getColors = [&](const ColorSequence& seq, float t) -> std::optional<Colors> {
225 for (auto i : xrange(seq.size() - 1)) { // TODO c++23 std::views::pairwise
226 const auto& step0 = seq[i + 0];
227 const auto& step1 = seq[i + 1];
228 if (t < step1.start) {
229 PixelOperations p;
230 auto x = int(256.0f * (t / step1.start));
231 auto tCol = p.lerp(step0.colors.text, step1.colors.text, x);
232 auto bCol = p.lerp(step0.colors.background, step1.colors.background, x);
233 return Colors{tCol, bCol};
234 }
235 t -= step1.start;
236 }
237 return {};
238 };
239
240 const auto& style = ImGui::GetStyle();
241 gl::vec2 offset = style.FramePadding;
242 auto* mainViewPort = ImGui::GetMainViewport();
243
244 struct DrawInfo {
245 // clang workaround:
246 DrawInfo(const std::string m, gl::vec2 s, float y, uint32_t t, uint32_t b)
247 : message(m), boxSize(s), yPos(y), textCol(t), bgCol(b) {}
248
249 std::string message;
250 gl::vec2 boxSize;
251 float yPos;
252 uint32_t textCol, bgCol;
253 };
254 std::vector<DrawInfo> drawInfo;
255 float y = 0.0f;
256 float width = 0.0f;
257 //float wrapWidth = mainViewPort->WorkSize.x - 2.0f * offset[0]; // TODO wrap ?
258 float delta = ImGui::GetIO().DeltaTime;
259 std::erase_if(osdMessages, [&](OsdMessage& message) {
260 message.time += delta;
261 auto colors = getColors(colorSequence[message.level], message.time);
262 if (!colors) return true; // remove message
263
264 auto& text = message.text;
265 gl::vec2 textSize = ImGui::CalcTextSize(text.data(), text.data() + text.size());
266 gl::vec2 boxSize = textSize + 2.0f * offset;
267 drawInfo.emplace_back(text, boxSize, y, colors->text, colors->background);
268 y += boxSize[1] + style.ItemSpacing.y;
269 width = std::max(width, boxSize[0]);
270 return false; // keep message
271 });
272 if (drawInfo.empty()) return;
273
274 int flags = ImGuiWindowFlags_NoMove
275 | ImGuiWindowFlags_NoBackground
276 | ImGuiWindowFlags_NoSavedSettings
277 | ImGuiWindowFlags_NoDocking
278 | ImGuiWindowFlags_NoNav
279 | ImGuiWindowFlags_NoDecoration
280 | ImGuiWindowFlags_NoInputs;
281 ImGui::SetNextWindowViewport(mainViewPort->ID);
282 ImGui::SetNextWindowPos(gl::vec2(mainViewPort->WorkPos) + gl::vec2(style.ItemSpacing));
283 ImGui::SetNextWindowSize({width, y});
284 im::Window("OSD messages", nullptr, flags, [&]{
285 auto* drawList = ImGui::GetWindowDrawList();
286 gl::vec2 windowPos = ImGui::GetWindowPos();
287 for (const auto& [message, boxSize, yPos, textCol, bgCol] : drawInfo) {
288 gl::vec2 pos = windowPos + gl::vec2{0.0f, yPos};
289 drawList->AddRectFilled(pos, pos + boxSize, bgCol);
290 drawList->AddText(pos + offset, textCol, message.data(), message.data() + message.size());
291 }
292 });
293}
294
295void ImGuiMessages::paintLog()
296{
297 if (logWindow.do_raise) {
298 assert(logWindow.open);
299 ImGui::SetNextWindowFocus();
300 }
301 ImGui::SetNextWindowSize(gl::vec2{40, 14} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
302 im::Window("Message Log", logWindow, ImGuiWindowFlags_NoFocusOnAppearing, [&]{
303 const auto& style = ImGui::GetStyle();
304 auto buttonHeight = ImGui::GetFontSize() + 2.0f * style.FramePadding.y + style.ItemSpacing.y;
305 im::Child("messages", {0.0f, -buttonHeight}, ImGuiChildFlags_Border, ImGuiWindowFlags_HorizontalScrollbar, [&]{
306 printMessages(allMessages, [&](std::string_view prefix, std::string_view message) {
307 if (filterLog.empty()) return true;
308 auto full = tmpStrCat(prefix, message);
309 return ranges::all_of(StringOp::split_view<StringOp::REMOVE_EMPTY_PARTS>(filterLog, ' '),
310 [&](auto part) { return StringOp::containsCaseInsensitive(full, part); });
311 });
312 });
313 if (ImGui::Button("Clear")) {
314 allMessages.clear();
315 }
316 simpleToolTip("Remove all log messages");
317 ImGui::SameLine(0.0f, 30.0f);
318 ImGui::TextUnformatted(ICON_IGFD_SEARCH);
319 ImGui::SameLine();
320 auto size = ImGui::CalcTextSize("Configure..."sv).x + 30.0f + style.WindowPadding.x;
321 ImGui::SetNextItemWidth(-size);
322 ImGui::InputTextWithHint("##filter", "enter search terms", &filterLog);
323 ImGui::SameLine(0.0f, 30.0f);
324 if (ImGui::SmallButton("Configure...")) {
326 }
327 });
328}
329
330void ImGuiMessages::paintConfigure()
331{
332 ImGui::SetNextWindowSize(gl::vec2{29, 23} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
333 im::Window("Configure messages", configureWindow, [&]{
334 ImGui::TextUnformatted("When a message is emitted"sv);
335 im::Indent([&]{
336 auto size1 = ImGui::CalcTextSize("Open log window without focus"sv).x;
337 auto size2 = ImGui::CalcTextSize("Warning"sv).x;
338 im::Table("table", 4, ImGuiTableFlags_SizingFixedFit, [&]{
339 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size1);
340 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size2);
341 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size2);
342 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size2);
343
344 //ImGui::TableHeadersRow(); // don't want a colored header row
345 if (ImGui::TableNextColumn()) {}
346 if (ImGui::TableNextColumn()) ImGui::TextUnformatted("Error"sv);
347 if (ImGui::TableNextColumn()) ImGui::TextUnformatted("Warning"sv);
348 if (ImGui::TableNextColumn()) ImGui::TextUnformatted("Info"sv);
349
350 if (ImGui::TableNextColumn()) {
351 ImGui::TextUnformatted("Show modal popup"sv);
352 }
354 if (ImGui::TableNextColumn()) {
355 ImGui::RadioButton(tmpStrCat("##modal" , level).c_str(), &popupAction[level], MODAL_POPUP);
356 }
357 }
358
359 if (ImGui::TableNextColumn()) {
360 ImGui::TextUnformatted("Show non-modal popup"sv);
361 }
363 if (ImGui::TableNextColumn()) {
364 ImGui::RadioButton(tmpStrCat("##popup" , level).c_str(), &popupAction[level], POPUP);
365 }
366 }
367
368 if (ImGui::TableNextColumn()) {
369 ImGui::TextUnformatted("Don't show any popup"sv);
370 }
372 if (ImGui::TableNextColumn()) {
373 ImGui::RadioButton(tmpStrCat("##nopopup" , level).c_str(), &popupAction[level], NO_POPUP);
374 }
375 }
376 });
377
378 ImGui::Separator();
379
380 im::Table("table", 4, ImGuiTableFlags_SizingFixedFit, [&]{
381 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size1);
382 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size2);
383 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size2);
384 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size2);
385
386 if (ImGui::TableNextColumn()) {
387 ImGui::TextUnformatted("Open log window and focus"sv);
388 }
390 if (ImGui::TableNextColumn()) {
391 ImGui::RadioButton(tmpStrCat("##focus" , level).c_str(), &openLogAction[level], OPEN_LOG_FOCUS);
392 }
393 }
394
395 if (ImGui::TableNextColumn()) {
396 ImGui::TextUnformatted("Open log window without focus"sv);
397 }
399 if (ImGui::TableNextColumn()) {
400 ImGui::RadioButton(tmpStrCat("##log" , level).c_str(), &openLogAction[level], OPEN_LOG);
401 }
402 }
403
404 if (ImGui::TableNextColumn()) {
405 ImGui::TextUnformatted("Don't open log window"sv);
406 }
408 if (ImGui::TableNextColumn()) {
409 ImGui::RadioButton(tmpStrCat("##nolog" , level).c_str(), &openLogAction[level], NO_OPEN_LOG);
410 }
411 }
412 });
413
414 ImGui::Separator();
415
416 im::Table("table", 4, ImGuiTableFlags_SizingFixedFit, [&]{
417 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size1);
418 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size2);
419 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size2);
420 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size2);
421
422 if (ImGui::TableNextColumn()) {
423 ImGui::TextUnformatted("Show on-screen message"sv);
424 }
426 if (ImGui::TableNextColumn()) {
427 ImGui::RadioButton(tmpStrCat("##osd" , level).c_str(), &osdAction[level], SHOW_OSD);
428 }
429 }
430
431 if (ImGui::TableNextColumn()) {
432 ImGui::TextUnformatted("Don't show on-screen message"sv);
433 }
435 if (ImGui::TableNextColumn()) {
436 ImGui::RadioButton(tmpStrCat("##no-osd" , level).c_str(), &osdAction[level], NO_OSD);
437 }
438 }
439
440 if (ImGui::TableNextColumn()) {
441 ImGui::TextUnformatted("Fade-out duration (seconds)"sv);
442 }
444 if (ImGui::TableNextColumn()) {
445 float& d = colorSequence[level][2].start;
446 if (ImGui::InputFloat(tmpStrCat("##dur", level).c_str(), &d, 0.0f, 0.0f, "%.0f", ImGuiInputTextFlags_CharsDecimal)) {
447 d = std::clamp(d, 1.0f, 99.0f);
448 }
449 }
450 }
451 });
452 });
453 });
454}
455
456void ImGuiMessages::log(CliComm::LogLevel level, std::string_view text, float fraction)
457{
458 if (level == CliComm::PROGRESS) {
459 progressMessage = text;
460 progressFraction = fraction;
461 if (progressFraction < 1.0f) doOpenProgress = true;
462 return;
463 }
464
465 Message message{level, std::string(text)};
466
467 if (popupAction[level] == MODAL_POPUP) {
468 if (modalMessages.full()) modalMessages.pop_back();
469 modalMessages.push_front(message);
470 doOpenModal = true;
471 } else if (popupAction[level] == POPUP) {
472 if (popupMessages.full()) popupMessages.pop_back();
473 popupMessages.push_front(message);
474 doOpenPopup = popupMessages.size();
475 }
476
477 if (openLogAction[level] == OPEN_LOG) {
478 logWindow.open = true;
479 } else if (openLogAction[level] == OPEN_LOG_FOCUS) {
481 }
482
483 if (osdAction[level] == SHOW_OSD) {
484 osdMessages.emplace_back(std::string(text), 0.0f, level);
485 }
486
487 if (allMessages.full()) allMessages.pop_back();
488 allMessages.push_front(std::move(message));
489}
490
491} // namespace openmsx
TclObject t
Circular buffer class, based on boost::circular_buffer/.
std::unique_ptr< CliListener > removeListener(CliListener &listener)
void loadLine(std::string_view name, zstring_view value) override
void save(ImGuiTextBuffer &buf) override
void paint(MSXMotherBoard *motherBoard) override
im::WindowStatus logWindow
ImGuiMessages(ImGuiManager &manager_)
im::WindowStatus configureWindow
ImGuiManager & manager
Definition ImGuiPart.hh:30
GlobalCliComm & getGlobalCliComm()
Definition Reactor.hh:87
Like std::string_view, but with the extra guarantee that it refers to a zero-terminated string.
constexpr const char * c_str() const
auto CalcTextSize(std::string_view str)
Definition ImGuiUtils.hh:37
void TextUnformatted(const std::string &str)
Definition ImGuiUtils.hh:24
bool containsCaseInsensitive(std::string_view haystack, std::string_view needle)
Definition StringOp.hh:181
void Table(const char *str_id, int column, ImGuiTableFlags flags, const ImVec2 &outer_size, float inner_width, std::invocable<> auto next)
Definition ImGuiCpp.hh:473
void Window(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:63
void Child(const char *str_id, const ImVec2 &size, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:104
void PopupModal(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:422
void TextWrapPos(float wrap_local_pos_x, std::invocable<> auto next)
Definition ImGuiCpp.hh:226
void StyleColor(ImGuiCol idx1, ImVec4 col1, ImGuiCol idx2, ImVec4 col2, std::invocable<> auto next)
Definition ImGuiCpp.hh:156
void Indent(float indent_w, std::invocable<> auto next)
Definition ImGuiCpp.hh:238
void Popup(const char *str_id, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:409
This file implemented 3 utility functions:
Definition Autofire.cc:9
bool loadOnePersistent(std::string_view name, zstring_view value, C &c, const std::tuple< Elements... > &tup)
void simpleToolTip(std::string_view desc)
Definition ImGuiUtils.hh:66
void savePersistent(ImGuiTextBuffer &buf, C &c, const std::tuple< Elements... > &tup)
ImU32 getColor(imColor col)
bool all_of(InputRange &&range, UnaryPredicate pred)
Definition ranges.hh:186
size_t size(std::string_view utf8)
TemporaryString tmpStrCat(Ts &&... ts)
Definition strCat.hh:742
constexpr auto xrange(T e)
Definition xrange.hh:132