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