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