OXYGENHUB
◆ main openmsx / src / imgui / ImGuiWatchExpr.cc
9.7 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "ImGuiWatchExpr.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiManager.hh"
5
#include "ImGuiUtils.hh"
6
7
#include "CommandException.hh"
8
#include "SymbolManager.hh"
9
10
#include "narrow.hh"
11
12
#include <imgui.h>
13
#include <imgui_stdlib.h>
14
15
#include <cassert>
16
17
namespace openmsx {
18
19
using namespace std::literals;
20
21
ImGuiWatchExpr::ImGuiWatchExpr(ImGuiManager& manager_)
22
	: ImGuiPart(manager_)
23
	, symbolManager(manager.getReactor().getSymbolManager())
24
{
25
}
26
27
void ImGuiWatchExpr::save(ImGuiTextBuffer& buf)
28
{
29
	savePersistent(buf, *this, persistentElements);
30
	for (const auto& watch : watches) {
31
		auto out = makeTclList(watch.description, watch.exprStr, watch.format);
32
		buf.appendf("watch=%s\n", out.getString().c_str());
33
	}
34
}
35
36
void ImGuiWatchExpr::loadStart()
37
{
38
	watches.clear();
39
}
40
41
void ImGuiWatchExpr::loadLine(std::string_view name, zstring_view value)
42
{
43
	if (loadOnePersistent(name, value, *this, persistentElements)) {
44
		// already handled
45
	} else if (name == "watch"sv) {
46
		TclObject in(value);
47
		if (in.size() == 3) {
48
			WatchExpr result;
49
			result.description = in.getListIndexUnchecked(0).getString();
50
			result.exprStr     = in.getListIndexUnchecked(1).getString();
51
			result.format      = in.getListIndexUnchecked(2);
52
			watches.push_back(std::move(result));
53
		}
54
	}
55
}
56
57
void ImGuiWatchExpr::paint(MSXMotherBoard* /*motherBoard*/)
58
{
59
	if (!show) return;
60
61
	ImGui::SetNextWindowSize(gl::vec2{35, 15} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
62
	im::Window("Watch expression", &show, [&]{
63
		const auto& style = ImGui::GetStyle();
64
		auto width = style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize("Examples").x;
65
		im::Child("child", {-width, 0}, [&] {
66
			int flags = ImGuiTableFlags_Resizable
67
				| ImGuiTableFlags_Reorderable
68
				| ImGuiTableFlags_Hideable
69
				| ImGuiTableFlags_Sortable
70
				| ImGuiTableFlags_RowBg
71
				| ImGuiTableFlags_BordersV
72
				| ImGuiTableFlags_BordersOuter
73
				| ImGuiTableFlags_SizingStretchProp
74
				| ImGuiTableFlags_SortTristate
75
				| ImGuiTableFlags_ScrollY;
76
			im::Table("table", 5, flags, [&]{
77
				ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible
78
				ImGui::TableSetupColumn("id", ImGuiTableColumnFlags_DefaultHide);
79
				ImGui::TableSetupColumn("description");
80
				ImGui::TableSetupColumn("expression");
81
				ImGui::TableSetupColumn("format");
82
				ImGui::TableSetupColumn("result", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_NoHide);
83
				ImGui::TableHeadersRow();
84
				checkSort();
85
86
				im::ID_for_range(watches.size(), [&](int row) {
87
					drawRow(row);
88
				});
89
			});
90
		});
91
		ImGui::SameLine();
92
		im::Group([&] {
93
			if (ImGui::Button("Add")) {
94
				selectedRow = narrow<int>(watches.size()); // select created row
95
				watches.emplace_back();
96
			}
97
			im::Disabled(selectedRow < 0 || selectedRow >= narrow<int>(watches.size()), [&]{
98
				if (ImGui::Button("Remove")) {
99
					watches.erase(watches.begin() + selectedRow);
100
					if (selectedRow == narrow<int>(watches.size())) {
101
						selectedRow = -1;
102
					}
103
				}
104
			});
105
			ImGui::Dummy({0, 20});
106
			if (ImGui::SmallButton("Examples")) {
107
				watches.emplace_back(
108
					"peek at fixed address",
109
					"[peek 0xfcaf]",
110
					TclObject("screen=%d"));
111
				watches.emplace_back(
112
					"VDP command executing",
113
					"[debug read \"VDP status regs\" 2] & 1",
114
					TclObject());
115
				watches.emplace_back(
116
					"PSG enable-channel status",
117
					"[debug read \"PSG regs\" 7]",
118
					TclObject("%08b"));
119
				watches.emplace_back(
120
					"The following 2 require an appropriate symbol file",
121
					"",
122
					TclObject(""));
123
				watches.emplace_back(
124
					"value of 'myLabel'",
125
					"$sym(myLabel)",
126
					TclObject("0x%04x"));
127
				watches.emplace_back(
128
					"peek at symbolic address",
129
					"[peek16 $sym(numItems)]",
130
					TclObject("%d items"));
131
			}
132
			ImGui::Dummy({0, 0}); // want help marker on the next line
133
			HelpMarker(R"(The given (Tcl) expressions are continuously evaluated and displayed in a nicely formatted way.
134
Press the 'Examples' button to see some examples.
135
136
There are 4 columns:
137
* description: optional description for the expression
138
* expression: the actual Tcl expression, see below
139
* format: optional format specifier, see below
140
* result: this shows the result of evaluating 'expression'
141
142
Add a new entry via the 'Add' button, then fill in the appropriate fields.
143
To remove an entry, first select a row (e.g. click on the corresponding 'result' cell), then press the 'Remove' button.
144
145
You can sort the table by clicking the column headers.
146
You can reorder the columns by dragging the column headers.
147
You can hide columns via the right-click context menu on a column header. For example, once configured, you may want to hide the 'expression' and 'format' columns.
148
149
The expression can be any Tcl expression, some examples:
150
* [peek 0x1234]: monitor a byte at a specific address
151
* [reg SP] < 0xe000: check that stack hasn't grown too large
152
                     (below address 0xe000)
153
If you have debug-symbols loaded (via the 'Symbol manager'), you can use them like:
154
* [peek16 $sym(mySymbol)]: monitor 16-bit value at 'mySymbol'
155
There's a shorthand notation to peek 8-bit values:
156
* <integer> -> [peek <integer>]
157
* <symbol>  -> [peek $sym(<symbol>)]
158
159
In the format column you can optionally enter a format-specifier (for the  Tcl 'format' command). Some examples:
160
* 0x%04x: 4-digit hex with leading zeros and '0x' prefix
161
* %08b: 8-bit binary value
162
* %d items: decimal value followed by the string " items"
163
)");
164
		});
165
	});
166
}
167
168
static void tooWideToolTip(float available, zstring_view str)
169
{
170
	if (str.empty()) return;
171
	auto width = ImGui::CalcTextSize(str).x;
172
	width += 2.0f * ImGui::GetStyle().FramePadding.x;
173
	if (width >= available) {
174
		simpleToolTip(str);
175
	}
176
}
177
178
void ImGuiWatchExpr::refreshSymbols()
179
{
180
	// symbols changed, expression might have used those symbols
181
	for (auto& watch : watches) {
182
		watch.expression.reset(); // drop cache
183
	}
184
}
185
186
std::expected<TclObject, std::string> ImGuiWatchExpr::evalExpr(WatchExpr& watch, Interpreter& interp) const
187
{
188
	if (watch.exprStr.empty()) return {};
189
190
	if (!watch.expression) {
191
		if (auto addr = symbolManager.parseSymbolOrValue(watch.exprStr)) {
192
			// expression is a symbol or an integer -> rewrite
193
			watch.expression = TclObject(tmpStrCat("[peek ", *addr, ']'));
194
		} else {
195
			// keep original expression
196
			watch.expression = watch.exprStr;
197
		}
198
	}
199
	assert(watch.expression);
200
201
	try {
202
		return watch.expression->eval(interp);
203
	} catch (CommandException& e) {
204
		return std::unexpected(e.getMessage());
205
	}
206
}
207
208
void ImGuiWatchExpr::drawRow(int row)
209
{
210
	auto& interp = manager.getInterpreter();
211
	auto& watch = watches[row];
212
213
	// evaluate 'expression'
214
	auto exprVal = evalExpr(watch, interp);
215
216
	// format the result
217
	std::expected<TclObject, std::string> formatted;
218
	if (!watch.format.getString().empty()) {
219
		auto frmtCmd = makeTclList("format", watch.format, exprVal ? *exprVal : TclObject("0"));
220
		try {
221
			formatted = frmtCmd.executeCommand(interp);
222
		} catch (CommandException& e) {
223
			formatted = std::unexpected(e.getMessage());
224
		}
225
	} else {
226
		formatted = exprVal ? *exprVal : TclObject();
227
	}
228
229
	const auto& display = exprVal ? (formatted ? formatted->getString() : exprVal->getString())
230
	                              : exprVal.error();
231
232
	if (ImGui::TableNextColumn()) { // id
233
		ImGui::TextUnformatted(watch.getIdStr());
234
	}
235
	if (ImGui::TableNextColumn()) { // description
236
		auto pos = ImGui::GetCursorPos();
237
		const auto& style = ImGui::GetStyle();
238
		float rowHeight = 2.0f * style.FramePadding.y;
239
		rowHeight += ImGui::CalcTextSize(display).y;
240
		if (ImGui::Selectable("##selection", selectedRow == row,
241
				ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap,
242
				{0.0f, rowHeight})) {
243
			selectedRow = row;
244
		}
245
		ImGui::SetCursorPos(pos);
246
247
		auto avail = ImGui::GetContentRegionAvail().x;
248
		ImGui::SetNextItemWidth(-FLT_MIN);
249
		ImGui::InputText("##desc", &watch.description);
250
		tooWideToolTip(avail, watch.description);
251
	}
252
	if (ImGui::TableNextColumn()) { // expression
253
		im::StyleColor(!exprVal, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
254
			im::ScopedFont sf(manager.fontMono);
255
			auto avail = ImGui::GetContentRegionAvail().x;
256
			ImGui::SetNextItemWidth(-FLT_MIN);
257
			if (ImGui::InputText("##expr", &watch.exprStr)) {
258
				watch.expression.reset();
259
			}
260
			tooWideToolTip(avail, watch.exprStr);
261
		});
262
	}
263
	if (ImGui::TableNextColumn()) { // format
264
		im::StyleColor(!formatted, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
265
			im::ScopedFont sf(manager.fontMono);
266
			auto avail = ImGui::GetContentRegionAvail().x;
267
			ImGui::SetNextItemWidth(-FLT_MIN);
268
			auto str = std::string(watch.format.getString());
269
			if (ImGui::InputText("##format", &str)) {
270
				watch.format = str;
271
			}
272
			if (formatted) {
273
				tooWideToolTip(avail, str);
274
			} else {
275
				simpleToolTip(formatted.error());
276
			}
277
		});
278
	}
279
	if (ImGui::TableNextColumn()) { // result
280
		im::ScopedFont sf(manager.fontMono);
281
		auto avail = ImGui::GetContentRegionAvail().x;
282
		im::StyleColor(!exprVal, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
283
			ImGui::TextUnformatted(display);
284
			tooWideToolTip(avail, display);
285
		});
286
	}
287
}
288
289
void ImGuiWatchExpr::checkSort()
290
{
291
	auto* sortSpecs = ImGui::TableGetSortSpecs();
292
	if (!sortSpecs->SpecsDirty) return;
293
294
	sortSpecs->SpecsDirty = false;
295
	if (sortSpecs->SpecsCount == 0) return;
296
	assert(sortSpecs->SpecsCount == 1);
297
	assert(sortSpecs->Specs);
298
	assert(sortSpecs->Specs->SortOrder == 0);
299
300
	switch (sortSpecs->Specs->ColumnIndex) {
301
	case 0: // id
302
		sortUpDown_T(watches, sortSpecs, &WatchExpr::id);
303
		break;
304
	case 1: // description
305
		sortUpDown_String(watches, sortSpecs, &WatchExpr::description);
306
		break;
307
	case 2: // expression
308
		sortUpDown_String(watches, sortSpecs, [](const auto& item) { return item.exprStr; });
309
		break;
310
	case 3: // format
311
		sortUpDown_String(watches, sortSpecs, [](const auto& item) { return item.format.getString(); });
312
		break;
313
	default:
314
		UNREACHABLE;
315
	}
316
}
317
318
} // namespace openmsx