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