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