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 const 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.y + style.ItemSpacing.y;
269 width = std::max(width, boxSize.x);
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 | ImGuiWindowFlags_NoFocusOnAppearing;
282 ImGui::SetNextWindowViewport(mainViewPort->ID);
283 ImGui::SetNextWindowPos(gl::vec2(mainViewPort->WorkPos) + gl::vec2(style.ItemSpacing));
284 ImGui::SetNextWindowSize({width, y});
285 im::Window("OSD messages", nullptr, flags, [&]{
286 auto* drawList = ImGui::GetWindowDrawList();
287 gl::vec2 windowPos = ImGui::GetWindowPos();
288 for (const auto& [message, boxSize, yPos, textCol, bgCol] : drawInfo) {
289 gl::vec2 pos = windowPos + gl::vec2{0.0f, yPos};
290 drawList->AddRectFilled(pos, pos + boxSize, bgCol);
291 drawList->AddText(pos + offset, textCol, message.data(), message.data() + message.size());
292 }
293 });
294}
295
296void ImGuiMessages::paintLog()
297{
298 if (logWindow.do_raise) {
299 assert(logWindow.open);
300 ImGui::SetNextWindowFocus();
301 }
302 ImGui::SetNextWindowSize(gl::vec2{40, 14} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
303 im::Window("Message Log", logWindow, ImGuiWindowFlags_NoFocusOnAppearing, [&]{
304 const auto& style = ImGui::GetStyle();
305 auto buttonHeight = ImGui::GetFontSize() + 2.0f * style.FramePadding.y + style.ItemSpacing.y;
306 im::Child("messages", {0.0f, -buttonHeight}, ImGuiChildFlags_Border, ImGuiWindowFlags_HorizontalScrollbar, [&]{
307 printMessages(allMessages, [&](std::string_view prefix, std::string_view message) {
308 if (filterLog.empty()) return true;
309 auto full = tmpStrCat(prefix, message);
310 return ranges::all_of(StringOp::split_view<StringOp::EmptyParts::REMOVE>(filterLog, ' '),
311 [&](auto part) { return StringOp::containsCaseInsensitive(full, part); });
312 });
313 });
314 if (ImGui::Button("Clear")) {
315 allMessages.clear();
316 }
317 simpleToolTip("Remove all log messages");
318 ImGui::SameLine(0.0f, 30.0f);
319 ImGui::TextUnformatted(ICON_IGFD_SEARCH);
320 ImGui::SameLine();
321 auto size = ImGui::CalcTextSize("Configure..."sv).x + 30.0f + style.WindowPadding.x;
322 ImGui::SetNextItemWidth(-size);
323 ImGui::InputTextWithHint("##filter", "enter search terms", &filterLog);
324 ImGui::SameLine(0.0f, 30.0f);
325 if (ImGui::SmallButton("Configure...")) {
327 }
328 });
329}
330
331void ImGuiMessages::paintConfigure()
332{
333 ImGui::SetNextWindowSize(gl::vec2{24, 26} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
334 im::Window("Configure messages", configureWindow, [&]{
335 ImGui::TextUnformatted("When a message is emitted"sv);
336
337 auto size = ImGui::CalcTextSize("Warning"sv).x;
338 im::Table("table", 4, ImGuiTableFlags_SizingFixedFit, [&]{
339 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_None, 0);
340 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size);
341 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size);
342 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, size);
343
344 if (ImGui::TableNextColumn()) { /* nothing */ }
345 if (ImGui::TableNextColumn()) ImGui::TextUnformatted("Error"sv);
346 if (ImGui::TableNextColumn()) ImGui::TextUnformatted("Warning"sv);
347 if (ImGui::TableNextColumn()) ImGui::TextUnformatted("Info"sv);
348
349 if (ImGui::TableNextColumn()) {
350 ImGui::TextUnformatted("Show popup"sv);
351 }
352 ImGui::TableNextRow();
353 if (ImGui::TableNextColumn()) {
354 im::Indent([]{ ImGui::TextUnformatted("modal"sv); });
355 }
357 if (ImGui::TableNextColumn()) {
358 ImGui::RadioButton(tmpStrCat("##modal" , level).c_str(), &popupAction[level], MODAL_POPUP);
359 }
360 }
361 if (ImGui::TableNextColumn()) {
362 im::Indent([]{ ImGui::TextUnformatted("non-modal"sv); });
363 }
365 if (ImGui::TableNextColumn()) {
366 ImGui::RadioButton(tmpStrCat("##popup" , level).c_str(), &popupAction[level], POPUP);
367 }
368 }
369 if (ImGui::TableNextColumn()) {
370 im::Indent([]{ ImGui::TextUnformatted("don't show"sv); });
371 }
373 if (ImGui::TableNextColumn()) {
374 ImGui::RadioButton(tmpStrCat("##noPopup" , level).c_str(), &popupAction[level], NO_POPUP);
375 }
376 }
377
378 if (ImGui::TableNextColumn()) {
379 ImGui::TextUnformatted("Log window"sv);
380 }
381 ImGui::TableNextRow();
382 if (ImGui::TableNextColumn()) {
383 im::Indent([]{ ImGui::TextUnformatted("open and focus"sv); });
384 }
386 if (ImGui::TableNextColumn()) {
387 ImGui::RadioButton(tmpStrCat("##focus" , level).c_str(), &openLogAction[level], OPEN_LOG_FOCUS);
388 }
389 }
390 if (ImGui::TableNextColumn()) {
391 im::Indent([]{ ImGui::TextUnformatted("open without focus"sv); });
392 }
394 if (ImGui::TableNextColumn()) {
395 ImGui::RadioButton(tmpStrCat("##log" , level).c_str(), &openLogAction[level], OPEN_LOG);
396 }
397 }
398 if (ImGui::TableNextColumn()) {
399 im::Indent([]{ ImGui::TextUnformatted("don't open"sv); });
400 }
402 if (ImGui::TableNextColumn()) {
403 ImGui::RadioButton(tmpStrCat("##nolog" , level).c_str(), &openLogAction[level], NO_OPEN_LOG);
404 }
405 }
406
407 if (ImGui::TableNextColumn()) {
408 ImGui::TextUnformatted("On-screen message"sv);
409 }
410 ImGui::TableNextRow();
411 if (ImGui::TableNextColumn()) {
412 im::Indent([]{ ImGui::TextUnformatted("show"sv); });
413 }
415 if (ImGui::TableNextColumn()) {
416 ImGui::RadioButton(tmpStrCat("##osd" , level).c_str(), &osdAction[level], SHOW_OSD);
417 }
418 }
419 if (ImGui::TableNextColumn()) {
420 im::Indent([]{ ImGui::TextUnformatted("don't show"sv); });
421 }
423 if (ImGui::TableNextColumn()) {
424 ImGui::RadioButton(tmpStrCat("##no-osd" , level).c_str(), &osdAction[level], NO_OSD);
425 }
426 }
427 if (ImGui::TableNextColumn()) {
428 im::Indent([]{ ImGui::TextUnformatted("fade-out (seconds)"sv); });
429 }
431 if (ImGui::TableNextColumn()) {
432 float& d = colorSequence[level][2].start;
433 if (ImGui::InputFloat(tmpStrCat("##dur", level).c_str(), &d, 0.0f, 0.0f, "%.0f", ImGuiInputTextFlags_CharsDecimal)) {
434 d = std::clamp(d, 1.0f, 99.0f);
435 }
436 }
437 }
438 });
439 });
440}
441
442void ImGuiMessages::log(CliComm::LogLevel level, std::string_view text, float fraction)
443{
444 if (level == CliComm::PROGRESS) {
445 progressMessage = text;
446 progressFraction = fraction;
447 if (progressFraction < 1.0f) doOpenProgress = true;
448 return;
449 }
450
451 Message message{level, std::string(text)};
452
453 if (popupAction[level] == MODAL_POPUP) {
454 if (modalMessages.full()) modalMessages.pop_back();
455 modalMessages.push_front(message);
456 doOpenModal = true;
457 } else if (popupAction[level] == POPUP) {
458 if (popupMessages.full()) popupMessages.pop_back();
459 popupMessages.push_front(message);
460 doOpenPopup = popupMessages.size();
461 }
462
463 if (openLogAction[level] == OPEN_LOG) {
464 logWindow.open = true;
465 } else if (openLogAction[level] == OPEN_LOG_FOCUS) {
467 }
468
469 if (osdAction[level] == SHOW_OSD) {
470 osdMessages.emplace_back(std::string(text), 0.0f, level);
471 }
472
473 if (allMessages.full()) allMessages.pop_back();
474 allMessages.push_front(std::move(message));
475}
476
477} // 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)
TemporaryString tmpStrCat(Ts &&... ts)
Definition strCat.hh:742
constexpr auto xrange(T e)
Definition xrange.hh:132