1 /*
2 	This file is part of Warzone 2100.
3 	Copyright (C) 2020-2021  Warzone 2100 Project
4 
5 	Warzone 2100 is free software; you can redistribute it and/or modify
6 	it under the terms of the GNU General Public License as published by
7 	the Free Software Foundation; either version 2 of the License, or
8 	(at your option) any later version.
9 
10 	Warzone 2100 is distributed in the hope that it will be useful,
11 	but WITHOUT ANY WARRANTY; without even the implied warranty of
12 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 	GNU General Public License for more details.
14 
15 	You should have received a copy of the GNU General Public License
16 	along with Warzone 2100; if not, write to the Free Software
17 	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 */
19 /**
20  * @file
21  * Functions for scrollable JSON table widget.
22  */
23 
24 #include "jsontable.h"
25 #include "widgbase.h"
26 #include "button.h"
27 #include "label.h"
28 #include "table.h"
29 #include "lib/framework/frame.h"
30 #include "lib/framework/wzapp.h"
31 #include "lib/framework/input.h"
32 #include "lib/framework/math_ext.h"
33 #include "lib/ivis_opengl/pieblitfunc.h"
34 
35 // MARK: - PathBarWidget
36 
make(const std::string & pathSeparator)37 std::shared_ptr<PathBarWidget> PathBarWidget::make(const std::string& pathSeparator)
38 {
39 	auto result = std::make_shared<PathBarWidget>(pathSeparator);
40 
41 	result->ellipsis = std::make_shared<W_LABEL>();
42 	result->attach(result->ellipsis);
43 	result->ellipsis->setFont(font_regular, WZCOL_FORM_TEXT);
44 	result->ellipsis->setString("...");
45 	result->ellipsis->setGeometry(0, 0, result->ellipsis->getMaxLineWidth(), iV_GetTextLineSize(font_regular));
46 	result->ellipsis->hide();
47 
48 	result->pathSeparator = std::make_shared<W_LABEL>();
49 	result->pathSeparator->setFont(font_regular, WZCOL_FORM_TEXT);
50 	result->pathSeparator->setString(WzString::fromUtf8(pathSeparator));
51 	result->pathSeparator->setGeometry(0, 0, result->pathSeparator->getMaxLineWidth(), iV_GetTextLineSize(font_regular));
52 	// NOTE: Do not attach the pathSeparator widget - it's used for manual drawing
53 
54 	return result;
55 }
56 
display(int xOffset,int yOffset)57 void PathBarWidget::display(int xOffset, int yOffset)
58 {
59 	int x0 = x() + xOffset;
60 	int y0 = y() + yOffset;
61 	int x1 = x0 + width();
62 	int y1 = y0 + height();
63 
64 	// Draw the path bar background
65 	iV_ShadowBox(x0, y0, x1, y1, 0, pal_RGBA(20, 0, 88, 230), WZCOL_FORM_DARK, pal_RGBA(20, 0, 88, 230));
66 
67 	if (pathComponents.size() <= 1) { return; }
68 	// Draw the path separator between all path components
69 	WidgetGraphicsContext baseContext;
70 	baseContext = baseContext.translatedBy(x() + xOffset, y() + yOffset);
71 	for (size_t idx = 0; idx < (pathComponents.size() - 1); idx++)
72 	{
73 		auto& button = componentButtons[idx];
74 		if (!button->visible())
75 		{
76 			continue;
77 		}
78 		WidgetGraphicsContext context = baseContext.translatedBy(button->x() + button->width(), button->y());
79 		pathSeparator->displayRecursive(context);
80 	}
81 }
82 
pushPathComponent(const std::string & pathComponent)83 void PathBarWidget::pushPathComponent(const std::string &pathComponent)
84 {
85 	pathComponents.push_back(pathComponent);
86 	size_t newComponentIndex = pathComponents.size() - 1;
87 	componentButtons[newComponentIndex] = createPathComponentButton(pathComponent, newComponentIndex);
88 	relayoutComponentButtons();
89 }
90 
popPathComponents(size_t numComponents)91 void PathBarWidget::popPathComponents(size_t numComponents)
92 {
93 	for (size_t i = 0; i < numComponents; i++)
94 	{
95 		pathComponents.pop_back();
96 		componentButtons[pathComponents.size()]->hide();
97 		componentButtons[pathComponents.size()]->deleteLater();
98 		componentButtons.erase(pathComponents.size());
99 	}
100 	relayoutComponentButtons();
101 }
102 
reset()103 void PathBarWidget::reset()
104 {
105 	pathComponents.clear();
106 	for (auto& button : componentButtons)
107 	{
108 		button.second->hide();
109 		button.second->deleteLater();
110 	}
111 	componentButtons.clear();
112 }
113 
currentPath() const114 const std::vector<std::string>& PathBarWidget::currentPath() const
115 {
116 	return pathComponents;
117 }
118 
currentPathStr() const119 std::string PathBarWidget::currentPathStr() const
120 {
121 	if (pathComponents.empty()) { return ""; }
122 	std::string fullPathString = pathComponents.front();
123 	for (size_t i = 1; i < pathComponents.size(); i++)
124 	{
125 		fullPathString += pathSeparatorStr + pathComponents[i];
126 	}
127 	return fullPathString;
128 }
129 
130 struct PathButtonDisplayCache
131 {
132 	WzText text;
133 };
134 
PathButtonDisplayFunc(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)135 static void PathButtonDisplayFunc(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
136 {
137 	// Any widget using PathButtonDisplayFunc must have its pUserData initialized to a (PathButtonDisplayCache*)
138 	assert(psWidget->pUserData != nullptr);
139 	PathButtonDisplayCache& cache = *static_cast<PathButtonDisplayCache*>(psWidget->pUserData);
140 
141 	W_BUTTON *psButton = dynamic_cast<W_BUTTON*>(psWidget);
142 	ASSERT_OR_RETURN(, psButton, "psWidget is null");
143 
144 	int x0 = psButton->x() + xOffset;
145 	int y0 = psButton->y() + yOffset;
146 	int x1 = x0 + psButton->width();
147 	int y1 = y0 + psButton->height();
148 
149 	bool haveText = !psButton->pText.isEmpty();
150 
151 	bool isDown = (psButton->getState() & (WBUT_DOWN | WBUT_LOCK | WBUT_CLICKLOCK)) != 0;
152 	bool isDisabled = (psButton->getState() & WBUT_DISABLE) != 0;
153 	bool isHighlight = (psButton->getState() & WBUT_HIGHLIGHT) != 0;
154 
155 	// Display the button.
156 	auto light_border = pal_RGBA(255, 255, 255, 80);
157 	if (isDown || psButton->UserData == 0)
158 	{
159 		iV_ShadowBox(x0, y0, x1, y1, 0, isDown ? pal_RGBA(0,0,0,0) : light_border, isDisabled ? light_border : WZCOL_FORM_DARK, isDown ? pal_RGBA(10, 0, 70, 250) : pal_RGBA(25, 0, 110, 220));
160 	}
161 	if (isHighlight)
162 	{
163 		iV_Box(x0, y0, x1, y1, WZCOL_FORM_LIGHT);
164 	}
165 
166 	if (haveText)
167 	{
168 		cache.text.setText(psButton->pText.toUtf8(), psButton->FontID);
169 		int fw = cache.text.width();
170 		int fx = x0 + (psButton->width() - fw) / 2;
171 		int fy = y0 + (psButton->height() - cache.text.lineSize()) / 2 - cache.text.aboveBase();
172 		PIELIGHT textColor = WZCOL_FORM_TEXT;
173 		if (isDisabled)
174 		{
175 			cache.text.render(fx + 1, fy + 1, WZCOL_FORM_LIGHT);
176 			textColor = WZCOL_FORM_DISABLE;
177 		}
178 		cache.text.render(fx, fy, textColor);
179 	}
180 
181 	if (isDisabled)
182 	{
183 		// disabled, render something over it!
184 		iV_TransBoxFill(x0, y0, x0 + psButton->width(), y0 + psButton->height());
185 	}
186 }
187 
createPathComponentButton(const std::string & pathComponent,size_t newComponentIndex)188 std::shared_ptr<W_BUTTON> PathBarWidget::createPathComponentButton(const std::string& pathComponent, size_t newComponentIndex)
189 {
190 	W_BUTINIT butInit;
191 	butInit.pText = pathComponent.c_str();
192 	butInit.FontID = font_regular;
193 
194 	int textWidth = iV_GetTextWidth(pathComponent.c_str(), butInit.FontID);
195 	int buttonWidth = textWidth + 2;
196 	butInit.width = buttonWidth;
197 	butInit.height = iV_GetTextLineSize(butInit.FontID);
198 	butInit.UserData = static_cast<UDWORD>(newComponentIndex);
199 	butInit.pDisplay = PathButtonDisplayFunc;
200 	butInit.initPUserDataFunc = []() -> void * { return new PathButtonDisplayCache(); };
201 	butInit.onDelete = [](WIDGET *psWidget) {
202 		assert(psWidget->pUserData != nullptr);
203 		delete static_cast<PathButtonDisplayCache *>(psWidget->pUserData);
204 		psWidget->pUserData = nullptr;
205 	};
206 
207 	auto button = std::make_shared<W_BUTTON>(&butInit);
208 	attach(button);
209 	button->addOnClickHandler([newComponentIndex](W_BUTTON& button) {
210 		auto psParent = std::dynamic_pointer_cast<PathBarWidget>(button.parent());
211 		ASSERT_OR_RETURN(, psParent != nullptr, "No parent");
212 		if (psParent->onClickPath)
213 		{
214 			psParent->onClickPath(*psParent, newComponentIndex);
215 		}
216 	});
217 
218 	return button;
219 }
220 
relayoutComponentButtons()221 void PathBarWidget::relayoutComponentButtons()
222 {
223 	ellipsis->hide();
224 
225 	if (componentButtons.empty()) { return; }
226 
227 	const int requiredWidthForEllipsis = ellipsis->width();
228 	// start from the end / right-side, and display as many path component buttons as possible
229 	// while always displaying the first path component!
230 	int remainingWidth = width();
231 	const int spaceBetweenButtons = pathSeparator->width();
232 
233 	int widthRequiredToDisplayAllButtonsInFull = componentButtons[0]->width();
234 	for (size_t idx = pathComponents.size() - 1; idx > 0; idx--)
235 	{
236 		widthRequiredToDisplayAllButtonsInFull += spaceBetweenButtons + componentButtons[idx]->width();
237 	}
238 
239 	// display the first path component on the left side
240 	componentButtons[0]->move(0, componentButtons[0]->y());
241 
242 	remainingWidth -= (componentButtons[0]->width() + spaceBetweenButtons);
243 	int lastButtonX = width() + spaceBetweenButtons;
244 	for (size_t idx = pathComponents.size() - 1; idx > 0; idx--)
245 	{
246 		auto& button = componentButtons[idx];
247 		bool additionalButtonsRemaining = (idx > 1);
248 		int requiredRemainingWidthToDisplayThisButton = button->width() + spaceBetweenButtons;
249 		if (additionalButtonsRemaining)
250 		{
251 			requiredRemainingWidthToDisplayThisButton += requiredWidthForEllipsis + spaceBetweenButtons;
252 		}
253 		if (requiredRemainingWidthToDisplayThisButton >= remainingWidth)
254 		{
255 			// not enough space to display this button + remaining buttons
256 			// stop looping and add the ellipsis instead
257 			ellipsis->move(componentButtons[0]->x() + componentButtons[0]->width() + spaceBetweenButtons, ellipsis->y());
258 			ellipsis->show();
259 			int extraSpace = lastButtonX - (ellipsis->x() + ellipsis->width() + spaceBetweenButtons);
260 			if (extraSpace > 0)
261 			{
262 				// then shift all the buttons (besides the first one) over by any remaining width
263 				for (size_t i = idx + 1; i < pathComponents.size(); i++)
264 				{
265 					componentButtons[i]->move(componentButtons[i]->x() - extraSpace, componentButtons[i]->y());
266 				}
267 			}
268 			// finally, set any remaining buttons to invisible
269 			for (size_t i = idx; i > 0; i--)
270 			{
271 				componentButtons[i]->hide();
272 			}
273 			break;
274 		}
275 		button->move(lastButtonX - spaceBetweenButtons - button->width(), button->y());
276 		button->show();
277 		lastButtonX = button->x();
278 		remainingWidth -= (spaceBetweenButtons + button->width());
279 	}
280 
281 	if (!ellipsis->visible() && pathComponents.size() > 1)
282 	{
283 		int extraSpace = componentButtons[1]->x() - (componentButtons[0]->x() + componentButtons[0]->width() + spaceBetweenButtons);
284 		if (extraSpace > 0)
285 		{
286 			// then shift all the buttons (besides the first one) over by any remaining width
287 			for (size_t i = 1; i < pathComponents.size(); i++)
288 			{
289 				componentButtons[i]->move(componentButtons[i]->x() - extraSpace, componentButtons[i]->y());
290 			}
291 		}
292 	}
293 }
294 
295 // MARK: - JSONTableWidget
296 
297 #define JSON_ACTION_BUTTONS_PADDING 10
298 #define JSON_ACTION_BUTTONS_SPACING 10
299 #define MENU_BUTTONS_PADDING 20
300 
301 template<typename json_type>
setTableToJson(const json_type & json,JSONTableWidget & jsonTable)302 void setTableToJson(const json_type& json, JSONTableWidget& jsonTable)
303 {
304 	jsonTable.table->clearRows();
305 	jsonTable.currentMaxColumnWidths = {0,0,0};
306 	PIELIGHT specialValueTextColor = WZCOL_TEXT_MEDIUM;
307 	specialValueTextColor.byte.a = static_cast<uint8_t>((float)specialValueTextColor.byte.a * 0.75f);
308 	for (auto item : json.items())
309 	{
310 		auto keyLabel = std::make_shared<W_LABEL>();
311 		keyLabel->setFont(font_regular, WZCOL_FORM_LIGHT);
312 		keyLabel->setString(WzString::fromUtf8(item.key()));
313 		keyLabel->setCanTruncate(true);
314 		keyLabel->setTransparentToClicks(true);
315 		jsonTable.currentMaxColumnWidths[0] = std::max(jsonTable.currentMaxColumnWidths[0], keyLabel->getMaxLineWidth());
316 
317 		auto valueLabel = std::make_shared<W_LABEL>();
318 		std::string valueStr;
319 		PIELIGHT valueTextColor = WZCOL_FORM_LIGHT;
320 		bool expandableItem = false;
321 		if (item.value().is_object())
322 		{
323 			valueStr = "[Object]";
324 			if (!item.value().empty())
325 			{
326 				expandableItem = true;
327 				valueStr += " (items: " + std::to_string(item.value().size()) + ")";
328 			}
329 			else
330 			{
331 				valueStr += " (empty)";
332 			}
333 			valueTextColor = specialValueTextColor;
334 		}
335 		else if (item.value().is_array())
336 		{
337 			valueStr = "[Array]";
338 			if (!item.value().empty())
339 			{
340 				expandableItem = true;
341 				valueStr += "[" + std::to_string(item.value().size()) + "]";
342 			}
343 			else
344 			{
345 				valueStr += " (empty)";
346 			}
347 			valueTextColor = specialValueTextColor;
348 		}
349 		else if (item.value().is_null())
350 		{
351 			valueStr = "[null]";
352 		}
353 		else
354 		{
355 			try {
356 				valueStr = item.value().dump();
357 				if (item.value().is_string() && !valueStr.empty())
358 				{
359 					auto it = jsonTable._specialStrings.find(valueStr);
360 					if (it != jsonTable._specialStrings.end())
361 					{
362 						switch (it->second)
363 						{
364 						case JSONTableWidget::SpecialJSONStringTypes::TYPE_DESCRIPTION:
365 							valueTextColor = specialValueTextColor;
366 							break;
367 						}
368 						// remove surrounding quotes
369 						if (valueStr.back() == '"')
370 						{
371 							valueStr.erase(valueStr.end() - 1);
372 						}
373 						if (!valueStr.empty() && valueStr.front() == '"')
374 						{
375 							valueStr.erase(valueStr.begin());
376 						}
377 					}
378 				}
379 			}
380 			catch (const std::exception& e)
381 			{
382 				debug(LOG_ERROR, "Failed to dump value: %s", e.what());
383 				valueStr = "<failed to dump value>";
384 				valueTextColor = pal_RGBA(150, 0, 0, 250);
385 			}
386 		}
387 		valueLabel->setFont(font_regular, valueTextColor);
388 		valueLabel->setString(WzString::fromUtf8(valueStr));
389 		valueLabel->setCanTruncate(true);
390 		valueLabel->setTransparentToClicks(true);
391 		jsonTable.currentMaxColumnWidths[1] = std::max(jsonTable.currentMaxColumnWidths[1], valueLabel->getMaxLineWidth());
392 
393 		auto expandButton = std::make_shared<W_LABEL>();
394 		expandButton->setFont(font_regular, WZCOL_TEXT_MEDIUM);
395 		if (expandableItem)
396 		{
397 			expandButton->setString("\u27A4"); // ➤
398 		}
399 		expandButton->setCanTruncate(false);
400 		expandButton->setTransparentToClicks(true);
401 
402 		std::vector<std::shared_ptr<WIDGET>> columnWidgets {keyLabel, valueLabel, expandButton};
403 		auto row = TableRow::make(columnWidgets, 24);
404 		// Add custom "on click" handler TableRow itself to "dive into" objects / arrays (if expandableItem)
405 		if (expandableItem)
406 		{
407 			std::string itemKey = item.key();
408 			std::weak_ptr<JSONTableWidget> psWeakJsonTable = std::dynamic_pointer_cast<JSONTableWidget>(jsonTable.shared_from_this());
409 			row->addOnClickHandler([itemKey, psWeakJsonTable](W_BUTTON& button) {
410 				widgScheduleTask([psWeakJsonTable, itemKey]{
411 					if (auto jsonTable = psWeakJsonTable.lock())
412 					{
413 						jsonTable->pushJSONPath(itemKey);
414 					}
415 				});
416 			});
417 			row->setHighlightsOnMouseOver(true);
418 		}
419 		jsonTable.table->addRow(row);
420 	}
421 }
422 
423 template<typename json_type>
jsonHasExpandableChildren(const json_type & json)424 static bool jsonHasExpandableChildren(const json_type& json)
425 {
426 	for (auto item : json.items())
427 	{
428 		if (item.value().is_object())
429 		{
430 			if (!item.value().empty())
431 			{
432 				return true;
433 			}
434 		}
435 		else if (item.value().is_array())
436 		{
437 			if (!item.value().empty())
438 			{
439 				return true;
440 			}
441 		}
442 	}
443 	return false;
444 }
445 
~JSONTableWidget()446 JSONTableWidget::~JSONTableWidget()
447 {
448 	if (optionsOverlayScreen)
449 	{
450 		widgRemoveOverlayScreen(optionsOverlayScreen);
451 	}
452 }
453 
454 struct PopoverMenuButtonDisplayCache
455 {
456 	WzText text;
457 };
458 
PopoverMenuButtonDisplayFunc(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)459 static void PopoverMenuButtonDisplayFunc(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
460 {
461 	// Any widget using PopoverMenuButtonDisplayFunc must have its pUserData initialized to a (PopoverMenuButtonDisplayCache*)
462 	assert(psWidget->pUserData != nullptr);
463 	PopoverMenuButtonDisplayCache& cache = *static_cast<PopoverMenuButtonDisplayCache*>(psWidget->pUserData);
464 
465 	W_BUTTON *psButton = dynamic_cast<W_BUTTON*>(psWidget);
466 	ASSERT_OR_RETURN(, psButton, "psWidget is null");
467 
468 	int x0 = psButton->x() + xOffset;
469 	int y0 = psButton->y() + yOffset;
470 
471 	bool haveText = !psButton->pText.isEmpty();
472 
473 	bool isDown = (psButton->getState() & (WBUT_DOWN | WBUT_LOCK | WBUT_CLICKLOCK)) != 0;
474 	bool isDisabled = (psButton->getState() & WBUT_DISABLE) != 0;
475 	bool isHighlight = (psButton->getState() & WBUT_HIGHLIGHT) != 0;
476 
477 	// Display the button background
478 	PIELIGHT backgroundColor;
479 	backgroundColor.rgba = 0;
480 	if (isDown)
481 	{
482 		backgroundColor = pal_RGBA(10, 0, 70, 250); //WZCOL_FORM_DARK;
483 	}
484 	else if (isHighlight)
485 	{
486 		backgroundColor = pal_RGBA(25, 0, 110, 220); //WZCOL_TEXT_MEDIUM;
487 	}
488 	if (backgroundColor.rgba != 0)
489 	{
490 		// Draw the background
491 		pie_UniTransBoxFill(x0, y0, x0 + psButton->width(), y0 + psButton->height(), backgroundColor);
492 	}
493 
494 	if (haveText)
495 	{
496 		cache.text.setText(psButton->pText.toUtf8(), psButton->FontID);
497 		int fx = x0 + JSON_ACTION_BUTTONS_PADDING;
498 		int fy = y0 + (psButton->height() - cache.text.lineSize()) / 2 - cache.text.aboveBase();
499 		PIELIGHT textColor = WZCOL_FORM_TEXT;
500 		if (isDisabled)
501 		{
502 			cache.text.render(fx + 1, fy + 1, WZCOL_FORM_LIGHT);
503 			textColor = WZCOL_FORM_DISABLE;
504 		}
505 		cache.text.render(fx, fy, textColor);
506 	}
507 
508 	if (isDisabled)
509 	{
510 		// disabled, render something over it!
511 		iV_TransBoxFill(x0, y0, x0 + psButton->width(), y0 + psButton->height());
512 	}
513 }
514 
createOptionsPopoverForm()515 std::shared_ptr<WIDGET> JSONTableWidget::createOptionsPopoverForm()
516 {
517 	// create all the buttons / option rows
518 	std::weak_ptr<JSONTableWidget> weakJSONTableWidget = std::dynamic_pointer_cast<JSONTableWidget>(shared_from_this());
519 	auto createOptionsButton = [weakJSONTableWidget](const WzString& text, const std::function<void (W_BUTTON& button)>& onClickFunc) -> std::shared_ptr<W_BUTTON> {
520 		auto button = std::make_shared<W_BUTTON>();
521 		button->setString(text);
522 		button->FontID = font_regular;
523 		button->displayFunction = PopoverMenuButtonDisplayFunc;
524 		button->pUserData = new PopoverMenuButtonDisplayCache();
525 		button->setOnDelete([](WIDGET *psWidget) {
526 			assert(psWidget->pUserData != nullptr);
527 			delete static_cast<PopoverMenuButtonDisplayCache *>(psWidget->pUserData);
528 			psWidget->pUserData = nullptr;
529 		});
530 		int minButtonWidthForText = iV_GetTextWidth(text.toUtf8().c_str(), button->FontID);
531 		button->setGeometry(0, 0, minButtonWidthForText + MENU_BUTTONS_PADDING, iV_GetTextLineSize(button->FontID) + 4);
532 
533 		// On click, perform the designated onClickHandler and close the popover form / overlay
534 		button->addOnClickHandler([weakJSONTableWidget, onClickFunc](W_BUTTON& button) {
535 			if (auto JSONTableWidget = weakJSONTableWidget.lock())
536 			{
537 				widgRemoveOverlayScreen(JSONTableWidget->optionsOverlayScreen);
538 				onClickFunc(button);
539 				JSONTableWidget->optionsOverlayScreen.reset();
540 			}
541 		});
542 		return button;
543 	};
544 
545 	std::vector<std::shared_ptr<W_BUTTON>> buttons;
546 	if (pathBar && pathBar->visible())
547 	{
548 		buttons.push_back(createOptionsButton(_("Copy Path to Clipboard"), [weakJSONTableWidget](W_BUTTON& button){
549 			if (auto JSONTableWidget = weakJSONTableWidget.lock())
550 			{
551 				wzSetClipboardText(JSONTableWidget->currentJSONPathStr().c_str());
552 			}
553 		}));
554 	}
555 	buttons.push_back(createOptionsButton(_("Copy JSON to Clipboard"), [weakJSONTableWidget](W_BUTTON& button){
556 		if (auto JSONTableWidget = weakJSONTableWidget.lock())
557 		{
558 			std::string dumpedData = JSONTableWidget->dumpDataAtCurrentPath();
559 			wzSetClipboardText(dumpedData.c_str());
560 		}
561 	}));
562 	buttons.push_back(createOptionsButton(_("Dump JSON to StdOut"), [weakJSONTableWidget](W_BUTTON& button){
563 		if (auto JSONTableWidget = weakJSONTableWidget.lock())
564 		{
565 			std::string dumpedData = JSONTableWidget->dumpDataAtCurrentPath();
566 			fprintf(stdout, "[Dumped JSON from path]: %s\n%s\n", JSONTableWidget->currentJSONPathStr().c_str(), dumpedData.c_str());
567 		}
568 	}));
569 
570 	// determine required height for all buttons
571 	int totalButtonHeight = std::accumulate(buttons.begin(), buttons.end(), 0, [](int a, const std::shared_ptr<W_BUTTON>& b) {
572 		return a + b->height();
573 	});
574 	int maxButtonWidth = (*(std::max_element(buttons.begin(), buttons.end(), [](const std::shared_ptr<W_BUTTON>& a, const std::shared_ptr<W_BUTTON>& b){
575 		return a->width() < b->width();
576 	})))->width();
577 
578 	auto itemsList = ScrollableListWidget::make();
579 	itemsList->setBackgroundColor(WZCOL_MENU_BACKGROUND);
580 	itemsList->setPadding({3, 4, 3, 4});
581 	const int itemSpacing = 1;
582 	itemsList->setItemSpacing(itemSpacing);
583 	itemsList->setGeometry(itemsList->x(), itemsList->y(), maxButtonWidth + 8, totalButtonHeight + (static_cast<int>(buttons.size()) * itemSpacing) + 6);
584 	for (auto& button : buttons)
585 	{
586 		itemsList->addItem(button);
587 	}
588 
589 	return itemsList;
590 }
591 
displayChildDropShadows(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)592 static void displayChildDropShadows(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
593 {
594 	PIELIGHT dropShadowColor = pal_RGBA(0, 0, 0, 40);
595 	const int widerPadding = 4;
596 	const int closerPadding = 2;
597 	int childXOffset = psWidget->x() + xOffset;
598 	int childYOffset = psWidget->y() + yOffset;
599 	for (auto& child : psWidget->children())
600 	{
601 		if (!child->visible()) { continue; }
602 		int childX0 = child->x() + childXOffset;
603 		int childY0 = child->y() + childYOffset;
604 		int childDropshadowWiderX0 = std::max(childX0 - widerPadding, 0);
605 		int childDropshadowWiderX1 = std::min(childX0 + child->width() + widerPadding, pie_GetVideoBufferWidth());
606 		int childDropshadowWiderY1 = std::min(childY0 + child->height() + widerPadding, pie_GetVideoBufferHeight());
607 		int childDropshadowCloserX0 = std::max(childX0 - closerPadding, 0);
608 		int childDropshadowCloserX1 = std::min(childX0 + child->width() + closerPadding, pie_GetVideoBufferWidth());
609 		int childDropshadowCloserY1 = std::min(childY0 + child->height() + closerPadding, pie_GetVideoBufferHeight());
610 		pie_UniTransBoxFill(childDropshadowWiderX0, childY0, childDropshadowWiderX1, childDropshadowWiderY1, dropShadowColor);
611 		pie_UniTransBoxFill(childDropshadowCloserX0, childY0, childDropshadowCloserX1, childDropshadowCloserY1, dropShadowColor);
612 	}
613 }
614 
displayOptionsOverlay(const std::shared_ptr<WIDGET> & psParent)615 void JSONTableWidget::displayOptionsOverlay(const std::shared_ptr<WIDGET>& psParent)
616 {
617 	auto lockedScreen = screenPointer.lock();
618 	ASSERT(lockedScreen != nullptr, "The JSONTableWidget does not have an associated screen pointer?");
619 
620 	// Initialize the options overlay screen
621 	optionsOverlayScreen = W_SCREEN::make();
622 	auto newRootFrm = W_FULLSCREENOVERLAY_CLICKFORM::make();
623 	newRootFrm->displayFunction = displayChildDropShadows;
624 	std::weak_ptr<W_SCREEN> psWeakOptionsOverlayScreen(optionsOverlayScreen);
625 	std::weak_ptr<JSONTableWidget> psWeakJsonTable(std::dynamic_pointer_cast<JSONTableWidget>(shared_from_this()));
626 	newRootFrm->onClickedFunc = [psWeakOptionsOverlayScreen, psWeakJsonTable]() {
627 		if (auto psOverlayScreen = psWeakOptionsOverlayScreen.lock())
628 		{
629 			widgRemoveOverlayScreen(psOverlayScreen);
630 		}
631 		// Destroy Options overlay / overlay screen
632 		if (auto strongJsonTable = psWeakJsonTable.lock())
633 		{
634 			strongJsonTable->optionsOverlayScreen.reset();
635 		}
636 	};
637 	newRootFrm->onCancelPressed = newRootFrm->onClickedFunc;
638 	optionsOverlayScreen->psForm->attach(newRootFrm);
639 
640 	// Create the pop-over form
641 	auto optionsPopOver = createOptionsPopoverForm();
642 	newRootFrm->attach(optionsPopOver);
643 
644 	// Position the pop-over form
645 	std::weak_ptr<WIDGET> weakParent = psParent;
646 	optionsPopOver->setCalcLayout([weakParent](WIDGET *psWidget, unsigned int, unsigned int, unsigned int newScreenWidth, unsigned int newScreenHeight){
647 		auto psParent = weakParent.lock();
648 		ASSERT_OR_RETURN(, psParent != nullptr, "parent is null");
649 		// (Ideally, with its left aligned with the left side of the "parent" widget, but ensure full visibility on-screen)
650 		int popOverX0 = psParent->screenPosX();
651 		if (popOverX0 + psWidget->width() > newScreenWidth)
652 		{
653 			popOverX0 = newScreenWidth - psWidget->width();
654 		}
655 		// (Ideally, with its top aligned with the bottom of the "parent" widget, but ensure full visibility on-screen)
656 		int popOverY0 = psParent->screenPosY() + psParent->height();
657 		if (popOverY0 + psWidget->height() > newScreenHeight)
658 		{
659 			popOverY0 = newScreenHeight - psWidget->height();
660 		}
661 		psWidget->move(popOverX0, popOverY0);
662 	});
663 
664 	widgRegisterOverlayScreenOnTopOfScreen(optionsOverlayScreen, lockedScreen);
665 }
666 
667 struct JsonActionButtonDisplayCache
668 {
669 	WzText text;
670 };
671 
JsonActionButtonDisplayFunc(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)672 static void JsonActionButtonDisplayFunc(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
673 {
674 	// Any widget using JsonActionButtonDisplayFunc must have its pUserData initialized to a (JsonActionButtonDisplayCache*)
675 	assert(psWidget->pUserData != nullptr);
676 	JsonActionButtonDisplayCache& cache = *static_cast<JsonActionButtonDisplayCache*>(psWidget->pUserData);
677 
678 	W_BUTTON *psButton = dynamic_cast<W_BUTTON*>(psWidget);
679 	ASSERT_OR_RETURN(, psButton, "psWidget is null");
680 
681 	int x0 = psButton->x() + xOffset;
682 	int y0 = psButton->y() + yOffset;
683 	int x1 = x0 + psButton->width();
684 	int y1 = y0 + psButton->height();
685 
686 	bool haveText = !psButton->pText.isEmpty();
687 
688 	bool isDown = (psButton->getState() & (WBUT_DOWN | WBUT_LOCK | WBUT_CLICKLOCK)) != 0;
689 	bool isDisabled = (psButton->getState() & WBUT_DISABLE) != 0;
690 	bool isHighlight = !isDisabled && ((psButton->getState() & WBUT_HIGHLIGHT) != 0);
691 
692 	// Display the button.
693 	auto light_border = pal_RGBA(255, 255, 255, 80);
694 	auto fill_color = isDown || isDisabled ? pal_RGBA(10, 0, 70, 250) : pal_RGBA(25, 0, 110, 220);
695 	iV_ShadowBox(x0, y0, x1, y1, 0, isDown ? pal_RGBA(0,0,0,0) : light_border, isDisabled ? light_border : WZCOL_FORM_DARK, fill_color);
696 	if (isHighlight)
697 	{
698 		iV_Box(x0 + 2, y0 + 2, x1 - 2, y1 - 2, WZCOL_FORM_HILITE);
699 	}
700 
701 	if (haveText)
702 	{
703 		cache.text.setText(psButton->pText.toUtf8(), psButton->FontID);
704 		int fw = cache.text.width();
705 		int fx = x0 + (psButton->width() - fw) / 2;
706 		int fy = y0 + (psButton->height() - cache.text.lineSize()) / 2 - cache.text.aboveBase();
707 		PIELIGHT textColor = WZCOL_FORM_TEXT;
708 		if (isDisabled)
709 		{
710 			textColor.byte.a = (textColor.byte.a / 2);
711 		}
712 		cache.text.render(fx, fy, textColor);
713 	}
714 }
715 
makeJsonActionButton(const char * text)716 static std::shared_ptr<W_BUTTON> makeJsonActionButton(const char* text)
717 {
718 	auto button = std::make_shared<W_BUTTON>();
719 	button->setString(text);
720 	button->FontID = font_regular;
721 	button->displayFunction = JsonActionButtonDisplayFunc;
722 	button->pUserData = new JsonActionButtonDisplayCache();
723 	button->setOnDelete([](WIDGET *psWidget) {
724 		assert(psWidget->pUserData != nullptr);
725 		delete static_cast<JsonActionButtonDisplayCache *>(psWidget->pUserData);
726 		psWidget->pUserData = nullptr;
727 	});
728 	int minButtonWidthForText = iV_GetTextWidth(text, button->FontID);
729 	button->setGeometry(0, 0, minButtonWidthForText + JSON_ACTION_BUTTONS_PADDING, iV_GetTextLineSize(button->FontID));
730 	return button;
731 }
732 
make(const std::string & title)733 std::shared_ptr<JSONTableWidget> JSONTableWidget::make(const std::string& title)
734 {
735 	auto result = std::make_shared<JSONTableWidget>();
736 
737 	if (!title.empty())
738 	{
739 		// Add title text
740 		result->titleLabel = std::make_shared<W_LABEL>();
741 		result->titleLabel->setFont(font_regular_bold, WZCOL_FORM_TEXT);
742 		result->titleLabel->setString(WzString::fromUtf8(title));
743 		result->titleLabel->setGeometry(/*5*/0, 0, result->titleLabel->getMaxLineWidth() + JSON_ACTION_BUTTONS_PADDING, iV_GetTextLineSize(font_regular_bold));
744 		result->titleLabel->setCacheNeverExpires(true);
745 		result->attach(result->titleLabel);
746 	}
747 
748 	// Add "gear" / options button
749 	result->optionsButton = makeJsonActionButton("\u2699"); // "⚙"
750 	result->optionsButton->setTip("Options");
751 	result->attach(result->optionsButton);
752 	result->optionsButton->addOnClickHandler([](W_BUTTON& button) {
753 		auto psParent = std::dynamic_pointer_cast<JSONTableWidget>(button.parent());
754 		ASSERT_OR_RETURN(, psParent != nullptr, "No parent");
755 		// Display a "pop-over" options menu
756 		psParent->displayOptionsOverlay(button.shared_from_this());
757 	});
758 	result->optionsButton->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
759 		auto psParent = std::dynamic_pointer_cast<JSONTableWidget>(psWidget->parent());
760 		ASSERT_OR_RETURN(, psParent != nullptr, "No parent");
761 		int x0 = psParent->width() - psWidget->width();
762 		psWidget->setGeometry(x0, psWidget->y(), psWidget->width(), psWidget->height());
763 	}));
764 
765 	// Add "update"/refresh button
766 	result->updateButton = makeJsonActionButton("\u21BB"); // "↻"
767 	result->updateButton->setTip("Update");
768 	result->attach(result->updateButton);
769 	result->updateButton->addOnClickHandler([](W_BUTTON& button) {
770 		auto psParent = std::dynamic_pointer_cast<JSONTableWidget>(button.parent());
771 		ASSERT_OR_RETURN(, psParent != nullptr, "No parent");
772 		widgScheduleTask([psParent](){
773 			if (psParent->updateButtonFunc)
774 			{
775 				psParent->updateButtonFunc(*psParent);
776 			}
777 		});
778 	});
779 	result->updateButton->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
780 		auto psParent = std::dynamic_pointer_cast<JSONTableWidget>(psWidget->parent());
781 		ASSERT_OR_RETURN(, psParent != nullptr, "No parent");
782 		int x0 = psParent->optionsButton->x() - psWidget->width() - JSON_ACTION_BUTTONS_SPACING;
783 		psWidget->setGeometry(x0, psWidget->y(), psWidget->width(), psWidget->height());
784 	}));
785 	result->updateButton->hide(); // hidden by default until an updateButtonFunc is set
786 
787 	// Add "path bar" widget
788 	result->pathBar = PathBarWidget::make(".");
789 	result->pathBar->setGeometry(0, 0, 0, iV_GetTextLineSize(font_regular));
790 	result->pathBar->pushPathComponent(" $ ");
791 	result->pathBar->setOnClickPath([](PathBarWidget& pathBarWidget, size_t componentIndex) {
792 		auto psParent = std::dynamic_pointer_cast<JSONTableWidget>(pathBarWidget.parent());
793 		ASSERT_OR_RETURN(, psParent != nullptr, "No parent");
794 		std::weak_ptr<JSONTableWidget> psWeakJsonTable = psParent;
795 		size_t numComponentsToPop = pathBarWidget.numPathComponents() - (componentIndex + 1);
796 		if (numComponentsToPop == 0) { return; }
797 		widgScheduleTask([psWeakJsonTable, numComponentsToPop]{
798 			if (auto jsonTable = psWeakJsonTable.lock())
799 			{
800 				jsonTable->popJSONPath(numComponentsToPop);
801 			}
802 		});
803 	});
804 	result->attach(result->pathBar);
805 	result->pathBar->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
806 		auto psParent = std::dynamic_pointer_cast<JSONTableWidget>(psWidget->parent());
807 		ASSERT_OR_RETURN(, psParent != nullptr, "No parent");
808 		int x0 = (psParent->titleLabel) ? psParent->titleLabel->x() + psParent->titleLabel->width() : 0;
809 		int rightMostEndPoint = ((psParent->updateButton->visible()) ? psParent->updateButton->x() : psParent->optionsButton->x()) - JSON_ACTION_BUTTONS_SPACING;
810 		psWidget->setGeometry(x0, 0, rightMostEndPoint - x0, psWidget->height());
811 	}));
812 	result->pathBar->hide(); // hide path bar by default (until data is set)
813 
814 	// Create Key / Value / "Expand" column headers
815 	auto keyLabel = std::make_shared<W_LABEL>();
816 	keyLabel->setString("Key");
817 	keyLabel->setGeometry(0, 0, 150, 0);
818 	keyLabel->setCacheNeverExpires(true);
819 	auto valueLabel = std::make_shared<W_LABEL>();
820 	valueLabel->setString("Value");
821 	valueLabel->setGeometry(0, 0, 300, 0);
822 	valueLabel->setCacheNeverExpires(true);
823 	auto expandLabel = std::make_shared<W_LABEL>();
824 	expandLabel->setString("+");
825 	expandLabel->setGeometry(0, 0, expandLabel->getMaxLineWidth(), 0);
826 	expandLabel->setCacheNeverExpires(true);
827 	std::vector<TableColumn> columns {{keyLabel, TableColumn::ResizeBehavior::RESIZABLE}, {valueLabel, TableColumn::ResizeBehavior::RESIZABLE_AUTOEXPAND}, {expandLabel, TableColumn::ResizeBehavior::FIXED_WIDTH}};
828 
829 	std::vector<size_t> minimumColumnWidths;
830 	minimumColumnWidths.push_back(static_cast<size_t>(std::max<int>(keyLabel->getMaxLineWidth(), 0)));
831 	minimumColumnWidths.push_back(static_cast<size_t>(std::max<int>(valueLabel->getMaxLineWidth(), 0)));
832 	minimumColumnWidths.push_back(static_cast<size_t>(std::max<int>(expandLabel->getMaxLineWidth(), 0)));
833 
834 	// Create + attach key / value scrollable table view
835 	result->table = ScrollableTableWidget::make(columns);
836 	result->attach(result->table);
837 	result->table->setMinimumColumnWidths(minimumColumnWidths);
838 	result->table->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
839 		auto psParent = std::dynamic_pointer_cast<JSONTableWidget>(psWidget->parent());
840 		ASSERT_OR_RETURN(, psParent != nullptr, "No parent");
841 		int oldWidth = psWidget->width();
842 		int y0 = psParent->pathBar->y() + psParent->pathBar->height() + JSON_ACTION_BUTTONS_SPACING;
843 		psWidget->setGeometry(0, y0, psParent->width(), psParent->height() - y0);
844 
845 		if (oldWidth != psWidget->width())
846 		{
847 			psParent->resizeColumnWidths(false);
848 		}
849 	}));
850 
851 	return result;
852 }
853 
display(int xOffset,int yOffset)854 void JSONTableWidget::display(int xOffset, int yOffset)
855 {
856 	// currently, no-op
857 }
858 
geometryChanged()859 void JSONTableWidget::geometryChanged()
860 {
861 	if (optionsButton)
862 	{
863 		optionsButton->callCalcLayout();
864 	}
865 	if (updateButton)
866 	{
867 		updateButton->callCalcLayout();
868 	}
869 	if (pathBar)
870 	{
871 		pathBar->callCalcLayout();
872 	}
873 	if (table)
874 	{
875 		// depends on the positioning of pathBar
876 		table->callCalcLayout();
877 	}
878 }
879 
screenSizeDidChange(int oldWidth,int oldHeight,int newWidth,int newHeight)880 void JSONTableWidget::screenSizeDidChange(int oldWidth, int oldHeight, int newWidth, int newHeight)
881 {
882 	// call default implementation (which ultimately propagates to all children
883 	W_FORM::screenSizeDidChange(oldWidth, oldHeight, newWidth, newHeight);
884 
885 	// NOTE: To properly support resizing the options overlay screen based on underlying screen layer recalculations
886 	// this must occur *after* the default implementation (above)
887 	if (optionsOverlayScreen == nullptr) return;
888 	optionsOverlayScreen->screenSizeDidChange(oldWidth, oldHeight, newWidth, newHeight);
889 }
890 
currentPathFromRoot() const891 std::vector<std::string> JSONTableWidget::currentPathFromRoot() const
892 {
893 	return actualPushedPathComponents;
894 }
895 
resetPath()896 void JSONTableWidget::resetPath()
897 {
898 	// Reset pathBar
899 	pathBar->reset();
900 	pathBar->pushPathComponent(" $ ");
901 	pathBar->show();
902 
903 	actualPushedPathComponents.clear();
904 }
905 
rootJsonHasExpandableChildren() const906 bool JSONTableWidget::rootJsonHasExpandableChildren() const
907 {
908 	if (_orderedjson.has_value())
909 	{
910 		return jsonHasExpandableChildren(_orderedjson.value());
911 	}
912 	else if (_json.has_value())
913 	{
914 		return jsonHasExpandableChildren(_json.value());
915 	}
916 	return false;
917 }
918 
setSpecialStrings_Internal(const SpecialStrings & specialStrings)919 void JSONTableWidget::setSpecialStrings_Internal(const SpecialStrings& specialStrings)
920 {
921 	JSONTableWidget::SpecialStrings internalResult;
922 	// surround every key with " "
923 	for (const auto& it : specialStrings)
924 	{
925 		internalResult["\"" + it.first + "\""] = it.second;
926 	}
927 	_specialStrings = std::move(internalResult);
928 }
929 
updateData_Internal(const std::function<void ()> & setJsonFunc,bool tryToPreservePath,const SpecialStrings & specialStrings)930 void JSONTableWidget::updateData_Internal(const std::function<void ()>& setJsonFunc, bool tryToPreservePath, const SpecialStrings& specialStrings)
931 {
932 	std::vector<std::string> previousPath = currentPathFromRoot();
933 	auto oldScrollPosition = table->getScrollPosition();
934 	resetPath();
935 
936 	setJsonFunc();
937 	setSpecialStrings_Internal(specialStrings);
938 
939 	bool hasExpandableChildren = rootJsonHasExpandableChildren();
940 	if (!tryToPreservePath)
941 	{
942 		previousPath.clear();
943 	}
944 	// return to the same path (or as close to it as possible) in the updated JSON
945 	bool returnedToSameExactPath = pushJSONPath(previousPath, !tryToPreservePath); // this already calls rebuildTableFromJSON()
946 	if (returnedToSameExactPath)
947 	{
948 		table->setScrollPosition(oldScrollPosition);
949 	}
950 	if (!hasExpandableChildren)
951 	{
952 		// hide path bar
953 		pathBar->hide();
954 	}
955 	refreshUpdateButtonState();
956 }
957 
updateData(const nlohmann::ordered_json & json,bool tryToPreservePath,const SpecialStrings & specialStrings)958 void JSONTableWidget::updateData(const nlohmann::ordered_json& json, bool tryToPreservePath /*= false*/, const SpecialStrings& specialStrings /*= SpecialStrings()*/)
959 {
960 	updateData_Internal([this, &json]{
961 		_orderedjson = json;
962 		_orderedjson_currentPointer = {&(_orderedjson.value())};
963 
964 		_json = nullopt;
965 		_json_currentPointer.clear();
966 	}, tryToPreservePath, specialStrings);
967 }
968 
updateData(const nlohmann::json & json,bool tryToPreservePath,const SpecialStrings & specialStrings)969 void JSONTableWidget::updateData(const nlohmann::json& json, bool tryToPreservePath /*= false*/, const SpecialStrings& specialStrings /*= SpecialStrings()*/)
970 {
971 	updateData_Internal([this, &json]{
972 		_json = json;
973 		_json_currentPointer = {&(_json.value())};
974 
975 		_orderedjson = nullopt;
976 		_orderedjson_currentPointer.clear();
977 	}, tryToPreservePath, specialStrings);
978 }
979 
currentJSONPathStr() const980 std::string JSONTableWidget::currentJSONPathStr() const
981 {
982 	ASSERT_OR_RETURN("", pathBar != nullptr, "pathBar is null");
983 	return pathBar->currentPathStr();
984 }
985 
dumpDataAtCurrentPath() const986 std::string JSONTableWidget::dumpDataAtCurrentPath() const
987 {
988 	if (_orderedjson.has_value())
989 	{
990 		ASSERT_OR_RETURN("", !_orderedjson_currentPointer.empty(), "Should not be empty");
991 		return (*(_orderedjson_currentPointer.back())).dump(1, '\t', true);
992 	}
993 	else if (_json.has_value())
994 	{
995 		ASSERT_OR_RETURN("", !_json_currentPointer.empty(), "Should not be empty");
996 		return (*(_json_currentPointer.back())).dump(1, '\t', true);
997 	}
998 	return "";
999 }
1000 
setUpdateButtonFunc(const CallbackFunc & newUpdateButtonFunc,optional<UDWORD> newAutomaticUpdateInterval)1001 void JSONTableWidget::setUpdateButtonFunc(const CallbackFunc& newUpdateButtonFunc, optional<UDWORD> newAutomaticUpdateInterval)
1002 {
1003 	updateButtonFunc = newUpdateButtonFunc;
1004 	automaticUpdateInterval = newAutomaticUpdateInterval;
1005 	refreshUpdateButtonState();
1006 }
1007 
setAutomaticUpdateInterval(optional<UDWORD> newAutomaticUpdateInterval)1008 void JSONTableWidget::setAutomaticUpdateInterval(optional<UDWORD> newAutomaticUpdateInterval)
1009 {
1010 	automaticUpdateInterval = newAutomaticUpdateInterval;
1011 	refreshUpdateButtonState();
1012 }
1013 
refreshUpdateButtonState()1014 void JSONTableWidget::refreshUpdateButtonState()
1015 {
1016 	bool updateButtonWasVisible = updateButton->visible();
1017 	if (!updateButtonFunc)
1018 	{
1019 		updateButton->hide();
1020 	}
1021 	else
1022 	{
1023 		updateButton->show();
1024 		if (automaticUpdateInterval.has_value())
1025 		{
1026 			updateButton->setProgressBorder(W_BUTTON::ProgressBorder::timed(automaticUpdateInterval.value(), true, W_BUTTON::ProgressBorder::BorderInset(1, 1, 1, 1)), pal_RGBA(100, 100, 255, 200));
1027 			lastUpdateTime = realTime;
1028 		}
1029 		else
1030 		{
1031 			updateButton->setProgressBorder(nullopt);
1032 		}
1033 	}
1034 	if (updateButtonWasVisible != updateButton->visible())
1035 	{
1036 		if (pathBar)
1037 		{
1038 			pathBar->callCalcLayout();
1039 		}
1040 	}
1041 }
1042 
run(W_CONTEXT * psContext)1043 void JSONTableWidget::run(W_CONTEXT *psContext)
1044 {
1045 	if (!automaticUpdateInterval.has_value() || !updateButtonFunc)
1046 	{
1047 		return;
1048 	}
1049 
1050 	if (realTime - lastUpdateTime >= automaticUpdateInterval.value())
1051 	{
1052 		// trigger update call
1053 		auto jsonTable = std::dynamic_pointer_cast<JSONTableWidget>(shared_from_this());
1054 		widgScheduleTask([jsonTable](){
1055 			if (jsonTable->updateButtonFunc)
1056 			{
1057 				jsonTable->updateButtonFunc(*jsonTable);
1058 				jsonTable->lastUpdateTime = realTime;
1059 				jsonTable->refreshUpdateButtonState();
1060 			}
1061 		});
1062 		lastUpdateTime = realTime;
1063 	}
1064 }
1065 
resizeColumnWidths(bool overrideUserColumnSizing)1066 void JSONTableWidget::resizeColumnWidths(bool overrideUserColumnSizing)
1067 {
1068 	if (table->isUserDraggingColumnHeader())
1069 	{
1070 		return;
1071 	}
1072 
1073 	// if necessary / possible, resize the key/value column widths
1074 	std::vector<size_t> columnWidths = table->getColumnWidths();
1075 	columnWidths[0] = currentMaxColumnWidths[0];
1076 	columnWidths[1] = currentMaxColumnWidths[1];
1077 	table->changeColumnWidths(columnWidths, overrideUserColumnSizing);
1078 }
1079 
rebuildTableFromJSON(bool overrideUserColumnSizing)1080 void JSONTableWidget::rebuildTableFromJSON(bool overrideUserColumnSizing)
1081 {
1082 	if (_orderedjson.has_value())
1083 	{
1084 		ASSERT_OR_RETURN(, !_orderedjson_currentPointer.empty(), "Should not be empty");
1085 		setTableToJson(*(_orderedjson_currentPointer.back()), *this);
1086 	}
1087 	else if (_json.has_value())
1088 	{
1089 		ASSERT_OR_RETURN(, !_json_currentPointer.empty(), "Should not be empty");
1090 		setTableToJson(*(_json_currentPointer.back()), *this);
1091 	}
1092 	resizeColumnWidths(overrideUserColumnSizing);
1093 }
1094 
1095 template<typename json_type>
pushJSONPath(const std::string & keyInCurrentJSONPointer,std::vector<json_type> & jsonPointerVec)1096 static optional<std::string> pushJSONPath(const std::string& keyInCurrentJSONPointer, std::vector<json_type>& jsonPointerVec)
1097 {
1098 	ASSERT_OR_RETURN(nullopt, !jsonPointerVec.empty(), "Should not be empty");
1099 	auto& current_json_level = *(jsonPointerVec.back());
1100 	if (current_json_level.is_object())
1101 	{
1102 		auto it = current_json_level.find(keyInCurrentJSONPointer);
1103 		ASSERT_OR_RETURN(nullopt, it != current_json_level.end(), "Could not find key");
1104 		jsonPointerVec.push_back(&(it.value()));
1105 		return keyInCurrentJSONPointer;
1106 	}
1107 	else if (current_json_level.is_array())
1108 	{
1109 		bool ok = false;
1110 		int arrayIndex = WzString::fromUtf8(keyInCurrentJSONPointer).toInt(&ok);
1111 		ASSERT_OR_RETURN(nullopt, ok, "Failed to convert %s to int", keyInCurrentJSONPointer.c_str());
1112 		ASSERT_OR_RETURN(nullopt, arrayIndex >= 0 && arrayIndex < current_json_level.size(), "Array index out of bounds: %d", arrayIndex);
1113 		jsonPointerVec.push_back(&(current_json_level[arrayIndex]));
1114 		return std::string("[") + keyInCurrentJSONPointer + "]";
1115 	}
1116 	return nullopt;
1117 }
1118 
1119 template<typename json_type>
popJSONPath(std::vector<json_type> & jsonPointerVec,size_t numLevels=1)1120 static size_t popJSONPath(std::vector<json_type>& jsonPointerVec, size_t numLevels = 1)
1121 {
1122 	if (numLevels == 0) { return 0; }
1123 	ASSERT_OR_RETURN(0, !jsonPointerVec.empty(), "Should not be empty");
1124 	size_t i = 0;
1125 	for (; i < numLevels; i++)
1126 	{
1127 		if (jsonPointerVec.size() <= 1)
1128 		{
1129 			break;
1130 		}
1131 		jsonPointerVec.pop_back();
1132 	}
1133 	return i;
1134 }
1135 
pushJSONPath(const std::vector<std::string> & pathInCurrentJSONPointer,bool overrideUserColumnSizing)1136 bool JSONTableWidget::pushJSONPath(const std::vector<std::string>& pathInCurrentJSONPointer, bool overrideUserColumnSizing)
1137 {
1138 	for (const auto& newPathComponent : pathInCurrentJSONPointer)
1139 	{
1140 		optional<std::string> descriptionOfNewPathComponent;
1141 		if (_orderedjson.has_value())
1142 		{
1143 			descriptionOfNewPathComponent = ::pushJSONPath(newPathComponent, _orderedjson_currentPointer);
1144 		}
1145 		else if (_json.has_value())
1146 		{
1147 			descriptionOfNewPathComponent = ::pushJSONPath(newPathComponent, _json_currentPointer);
1148 		}
1149 		if (!descriptionOfNewPathComponent.has_value())
1150 		{
1151 			debug(LOG_WZ, "Path component %s no longer exists at parent path: %s", newPathComponent.c_str(), pathBar->currentPathStr().c_str());
1152 			rebuildTableFromJSON(true); // new path, always override user column sizing (if possible)
1153 			return false;
1154 		}
1155 		pathBar->pushPathComponent(descriptionOfNewPathComponent.value());
1156 		actualPushedPathComponents.push_back(newPathComponent);
1157 	}
1158 	rebuildTableFromJSON(!pathInCurrentJSONPointer.empty() && overrideUserColumnSizing);
1159 	return true;
1160 }
1161 
pushJSONPath(const std::string & keyInCurrentJSONPointer)1162 bool JSONTableWidget::pushJSONPath(const std::string& keyInCurrentJSONPointer)
1163 {
1164 	return pushJSONPath(std::vector<std::string>({keyInCurrentJSONPointer}), true);
1165 }
1166 
popJSONPath(size_t numLevels)1167 size_t JSONTableWidget::popJSONPath(size_t numLevels /*= 1*/)
1168 {
1169 	size_t result = 0;
1170 	if (_orderedjson.has_value())
1171 	{
1172 		result = ::popJSONPath(_orderedjson_currentPointer, numLevels);
1173 	}
1174 	else
1175 	{
1176 		result = ::popJSONPath(_json_currentPointer, numLevels);
1177 	}
1178 	pathBar->popPathComponents(result);
1179 	for (size_t i = 0; i < result; i++)
1180 	{
1181 		actualPushedPathComponents.pop_back();
1182 	}
1183 	rebuildTableFromJSON(true);
1184 	return result;
1185 }
1186