Bug Summary

File:builds/wireshark/wireshark/ui/qt/lua_debugger/lua_debugger_dialog.cpp
Warning:line 374, column 1
Potential leak of memory pointed to by 'ph'

Annotated Source Code

Press '?' to see keyboard shortcuts

clang -cc1 -cc1 -triple x86_64-pc-linux-gnu -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name lua_debugger_dialog.cpp -analyzer-checker=core -analyzer-checker=apiModeling -analyzer-checker=unix -analyzer-checker=deadcode -analyzer-checker=cplusplus -analyzer-checker=security.insecureAPI.UncheckedReturn -analyzer-checker=security.insecureAPI.getpw -analyzer-checker=security.insecureAPI.gets -analyzer-checker=security.insecureAPI.mktemp -analyzer-checker=security.insecureAPI.mkstemp -analyzer-checker=security.insecureAPI.vfork -analyzer-checker=nullability.NullPassedToNonnull -analyzer-checker=nullability.NullReturnedFromNonnull -analyzer-output plist -w -setup-static-analyzer -mrelocation-model pic -pic-level 2 -fhalf-no-semantic-interposition -fno-delete-null-pointer-checks -mframe-pointer=all -relaxed-aliasing -fmath-errno -ffp-contract=on -fno-rounding-math -ffloat16-excess-precision=fast -fbfloat16-excess-precision=fast -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -tune-cpu generic -debugger-tuning=gdb -fdebug-compilation-dir=/builds/wireshark/wireshark/build -fcoverage-compilation-dir=/builds/wireshark/wireshark/build -resource-dir /usr/lib/llvm-21/lib/clang/21 -isystem /usr/include/glib-2.0 -isystem /usr/lib/x86_64-linux-gnu/glib-2.0/include -isystem /builds/wireshark/wireshark/build/ui/qt -isystem /builds/wireshark/wireshark/ui/qt -isystem /builds/wireshark/wireshark/ui/qt/lua_debugger -isystem /usr/include/x86_64-linux-gnu/qt6/QtWidgets -isystem /usr/include/x86_64-linux-gnu/qt6 -isystem /usr/include/x86_64-linux-gnu/qt6/QtCore -isystem /usr/lib/x86_64-linux-gnu/qt6/mkspecs/linux-g++ -isystem /usr/include/x86_64-linux-gnu/qt6/QtGui -isystem /usr/include/x86_64-linux-gnu/qt6/QtCore5Compat -isystem /usr/include/x86_64-linux-gnu/qt6/QtConcurrent -isystem /usr/include/x86_64-linux-gnu/qt6/QtPrintSupport -isystem /usr/include/x86_64-linux-gnu/qt6/QtNetwork -isystem /usr/include/x86_64-linux-gnu/qt6/QtMultimedia -isystem /usr/include/x86_64-linux-gnu/qt6/QtDBus -D G_DISABLE_DEPRECATED -D G_DISABLE_SINGLE_INCLUDES -D QT_CONCURRENT_LIB -D QT_CORE5COMPAT_LIB -D QT_CORE_LIB -D QT_DBUS_LIB -D QT_GUI_LIB -D QT_MULTIMEDIA_LIB -D QT_NETWORK_LIB -D QT_PRINTSUPPORT_LIB -D QT_WIDGETS_LIB -D WS_DEBUG -D WS_DEBUG_UTF_8 -I /builds/wireshark/wireshark/build/ui/qt/qtui_autogen/include -I /builds/wireshark/wireshark/build -I /builds/wireshark/wireshark -I /builds/wireshark/wireshark/include -D _GLIBCXX_ASSERTIONS -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/x86_64-linux-gnu/c++/14 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14/backward -internal-isystem /usr/lib/llvm-21/lib/clang/21/include -internal-isystem /usr/local/include -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/14/../../../../x86_64-linux-gnu/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -fmacro-prefix-map=/builds/wireshark/wireshark/= -fmacro-prefix-map=/builds/wireshark/wireshark/build/= -fmacro-prefix-map=../= -Wno-format-nonliteral -std=c++17 -fdeprecated-macro -ferror-limit 19 -fwrapv -fwrapv-pointer -fstrict-flex-arrays=3 -stack-protector 2 -fstack-clash-protection -fcf-protection=full -fgnuc-version=4.2.1 -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions -fcolor-diagnostics -analyzer-output=html -faddrsig -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o /builds/wireshark/wireshark/sbout/2026-04-21-100427-3641-1 -x c++ /builds/wireshark/wireshark/ui/qt/lua_debugger/lua_debugger_dialog.cpp
1/* lua_debugger_dialog.cpp
2 *
3 * Wireshark - Network traffic analyzer
4 * By Gerald Combs <[email protected]>
5 * Copyright 1998 Gerald Combs
6 *
7 * SPDX-License-Identifier: GPL-2.0-or-later
8 */
9
10#include "lua_debugger_dialog.h"
11#include "accordion_frame.h"
12#include <QApplication>
13#include "lua_debugger_code_view.h"
14#include "lua_debugger_find_frame.h"
15#include "lua_debugger_goto_line_frame.h"
16#include "main_application.h"
17#include "main_window.h"
18#include "ui_lua_debugger_dialog.h"
19#include "utils/stock_icon.h"
20#include "widgets/collapsible_section.h"
21
22#include <QAction>
23#include <QCheckBox>
24#include <QChildEvent>
25#include <QClipboard>
26#include <QCloseEvent>
27#include <QEvent>
28#if QT_VERSION((6<<16)|(4<<8)|(2)) >= QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0))
29#include <QKeyCombination>
30#endif
31#include <QKeyEvent>
32#include <QColor>
33#include <QComboBox>
34#include <QDir>
35#include <QAbstractItemView>
36#include <QDirIterator>
37#include <QDragMoveEvent>
38#include <QDropEvent>
39#include <QFile>
40#include <QFileInfo>
41#include <QFont>
42#include <QFontDatabase>
43#include <QFormLayout>
44#include <QGuiApplication>
45#include <QHeaderView>
46#include <QIcon>
47#include <QJsonArray>
48#include <QJsonParseError>
49#include <QLineEdit>
50#include <QJsonDocument>
51#include <QJsonObject>
52#include <QKeySequence>
53#include <QList>
54#include <QMenu>
55#include <QMessageBox>
56#include <QMouseEvent>
57#include <QMetaObject>
58#include <QPainter>
59#include <QPalette>
60#include <QPlainTextEdit>
61#include <QPointer>
62#include <QSet>
63#include <QStandardPaths>
64#include <QStyle>
65#include <QTextDocument>
66#include <QSplitter>
67#include <QTimer>
68#include <QStyledItemDelegate>
69#include <QStyleOptionViewItem>
70#include <QAbstractItemModel>
71#include <QModelIndex>
72#include <QTreeWidgetItem>
73#include <QTextStream>
74#include <QVBoxLayout>
75#include <QToolButton>
76#include <QHBoxLayout>
77#include <algorithm>
78
79#include <glib.h>
80
81#include "ui/recent.h"
82#include "app/application_flavor.h"
83#include "wsutil/filesystem.h"
84#include <epan/prefs.h>
85#include <ui/qt/utils/color_utils.h>
86#include <ui/qt/widgets/wireshark_file_dialog.h>
87#include <ui/qt/utils/qt_ui_utils.h>
88
89#define LUA_DEBUGGER_SETTINGS_FILE"lua_debugger.json" "lua_debugger.json"
90
91namespace
92{
93/** Global personal config path — debugger settings are not profile-specific. */
94QString
95luaDebuggerSettingsFilePath()
96{
97 char *p = get_persconffile_path(
98 LUA_DEBUGGER_SETTINGS_FILE"lua_debugger.json", false,
99 application_configuration_environment_prefix());
100 return gchar_free_to_qstring(p);
101}
102} // namespace
103
104extern "C" void wslua_debugger_ui_callback(const char *file_path, int64_t line)
105{
106 LuaDebuggerDialog *dialog = LuaDebuggerDialog::instance();
107 if (dialog)
108 {
109 dialog->handlePause(file_path, line);
110 }
111}
112
113LuaDebuggerDialog *LuaDebuggerDialog::_instance = nullptr;
114int32_t LuaDebuggerDialog::currentTheme_ = WSLUA_DEBUGGER_THEME_AUTO;
115
116int32_t LuaDebuggerDialog::currentTheme() {
117 return currentTheme_;
118}
119
120namespace
121{
122// ============================================================================
123// Settings Keys (for JSON persistence)
124// ============================================================================
125namespace SettingsKeys
126{
127constexpr const char *Theme = "theme";
128constexpr const char *MainSplitter = "mainSplitterState";
129constexpr const char *LeftSplitter = "leftSplitterState";
130constexpr const char *SectionVariables = "sectionVariables";
131constexpr const char *SectionStack = "sectionStack";
132constexpr const char *SectionFiles = "sectionFiles";
133constexpr const char *SectionBreakpoints = "sectionBreakpoints";
134constexpr const char *SectionEval = "sectionEval";
135constexpr const char *SectionSettings = "sectionSettings";
136constexpr const char *SectionWatch = "sectionWatch";
137constexpr const char *Breakpoints = "breakpoints";
138constexpr const char *Watches = "watches";
139} // namespace SettingsKeys
140
141/** QVariantMap values for JSON arrays are typically QVariantList of QVariantMap. */
142static QJsonArray
143jsonArrayFromSettingsMap(const QVariantMap &map, const char *key)
144{
145 const QVariant v = map.value(QString::fromUtf8(key));
146 if (!v.isValid())
147 {
148 return QJsonArray();
149 }
150 return QJsonValue::fromVariant(v).toArray();
151}
152
153// ============================================================================
154// Tree Widget User Roles
155// ============================================================================
156constexpr qint32 FileTreePathRole = static_cast<qint32>(Qt::UserRole);
157constexpr qint32 FileTreeIsDirectoryRole = static_cast<qint32>(Qt::UserRole + 1);
158constexpr qint32 BreakpointFileRole = static_cast<qint32>(Qt::UserRole + 2);
159constexpr qint32 BreakpointLineRole = static_cast<qint32>(Qt::UserRole + 3);
160constexpr qint32 StackItemFileRole = static_cast<qint32>(Qt::UserRole + 4);
161constexpr qint32 StackItemLineRole = static_cast<qint32>(Qt::UserRole + 5);
162constexpr qint32 StackItemNavigableRole = static_cast<qint32>(Qt::UserRole + 6);
163constexpr qint32 StackItemLevelRole = static_cast<qint32>(Qt::UserRole + 7);
164constexpr qint32 VariablePathRole = static_cast<qint32>(Qt::UserRole + 8);
165constexpr qint32 VariableTypeRole = static_cast<qint32>(Qt::UserRole + 9);
166constexpr qint32 VariableCanExpandRole = static_cast<qint32>(Qt::UserRole + 10);
167constexpr qint32 WatchSpecRole = static_cast<qint32>(Qt::UserRole + 11);
168constexpr qint32 WatchSubpathRole = static_cast<qint32>(Qt::UserRole + 13);
169constexpr qint32 WatchLastValueRole = static_cast<qint32>(Qt::UserRole + 14);
170constexpr qint32 WatchPendingNewRole = static_cast<qint32>(Qt::UserRole + 15);
171/*
172 * Expansion state for watch roots and Variables sections is tracked in
173 * LuaDebuggerDialog::watchExpansion_ and variablesExpansion_ (runtime-only
174 * QHashes). The dialog members are the single source of truth and survive
175 * child-item destruction during pause / resume / step.
176 */
177/** Per-root map path/subpath key → last raw value string (child bold-on-change). */
178constexpr qint32 WatchChildSnapRole =
179 static_cast<qint32>(Qt::UserRole + 20);
180
181constexpr qsizetype WATCH_TOOLTIP_MAX_CHARS = 4096;
182constexpr int WATCH_EXPR_MAX_CHARS = 65536;
183
184/** @brief Registers the UI callback with the Lua debugger core at load time. */
185class LuaDebuggerUiCallbackRegistrar
186{
187 public:
188 LuaDebuggerUiCallbackRegistrar()
189 {
190 wslua_debugger_register_ui_callback(wslua_debugger_ui_callback);
191 }
192
193 ~LuaDebuggerUiCallbackRegistrar()
194 {
195 wslua_debugger_register_ui_callback(NULL__null);
196 }
197};
198
199static LuaDebuggerUiCallbackRegistrar g_luaDebuggerUiCallbackRegistrar;
200
201/** @brief Build a key sequence from a key event for matching QAction shortcuts. */
202static QKeySequence luaSeqFromKeyEvent(const QKeyEvent *ke)
203{
204#if QT_VERSION((6<<16)|(4<<8)|(2)) >= QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0))
205 return QKeySequence(QKeyCombination(ke->modifiers(), static_cast<Qt::Key>(ke->key())));
206#else
207 return QKeySequence(ke->key() | ke->modifiers());
208#endif
209}
210
211/**
212 * @brief True if @a pressed is one of the debugger shortcuts that overlap the
213 * main window (Find, Save, Go to line, Reload Lua plugins).
214 */
215static bool matchesLuaDebuggerShortcutKeys(Ui::LuaDebuggerDialog *ui,
216 const QKeySequence &pressed)
217{
218 return (pressed.matches(ui->actionFind->shortcut()) == QKeySequence::ExactMatch) ||
219 (pressed.matches(ui->actionSaveFile->shortcut()) == QKeySequence::ExactMatch) ||
220 (pressed.matches(ui->actionGoToLine->shortcut()) == QKeySequence::ExactMatch) ||
221 (pressed.matches(ui->actionReloadLuaPlugins->shortcut()) == QKeySequence::ExactMatch);
222}
223
224/**
225 * @brief Run debugger toolbar actions that share shortcuts with the main window.
226 *
227 * When a capture file is open, Wireshark enables Find Packet (Ctrl+F) and
228 * Go to Packet (Ctrl+G). QEvent::ShortcutOverride is handled separately: we only
229 * accept() there so Qt does not activate the main-window QAction; triggering
230 * happens on KeyPress only. Doing both would call showAccordionFrame(..., true)
231 * twice and toggle the bar closed immediately after opening.
232 *
233 * @return True if @a pressed matches one of these shortcuts (handled or not).
234 */
235static bool triggerLuaDebuggerShortcuts(Ui::LuaDebuggerDialog *ui,
236 const QKeySequence &pressed)
237{
238 if (pressed.matches(ui->actionFind->shortcut()) == QKeySequence::ExactMatch)
239 {
240 if (ui->actionFind->isEnabled())
241 {
242 ui->actionFind->trigger();
243 }
244 return true;
245 }
246 if (pressed.matches(ui->actionSaveFile->shortcut()) == QKeySequence::ExactMatch)
247 {
248 if (ui->actionSaveFile->isEnabled())
249 {
250 ui->actionSaveFile->trigger();
251 }
252 return true;
253 }
254 if (pressed.matches(ui->actionGoToLine->shortcut()) == QKeySequence::ExactMatch)
255 {
256 if (ui->actionGoToLine->isEnabled())
257 {
258 ui->actionGoToLine->trigger();
259 }
260 return true;
261 }
262 if (pressed.matches(ui->actionReloadLuaPlugins->shortcut()) ==
263 QKeySequence::ExactMatch)
264 {
265 if (ui->actionReloadLuaPlugins->isEnabled())
266 {
267 ui->actionReloadLuaPlugins->trigger();
268 }
269 return true;
270 }
271 return false;
272}
273
274static QTreeWidgetItem *watchRootItem(QTreeWidgetItem *item)
275{
276 while (item && item->parent())
277 {
278 item = item->parent();
279 }
280 return item;
281}
282
283/** Top-level Variables section key (`Locals` / `Globals` / `Upvalues`). */
284static QString variableSectionRootKeyFromItem(const QTreeWidgetItem *item)
285{
286 if (!item)
287 {
288 return QString();
289 }
290 const QTreeWidgetItem *walk = item;
291 while (walk->parent())
292 {
293 walk = walk->parent();
294 }
295 return walk->data(0, VariablePathRole).toString();
296}
297
298static bool watchSpecUsesPathResolution(const QString &spec)
299{
300 const QByteArray ba = spec.toUtf8();
301 return wslua_debugger_watch_spec_uses_path_resolution(ba.constData());
302}
303
304/** Child variable path under @a parentPath (Variables tree and path-based Watch rows). */
305static QString variableTreeChildPath(const QString &parentPath,
306 const QString &nameText)
307{
308 if (parentPath.isEmpty())
309 {
310 return nameText;
311 }
312 if (nameText.startsWith(QLatin1Char('[')))
313 {
314 return parentPath + nameText;
315 }
316 return parentPath + QLatin1Char('.') + nameText;
317}
318
319/** Globals subtree is sorted by name; Locals/Upvalues keep engine order. */
320static bool variableChildrenShouldSortByName(const QString &parentPath)
321{
322 return !parentPath.isEmpty() &&
323 parentPath.startsWith(QLatin1String("Globals"));
324}
325
326/**
327 * Shared fields extracted from a `wslua_variable_t` for populating either the
328 * Variables tree or a Watch child row. Kept here so both call sites agree on
329 * how engine data maps to UI strings.
330 */
331struct VariableRowFields
332{
333 QString name;
334 QString value;
335 QString type;
336 bool canExpand = false;
337 QString childPath;
338};
339
340static VariableRowFields readVariableRowFields(const wslua_variable_t &v,
341 const QString &parentPath)
342{
343 VariableRowFields f;
344 f.name = QString::fromUtf8(v.name ? v.name : "");
345 f.value = QString::fromUtf8(v.value ? v.value : "");
346 f.type = QString::fromUtf8(v.type ? v.type : "");
347 f.canExpand = v.can_expand ? true : false;
348 f.childPath = variableTreeChildPath(parentPath, f.name);
349 return f;
350}
351
352/**
353 * Install the expansion indicator on @a item and (optionally) a dummy child
354 * placeholder so that onItemExpanded can lazily populate children. Watch rows
355 * use `enabledOnlyPlaceholder=true` so the placeholder never becomes selectable.
356 */
357static void applyVariableExpansionIndicator(QTreeWidgetItem *item,
358 bool canExpand,
359 bool enabledOnlyPlaceholder)
360{
361 if (canExpand)
34
Assuming 'canExpand' is true
35
Taking true branch
362 {
363 item->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);
364 QTreeWidgetItem *const ph = new QTreeWidgetItem(item);
36
Memory is allocated
365 if (enabledOnlyPlaceholder
36.1
'enabledOnlyPlaceholder' is false
)
37
Taking false branch
366 {
367 ph->setFlags(Qt::ItemIsEnabled);
368 }
369 }
370 else
371 {
372 item->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
373 }
374}
38
Potential leak of memory pointed to by 'ph'
375
376/** Full Variables path for path-style watches (e.g. Locals.foo for "foo"). */
377static QString watchVariablePathForSpec(const QString &spec)
378{
379 char *p =
380 wslua_debugger_watch_variable_path_for_spec(spec.toUtf8().constData());
381 if (!p)
382 {
383 return QString();
384 }
385 QString s = QString::fromUtf8(p);
386 g_free(p);
387 return s;
388}
389
390/**
391 * Variables-tree path for UI (matches locals / upvalues / globals resolution for
392 * the first path segment when paused; otherwise same as watchVariablePathForSpec).
393 */
394static QString watchResolvedVariablePathForTooltip(const QString &spec)
395{
396 if (spec.trimmed().isEmpty())
397 {
398 return QString();
399 }
400 char *p = wslua_debugger_watch_resolved_variable_path_for_spec(
401 spec.toUtf8().constData());
402 if (!p)
403 {
404 return QString();
405 }
406 QString s = QString::fromUtf8(p);
407 g_free(p);
408 return s;
409}
410
411/** Sets VariablePathRole on a watch root from spec (resolved section when paused). */
412static void watchRootSetVariablePathRoleFromSpec(QTreeWidgetItem *row,
413 const QString &spec)
414{
415 if (!row)
416 {
417 return;
418 }
419 const QString t = spec.trimmed();
420 if (t.isEmpty())
421 {
422 row->setData(0, VariablePathRole, QVariant());
423 return;
424 }
425 const QString vpRes = watchResolvedVariablePathForTooltip(t);
426 if (!vpRes.isEmpty())
427 {
428 row->setData(0, VariablePathRole, vpRes);
429 return;
430 }
431 const QString vp = watchVariablePathForSpec(t);
432 if (!vp.isEmpty())
433 {
434 row->setData(0, VariablePathRole, vp);
435 }
436 else
437 {
438 row->setData(0, VariablePathRole, QVariant());
439 }
440}
441
442/** Locals / Upvalues / Globals line for watch tooltips (full variable-tree path). */
443static QString watchPathOriginSuffix(const QTreeWidgetItem *item,
444 const QString &spec)
445{
446 /* Prefer resolver output (matches lookup order for unqualified names). */
447 QString vp;
448 if (!spec.trimmed().isEmpty())
449 {
450 vp = watchResolvedVariablePathForTooltip(spec);
451 }
452 if (vp.isEmpty() && item)
453 {
454 vp = item->data(0, VariablePathRole).toString();
455 }
456 if (vp.startsWith(QLatin1String("Locals.")) ||
457 vp == QLatin1String("Locals"))
458 {
459 return QStringLiteral("\n%1")(QString(QtPrivate::qMakeStringPrivate(u"" "\n%1"))).arg(
460 LuaDebuggerDialog::tr("From: Locals"));
461 }
462 if (vp.startsWith(QLatin1String("Upvalues.")) ||
463 vp == QLatin1String("Upvalues"))
464 {
465 return QStringLiteral("\n%1")(QString(QtPrivate::qMakeStringPrivate(u"" "\n%1"))).arg(
466 LuaDebuggerDialog::tr("From: Upvalues"));
467 }
468 if (vp.startsWith(QLatin1String("Globals.")) ||
469 vp == QLatin1String("Globals"))
470 {
471 return QStringLiteral("\n%1")(QString(QtPrivate::qMakeStringPrivate(u"" "\n%1"))).arg(
472 LuaDebuggerDialog::tr("From: Globals"));
473 }
474 return QString();
475}
476
477static QString capWatchTooltipText(const QString &s)
478{
479 if (s.size() <= WATCH_TOOLTIP_MAX_CHARS)
480 {
481 return s;
482 }
483 return s.left(WATCH_TOOLTIP_MAX_CHARS) +
484 LuaDebuggerDialog::tr("\n… (truncated)");
485}
486
487/** Parent path key for Locals.a.b / a[1].x style watch paths (expression subpaths or variable paths). */
488static QString watchPathParentKey(const QString &path)
489{
490 if (path.isEmpty())
491 {
492 return QString();
493 }
494 if (path.endsWith(QLatin1Char(']')))
495 {
496 int depth = 0;
497 for (int i = static_cast<int>(path.size()) - 1; i >= 0; --i)
498 {
499 const QChar c = path.at(i);
500 if (c == QLatin1Char(']'))
501 {
502 depth++;
503 }
504 else if (c == QLatin1Char('['))
505 {
506 depth--;
507 if (depth == 0)
508 {
509 return path.left(i);
510 }
511 }
512 }
513 return QString();
514 }
515 const qsizetype dot = path.lastIndexOf(QLatin1Char('.'));
516 if (dot > 0)
517 {
518 return path.left(dot);
519 }
520 return QString();
521}
522
523static void applyWatchChildRowPresentation(QTreeWidgetItem *item,
524 const QString &stableKey,
525 const QString &rawVal,
526 const QString &typeText)
527{
528 QTreeWidgetItem *root = watchRootItem(item);
529 if (!root)
530 {
531 return;
532 }
533 QVariantMap snaps = root->data(0, WatchChildSnapRole).toMap();
534 const QString prev = snaps.value(stableKey).toString();
535 item->setText(1, rawVal);
536 QString tooltipSuffix =
537 typeText.isEmpty()
538 ? QString()
539 : LuaDebuggerDialog::tr("Type: %1").arg(typeText);
540 item->setToolTip(
541 0,
542 capWatchTooltipText(
543 tooltipSuffix.isEmpty()
544 ? item->text(0)
545 : QStringLiteral("%1\n%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1\n%2"))).arg(item->text(0), tooltipSuffix)));
546 item->setToolTip(
547 1,
548 capWatchTooltipText(
549 tooltipSuffix.isEmpty()
550 ? rawVal
551 : QStringLiteral("%1\n%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1\n%2"))).arg(rawVal, tooltipSuffix)));
552 QFont f1 = item->font(1);
553 f1.setBold(!prev.isEmpty() && prev != rawVal);
554 item->setFont(1, f1);
555 snaps[stableKey] = rawVal;
556 root->setData(0, WatchChildSnapRole, snaps);
557}
558
559static int watchSubpathBoundaryCount(const QString &subpath)
560{
561 QString p = subpath;
562 if (p.startsWith(QLatin1Char('.')))
563 {
564 p = p.mid(1);
565 }
566 int n = 0;
567 for (QChar ch : p)
568 {
569 if (ch == QLatin1Char('.') || ch == QLatin1Char('['))
570 {
571 n++;
572 }
573 }
574 return n;
575}
576
577static QTreeWidgetItem *findWatchItemBySubpathOrPathKey(QTreeWidgetItem *subtree,
578 const QString &key)
579{
580 if (!subtree || key.isEmpty())
581 {
582 return nullptr;
583 }
584 QList<QTreeWidgetItem *> queue;
585 queue.append(subtree);
586 while (!queue.isEmpty())
587 {
588 QTreeWidgetItem *it = queue.takeFirst();
589 const QString sp = it->data(0, WatchSubpathRole).toString();
590 const QString vp = it->data(0, VariablePathRole).toString();
591 if ((!sp.isEmpty() && sp == key) || (!vp.isEmpty() && vp == key))
592 {
593 return it;
594 }
595 for (int i = 0; i < it->childCount(); ++i)
596 {
597 queue.append(it->child(i));
598 }
599 }
600 return nullptr;
601}
602
603/** Variables tree: match @a key against VariablePathRole only. */
604static QTreeWidgetItem *findVariableTreeItemByPathKey(QTreeWidgetItem *subtree,
605 const QString &key)
606{
607 if (!subtree || key.isEmpty())
608 {
609 return nullptr;
610 }
611 QList<QTreeWidgetItem *> queue;
612 queue.append(subtree);
613 while (!queue.isEmpty())
614 {
615 QTreeWidgetItem *it = queue.takeFirst();
616 if (it->data(0, VariablePathRole).toString() == key)
617 {
618 return it;
619 }
620 for (int i = 0; i < it->childCount(); ++i)
621 {
622 queue.append(it->child(i));
623 }
624 }
625 return nullptr;
626}
627
628using TreePathKeyFinder = QTreeWidgetItem *(*)(QTreeWidgetItem *,
629 const QString &);
630
631/**
632 * Re-expand @a subtree's descendants whose path key matches one of @a pathKeys.
633 * Ancestors are expanded first so that Qt's lazy expand handlers populate each
634 * level before we descend.
635 *
636 * Shared by Watch (`findWatchItemBySubpathOrPathKey`, `onWatchItemExpanded`)
637 * and Variables (`findVariableTreeItemByPathKey`, `onVariableItemExpanded`).
638 *
639 * Keys are processed shallow-first (by path-boundary count). The per-key
640 * ancestor chain handles deep-only keys whose intermediate ancestors are not
641 * in @a pathKeys; missing items are skipped (structural gaps between pauses).
642 */
643static void reexpandTreeDescendantsByPathKeys(QTreeWidgetItem *subtree,
644 QStringList pathKeys,
645 TreePathKeyFinder findByKey)
646{
647 if (!subtree || pathKeys.isEmpty() || !findByKey)
648 {
649 return;
650 }
651 std::sort(pathKeys.begin(), pathKeys.end(),
652 [](const QString &a, const QString &b)
653 {
654 const int ca = watchSubpathBoundaryCount(a);
655 const int cb = watchSubpathBoundaryCount(b);
656 if (ca != cb)
657 {
658 return ca < cb;
659 }
660 return a < b;
661 });
662 for (const QString &pathKey : pathKeys)
663 {
664 QStringList chain;
665 for (QString cur = pathKey; !cur.isEmpty();
666 cur = watchPathParentKey(cur))
667 {
668 chain.prepend(cur);
669 }
670 for (const QString &k : chain)
671 {
672 QTreeWidgetItem *n = findByKey(subtree, k);
673 if (!n)
674 {
675 continue;
676 }
677 if (!n->isExpanded())
678 {
679 n->setExpanded(true);
680 }
681 }
682 }
683}
684
685static void reexpandWatchDescendantsByPathKeys(QTreeWidgetItem *subtree,
686 QStringList pathKeys)
687{
688 reexpandTreeDescendantsByPathKeys(subtree, std::move(pathKeys),
689 findWatchItemBySubpathOrPathKey);
690}
691
692static void clearWatchFilterErrorChrome(QTreeWidgetItem *item, QTreeWidget *tree)
693{
694 const QPalette &pal = tree->palette();
695 item->setForeground(0, pal.brush(QPalette::Text));
696 item->setForeground(1, pal.brush(QPalette::Text));
697 item->setBackground(0, QBrush());
698 item->setBackground(1, QBrush());
699}
700
701static void applyWatchFilterErrorChrome(QTreeWidgetItem *item, QTreeWidget *tree)
702{
703 Q_UNUSED(tree)(void)tree;;
704 QColor fg = ColorUtils::fromColorT(&prefs.gui_filter_invalid_fg);
705 QColor bg = ColorUtils::fromColorT(&prefs.gui_filter_invalid_bg);
706 item->setForeground(0, fg);
707 item->setForeground(1, fg);
708 item->setBackground(0, bg);
709 item->setBackground(1, bg);
710}
711
712/* Initialize a freshly-created top-level watch row from a canonical spec.
713 * The on-disk "watches" array is a flat list of spec strings (see
714 * storeWatchList / rebuildWatchTreeFromSettings). */
715static void setupWatchRootItemFromSpec(QTreeWidgetItem *row,
716 const QString &spec)
717{
718 row->setFlags(row->flags() | Qt::ItemIsEditable | Qt::ItemIsEnabled |
719 Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
720 row->setText(0, spec);
721 row->setData(0, WatchSpecRole, spec);
722 row->setData(0, WatchSubpathRole, QString());
723 row->setData(0, WatchPendingNewRole, QVariant(false));
724 watchRootSetVariablePathRoleFromSpec(row, spec);
725 row->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);
726 QTreeWidgetItem *const ph = new QTreeWidgetItem(row);
727 ph->setFlags(Qt::ItemIsEnabled);
728}
729
730/**
731 * @brief Watch tree that only allows top-level reordering via drag-and-drop.
732 *
733 * The dialog's settings map (`settings_`) is refreshed from the tree only at
734 * close time via `storeDialogSettings()` / `saveSettingsFile()`, so a drop
735 * event has no persistence work to do beyond letting QTreeWidget finish the
736 * internal move.
737 */
738class WatchTreeWidget : public QTreeWidget
739{
740 public:
741 explicit WatchTreeWidget(LuaDebuggerDialog *dlg, QWidget *parent = nullptr)
742 : QTreeWidget(parent)
743 {
744 Q_UNUSED(dlg)(void)dlg;;
745 }
746
747 protected:
748 void dragMoveEvent(QDragMoveEvent *event) override
749 {
750 QTreeWidget::dragMoveEvent(event);
751 if (!event->isAccepted())
752 {
753 return;
754 }
755#if QT_VERSION((6<<16)|(4<<8)|(2)) >= QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0))
756 const QPoint pos = event->position().toPoint();
757#else
758 const QPoint pos = event->pos();
759#endif
760 const QModelIndex idx = indexAt(pos);
761 if (idx.isValid() && idx.parent().isValid())
762 {
763 event->ignore();
764 return;
765 }
766 /* OnItem on a top-level row nests the dragged row as a child — only allow
767 * AboveItem / BelowItem reorder between roots. */
768 if (idx.isValid() && !idx.parent().isValid() &&
769 dropIndicatorPosition() == QAbstractItemView::OnItem)
770 {
771 event->ignore();
772 return;
773 }
774 }
775
776 void dropEvent(QDropEvent *event) override
777 {
778#if QT_VERSION((6<<16)|(4<<8)|(2)) >= QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0))
779 const QPoint pos = event->position().toPoint();
780#else
781 const QPoint pos = event->pos();
782#endif
783 const QModelIndex idx = indexAt(pos);
784 if (idx.isValid() && idx.parent().isValid())
785 {
786 event->ignore();
787 return;
788 }
789 if (idx.isValid() && !idx.parent().isValid() &&
790 dropIndicatorPosition() == QAbstractItemView::OnItem)
791 {
792 event->ignore();
793 return;
794 }
795 QTreeWidget::dropEvent(event);
796 }
797};
798
799/** Elide long values in the Value column (plan: Qt::ElideMiddle). */
800class WatchValueColumnDelegate : public QStyledItemDelegate
801{
802 public:
803 using QStyledItemDelegate::QStyledItemDelegate;
804
805 void paint(QPainter *painter, const QStyleOptionViewItem &option,
806 const QModelIndex &index) const override
807 {
808 QStyleOptionViewItem opt = option;
809 initStyleOption(&opt, index);
810 const QString full = index.data(Qt::DisplayRole).toString();
811 const int avail = qMax(opt.rect.width() - 8, 1);
812 opt.text = opt.fontMetrics.elidedText(full, Qt::ElideMiddle, avail);
813 const QWidget *w = opt.widget;
814 QStyle *style = w ? w->style() : QApplication::style();
815 style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, w);
816 }
817
818 /* The Value column is read-only: block the default item editor so
819 * double-click / F2 cannot open a line edit here. The item's
820 * Qt::ItemIsEditable flag is kept because column 0 (the Watch spec)
821 * remains editable through WatchRootDelegate. */
822 QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
823 const QModelIndex &index) const override
824 {
825 Q_UNUSED(parent)(void)parent;;
826 Q_UNUSED(option)(void)option;;
827 Q_UNUSED(index)(void)index;;
828 return nullptr;
829 }
830};
831
832#if (QT_VERSION((6<<16)|(4<<8)|(2)) < QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0)))
833// QTreeWidget::itemFromIndex() is protected in Qt5 but public in Qt6. Use a
834// friend-style helper subclass to access it portably.
835class TreeWidgetItemFromIndex : public QTreeWidget
836{
837 public:
838 using QTreeWidget::itemFromIndex;
839};
840
841static inline QTreeWidgetItem *
842itemFromIndexHelper(const QTreeWidget *tree, const QModelIndex &index)
843{
844 return static_cast<const TreeWidgetItemFromIndex *>(tree)->itemFromIndex(
845 index);
846}
847#endif
848
849class WatchRootDelegate : public QStyledItemDelegate
850{
851 public:
852 WatchRootDelegate(QTreeWidget *tree, LuaDebuggerDialog *dialog,
853 QObject *parent = nullptr)
854 : QStyledItemDelegate(parent), tree_(tree), dialog_(dialog)
855 {
856 }
857
858 QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
859 const QModelIndex &index) const override;
860 void setEditorData(QWidget *editor, const QModelIndex &index) const override;
861 void setModelData(QWidget *editor, QAbstractItemModel *model,
862 const QModelIndex &index) const override;
863
864 private:
865 QTreeWidget *tree_;
866 LuaDebuggerDialog *dialog_;
867};
868
869QWidget *
870WatchRootDelegate::createEditor(QWidget *parent,
871 const QStyleOptionViewItem &option,
872 const QModelIndex &index) const
873{
874 Q_UNUSED(option)(void)option;;
875 if (!tree_ || !index.isValid() || index.column() != 0)
876 {
877 return nullptr;
878 }
879#if (QT_VERSION((6<<16)|(4<<8)|(2)) >= QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0)))
880 QTreeWidgetItem *it = tree_->itemFromIndex(index);
881#else
882 QTreeWidgetItem *it = itemFromIndexHelper(tree_, index);
883#endif
884 if (!it || it->parent() != nullptr)
885 {
886 return nullptr;
887 }
888 return new QLineEdit(parent);
889}
890
891void WatchRootDelegate::setEditorData(QWidget *editor,
892 const QModelIndex &index) const
893{
894 auto *le = qobject_cast<QLineEdit *>(editor);
895 if (!le || !tree_)
896 {
897 return;
898 }
899#if (QT_VERSION((6<<16)|(4<<8)|(2)) >= QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0)))
900 QTreeWidgetItem *it = tree_->itemFromIndex(index);
901#else
902 QTreeWidgetItem *it = itemFromIndexHelper(tree_, index);
903#endif
904 if (!it)
905 {
906 return;
907 }
908 QString s = it->data(0, WatchSpecRole).toString();
909 if (s.isEmpty())
910 {
911 s = it->text(0);
912 }
913 le->setText(s);
914}
915
916void WatchRootDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
917 const QModelIndex &index) const
918{
919 Q_UNUSED(model)(void)model;;
920 auto *le = qobject_cast<QLineEdit *>(editor);
921 if (!le || !dialog_ || !tree_)
922 {
923 return;
924 }
925#if (QT_VERSION((6<<16)|(4<<8)|(2)) >= QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0)))
926 QTreeWidgetItem *it = tree_->itemFromIndex(index);
927#else
928 QTreeWidgetItem *it = itemFromIndexHelper(tree_, index);
929#endif
930 if (!it)
931 {
932 return;
933 }
934 dialog_->commitWatchRootSpec(it, le->text());
935}
936
937} // namespace
938
939LuaDebuggerDialog::LuaDebuggerDialog(QWidget *parent)
940 : GeometryStateDialog(parent), ui(new Ui::LuaDebuggerDialog),
941 eventLoop(nullptr), enabledCheckBox(nullptr), breakpointTabsPrimed(false),
942 debuggerPaused(false), reloadDeferred(false), variablesSection(nullptr),
943 watchSection(nullptr), stackSection(nullptr), breakpointsSection(nullptr),
944 filesSection(nullptr), evalSection(nullptr), settingsSection(nullptr),
945 variablesTree(nullptr), watchTree(nullptr), stackTree(nullptr),
946 fileTree(nullptr), breakpointsTree(nullptr),
947 evalInputEdit(nullptr), evalOutputEdit(nullptr), evalButton(nullptr),
948 evalClearButton(nullptr), themeComboBox(nullptr)
949{
950 _instance = this;
951 setAttribute(Qt::WA_DeleteOnClose);
952 ui->setupUi(this);
953 loadGeometry();
954
955 lastOpenDirectory =
956 QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
957 if (lastOpenDirectory.isEmpty())
958 {
959 lastOpenDirectory = QDir::homePath();
960 }
961
962 // Create collapsible sections with their content widgets
963 createCollapsibleSections();
964
965 fileTree->setRootIsDecorated(true);
966 fileTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
967 fileTree->header()->setStretchLastSection(true);
968 fileTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
969
970 // Compact toolbar styling with consistent icons
971 ui->toolBar->setIconSize(QSize(18, 18));
972 ui->toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);
973 ui->toolBar->setStyleSheet(QStringLiteral((QString(QtPrivate::qMakeStringPrivate(u"" "QToolBar {" " background-color: palette(window);"
" border: none;" " spacing: 4px;" " padding: 2px 4px;" "}"
)))
974 "QToolBar {"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolBar {" " background-color: palette(window);"
" border: none;" " spacing: 4px;" " padding: 2px 4px;" "}"
)))
975 " background-color: palette(window);"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolBar {" " background-color: palette(window);"
" border: none;" " spacing: 4px;" " padding: 2px 4px;" "}"
)))
976 " border: none;"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolBar {" " background-color: palette(window);"
" border: none;" " spacing: 4px;" " padding: 2px 4px;" "}"
)))
977 " spacing: 4px;"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolBar {" " background-color: palette(window);"
" border: none;" " spacing: 4px;" " padding: 2px 4px;" "}"
)))
978 " padding: 2px 4px;"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolBar {" " background-color: palette(window);"
" border: none;" " spacing: 4px;" " padding: 2px 4px;" "}"
)))
979 "}")(QString(QtPrivate::qMakeStringPrivate(u"" "QToolBar {" " background-color: palette(window);"
" border: none;" " spacing: 4px;" " padding: 2px 4px;" "}"
)))
);
980 ui->actionOpenFile->setIcon(StockIcon("document-open"));
981 ui->actionSaveFile->setIcon(
982 style()->standardIcon(QStyle::SP_DialogSaveButton));
983 ui->actionContinue->setIcon(StockIcon("x-lua-debug-continue"));
984 ui->actionStepOver->setIcon(StockIcon("x-lua-debug-step-over"));
985 ui->actionStepIn->setIcon(StockIcon("x-lua-debug-step-in"));
986 ui->actionStepOut->setIcon(StockIcon("x-lua-debug-step-out"));
987 ui->actionReloadLuaPlugins->setIcon(StockIcon("view-refresh"));
988 ui->actionClearBreakpoints->setIcon(StockIcon("edit-clear"));
989 {
990 QIcon addWatchIcon = QIcon::fromTheme(QStringLiteral("system-search")(QString(QtPrivate::qMakeStringPrivate(u"" "system-search"))));
991 if (addWatchIcon.isNull())
992 {
993 addWatchIcon = QIcon::fromTheme(QStringLiteral("list-add")(QString(QtPrivate::qMakeStringPrivate(u"" "list-add"))));
994 }
995 if (addWatchIcon.isNull())
996 {
997 addWatchIcon = style()->standardIcon(
998 QStyle::SP_FileDialogDetailedView);
999 }
1000 ui->actionAddWatch->setIcon(addWatchIcon);
1001 }
1002 ui->actionFind->setIcon(StockIcon("edit-find"));
1003 ui->actionOpenFile->setToolTip(tr("Open Lua Script"));
1004 ui->actionSaveFile->setToolTip(tr("Save (%1)").arg(
1005 QKeySequence(QKeySequence::Save)
1006 .toString(QKeySequence::NativeText)));
1007 ui->actionContinue->setToolTip(tr("Continue execution (F5)"));
1008 ui->actionStepOver->setToolTip(tr("Step over (F10)"));
1009 ui->actionStepIn->setToolTip(tr("Step into (F11)"));
1010 ui->actionStepOut->setToolTip(tr("Step out (Shift+F11)"));
1011 ui->actionReloadLuaPlugins->setToolTip(
1012 tr("Reload Lua Plugins (Ctrl+Shift+L)"));
1013 ui->actionClearBreakpoints->setToolTip(tr("Remove all breakpoints"));
1014 ui->actionAddWatch->setToolTip(tr("%1 (%2)")
1015 .arg(ui->actionAddWatch->toolTip(),
1016 ui->actionAddWatch->shortcut()
1017 .toString(QKeySequence::NativeText)));
1018 ui->actionAddWatch->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1019 ui->actionFind->setToolTip(tr("Find in script (%1)")
1020 .arg(QKeySequence(QKeySequence::Find)
1021 .toString(QKeySequence::NativeText)));
1022 ui->actionGoToLine->setToolTip(tr("Go to line (%1)")
1023 .arg(QKeySequence(Qt::CTRL | Qt::Key_G)
1024 .toString(QKeySequence::NativeText)));
1025 ui->actionContinue->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1026 ui->actionStepOver->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1027 ui->actionStepIn->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1028 ui->actionStepOut->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1029 ui->actionReloadLuaPlugins->setShortcut(
1030 QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_L));
1031 ui->actionReloadLuaPlugins->setShortcutContext(
1032 Qt::WidgetWithChildrenShortcut);
1033 ui->actionSaveFile->setShortcut(QKeySequence::Save);
1034 ui->actionSaveFile->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1035 ui->actionFind->setShortcut(QKeySequence::Find);
1036 ui->actionFind->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1037 ui->actionGoToLine->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_G));
1038 ui->actionGoToLine->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1039 folderIcon = StockIcon("folder");
1040 fileIcon = StockIcon("text-x-generic");
1041
1042 // Toolbar controls - Checkbox for enable/disable
1043 // Order: Checkbox | Separator | Continue | Step Over/In/Out | Separator |
1044 // Open | Reload | Clear
1045 QAction *firstAction = ui->toolBar->actions().isEmpty()
1046 ? nullptr
1047 : ui->toolBar->actions().first();
1048
1049 // Enable/Disable checkbox with colored icon
1050 enabledCheckBox = new QCheckBox(ui->toolBar);
1051 enabledCheckBox->setChecked(wslua_debugger_is_enabled());
1052 ui->toolBar->insertWidget(firstAction, enabledCheckBox);
1053
1054 connect(enabledCheckBox, &QCheckBox::toggled, this,
1055 &LuaDebuggerDialog::onDebuggerToggled);
1056 connect(ui->actionContinue, &QAction::triggered, this,
1057 &LuaDebuggerDialog::onContinue);
1058 connect(ui->actionStepOver, &QAction::triggered, this,
1059 &LuaDebuggerDialog::onStepOver);
1060 connect(ui->actionStepIn, &QAction::triggered, this,
1061 &LuaDebuggerDialog::onStepIn);
1062 connect(ui->actionStepOut, &QAction::triggered, this,
1063 &LuaDebuggerDialog::onStepOut);
1064 connect(ui->actionClearBreakpoints, &QAction::triggered, this,
1065 &LuaDebuggerDialog::onClearBreakpoints);
1066 connect(ui->actionAddWatch, &QAction::triggered, this, [this]()
1067 { insertNewWatchRow(QString(), true); });
1068 connect(ui->actionOpenFile, &QAction::triggered, this,
1069 &LuaDebuggerDialog::onOpenFile);
1070 connect(ui->actionSaveFile, &QAction::triggered, this,
1071 &LuaDebuggerDialog::onSaveFile);
1072 connect(ui->actionFind, &QAction::triggered, this,
1073 &LuaDebuggerDialog::onEditorFind);
1074 connect(ui->actionGoToLine, &QAction::triggered, this,
1075 &LuaDebuggerDialog::onEditorGoToLine);
1076 connect(ui->actionReloadLuaPlugins, &QAction::triggered, this,
1077 &LuaDebuggerDialog::onReloadLuaPlugins);
1078 addAction(ui->actionContinue);
1079 addAction(ui->actionStepOver);
1080 addAction(ui->actionStepIn);
1081 addAction(ui->actionStepOut);
1082 addAction(ui->actionReloadLuaPlugins);
1083 addAction(ui->actionAddWatch);
1084 addAction(ui->actionSaveFile);
1085 addAction(ui->actionFind);
1086 addAction(ui->actionGoToLine);
1087
1088 ui->luaDebuggerFindFrame->hide();
1089 ui->luaDebuggerGoToLineFrame->hide();
1090
1091 // Tab Widget
1092 connect(ui->codeTabWidget, &QTabWidget::tabCloseRequested, this,
1093 &LuaDebuggerDialog::onCodeTabCloseRequested);
1094 connect(ui->codeTabWidget, &QTabWidget::currentChanged, this,
1095 [this](int)
1096 {
1097 updateSaveActionState();
1098 updateLuaEditorAuxFrames();
1099 });
1100
1101 // Breakpoints
1102 connect(breakpointsTree, &QTreeWidget::itemChanged, this,
1103 &LuaDebuggerDialog::onBreakpointItemChanged);
1104 connect(breakpointsTree, &QTreeWidget::itemClicked, this,
1105 &LuaDebuggerDialog::onBreakpointItemClicked);
1106 connect(breakpointsTree, &QTreeWidget::itemDoubleClicked, this,
1107 &LuaDebuggerDialog::onBreakpointItemDoubleClicked);
1108
1109 QHeaderView *breakpointHeader = breakpointsTree->header();
1110 breakpointHeader->setStretchLastSection(false);
1111 breakpointHeader->setSectionResizeMode(0, QHeaderView::ResizeToContents);
1112 breakpointHeader->setSectionResizeMode(1, QHeaderView::ResizeToContents);
1113 breakpointHeader->setSectionResizeMode(2, QHeaderView::Stretch);
1114 breakpointHeader->setSectionResizeMode(3, QHeaderView::ResizeToContents);
1115 breakpointsTree->headerItem()->setText(2, tr("Location"));
1116 breakpointsTree->setColumnHidden(1, true);
1117
1118 // Variables
1119 connect(variablesTree, &QTreeWidget::itemExpanded, this,
1120 &LuaDebuggerDialog::onVariableItemExpanded);
1121 connect(variablesTree, &QTreeWidget::itemCollapsed, this,
1122 &LuaDebuggerDialog::onVariableItemCollapsed);
1123 variablesTree->setContextMenuPolicy(Qt::CustomContextMenu);
1124 connect(variablesTree, &QTreeWidget::customContextMenuRequested, this,
1125 &LuaDebuggerDialog::onVariablesContextMenuRequested);
1126
1127 /* onWatchItemExpanded updates watchExpansion_ (the runtime expansion
1128 * map) and performs lazy child fill; onWatchItemCollapsed mirrors the
1129 * update on collapse. No storeWatchList() call is needed on expand /
1130 * collapse because expansion state is intentionally not persisted to
1131 * lua_debugger.json. */
1132 connect(watchTree, &QTreeWidget::itemExpanded, this,
1133 &LuaDebuggerDialog::onWatchItemExpanded);
1134 connect(watchTree, &QTreeWidget::itemCollapsed, this,
1135 &LuaDebuggerDialog::onWatchItemCollapsed);
1136 watchTree->setContextMenuPolicy(Qt::CustomContextMenu);
1137 connect(watchTree, &QTreeWidget::customContextMenuRequested, this,
1138 &LuaDebuggerDialog::onWatchContextMenuRequested);
1139 watchTree->setItemDelegateForColumn(
1140 0, new WatchRootDelegate(watchTree, this, watchTree));
1141 watchTree->setItemDelegateForColumn(
1142 1, new WatchValueColumnDelegate(watchTree));
1143 watchTree->viewport()->installEventFilter(this);
1144
1145 connect(watchTree, &QTreeWidget::currentItemChanged, this,
1146 &LuaDebuggerDialog::onWatchCurrentItemChanged);
1147 connect(variablesTree, &QTreeWidget::currentItemChanged, this,
1148 &LuaDebuggerDialog::onVariablesCurrentItemChanged);
1149
1150 // Files
1151 connect(fileTree, &QTreeWidget::itemDoubleClicked,
1152 [this](QTreeWidgetItem *item, int column)
1153 {
1154 Q_UNUSED(column)(void)column;;
1155 if (!item || item->data(0, FileTreeIsDirectoryRole).toBool())
1156 {
1157 return;
1158 }
1159 const QString path = item->data(0, FileTreePathRole).toString();
1160 if (!path.isEmpty())
1161 {
1162 loadFile(path);
1163 }
1164 });
1165
1166 connect(stackTree, &QTreeWidget::itemDoubleClicked, this,
1167 &LuaDebuggerDialog::onStackItemDoubleClicked);
1168 connect(stackTree, &QTreeWidget::currentItemChanged, this,
1169 &LuaDebuggerDialog::onStackCurrentItemChanged);
1170
1171 // Evaluate panel
1172 connect(evalButton, &QPushButton::clicked, this,
1173 &LuaDebuggerDialog::onEvaluate);
1174 connect(evalClearButton, &QPushButton::clicked, this,
1175 &LuaDebuggerDialog::onEvalClear);
1176
1177 configureVariablesTreeColumns();
1178 configureWatchTreeColumns();
1179 configureStackTreeColumns();
1180 applyMonospaceFonts();
1181
1182 /*
1183 * Register our reload callback with the debugger core.
1184 * This callback is invoked by wslua_reload_plugins() BEFORE
1185 * Lua scripts are reloaded, allowing us to refresh our cached
1186 * script content from disk.
1187 */
1188 wslua_debugger_register_reload_callback(
1189 LuaDebuggerDialog::onLuaReloadCallback);
1190
1191 /*
1192 * Register a callback to be notified AFTER Lua plugins are reloaded.
1193 * This allows us to refresh the file tree with newly loaded scripts.
1194 */
1195 wslua_debugger_register_post_reload_callback(
1196 LuaDebuggerDialog::onLuaPostReloadCallback);
1197
1198 /*
1199 * Register a callback to be notified when a Lua script is loaded.
1200 * This allows us to add the script to the file tree immediately.
1201 */
1202 wslua_debugger_register_script_loaded_callback(
1203 LuaDebuggerDialog::onScriptLoadedCallback);
1204
1205 if (mainApp)
1206 {
1207 connect(mainApp, &MainApplication::zoomMonospaceFont, this,
1208 &LuaDebuggerDialog::onMonospaceFontUpdated, Qt::UniqueConnection);
1209 connect(mainApp, &MainApplication::appInitialized, this,
1210 &LuaDebuggerDialog::onMainAppInitialized, Qt::UniqueConnection);
1211 connect(mainApp, &MainApplication::preferencesChanged, this,
1212 &LuaDebuggerDialog::onPreferencesChanged, Qt::UniqueConnection);
1213 /*
1214 * Connect to colorsChanged signal to update code view themes when
1215 * Wireshark's color scheme changes. This is important when the debugger
1216 * theme preference is set to "Auto (follow color scheme)".
1217 */
1218 connect(mainApp, &MainApplication::colorsChanged, this,
1219 &LuaDebuggerDialog::onColorsChanged, Qt::UniqueConnection);
1220 if (mainApp->isInitialized())
1221 {
1222 onMainAppInitialized();
1223 }
1224 }
1225
1226 refreshAvailableScripts();
1227 syncDebuggerToggleWithCore();
1228 updateWidgets();
1229
1230 /*
1231 * Apply all settings from JSON file (theme, font, sections, splitters,
1232 * breakpoints). This is done after all widgets are created.
1233 */
1234 applyDialogSettings();
1235 updateBreakpoints();
1236 updateSaveActionState();
1237 updateLuaEditorAuxFrames();
1238
1239 installDescendantShortcutFilters();
1240}
1241
1242LuaDebuggerDialog::~LuaDebuggerDialog()
1243{
1244 /*
1245 * Persist JSON only from closeEvent(); if the dialog is destroyed without
1246 * a normal close (rare), flush once here.
1247 */
1248 if (!luaDebuggerJsonSaved_)
1249 {
1250 storeDialogSettings();
1251 saveSettingsFile();
1252 }
1253
1254 /*
1255 * Unregister our reload callbacks when the dialog is destroyed.
1256 */
1257 wslua_debugger_register_reload_callback(NULL__null);
1258 wslua_debugger_register_post_reload_callback(NULL__null);
1259 wslua_debugger_register_script_loaded_callback(NULL__null);
1260
1261 delete ui;
1262 _instance = nullptr;
1263}
1264
1265void LuaDebuggerDialog::createCollapsibleSections()
1266{
1267 QSplitter *splitter = ui->leftSplitter;
1268
1269 // --- Variables Section ---
1270 variablesSection = new CollapsibleSection(tr("Variables"), this);
1271 variablesSection->setToolTip(
1272 tr("<p><b>Locals</b><br/>"
1273 "Parameters and local variables for the selected stack frame.</p>"
1274 "<p><b>Upvalues</b><br/>"
1275 "Outer variables that this function actually uses from surrounding code. "
1276 "Anything the function does not reference does not appear here.</p>"
1277 "<p><b>Globals</b><br/>"
1278 "Names from the global environment table.</p>"));
1279 variablesTree = new QTreeWidget();
1280 variablesTree->setColumnCount(3);
1281 variablesTree->setHeaderLabels({tr("Name"), tr("Value"), tr("Type")});
1282 variablesTree->setUniformRowHeights(true);
1283 variablesTree->setWordWrap(false);
1284 variablesSection->setContentWidget(variablesTree);
1285 variablesSection->setExpanded(true);
1286 splitter->addWidget(variablesSection);
1287
1288 /*
1289 * Watch panel: two columns; formats, expansion persistence, depth cap
1290 * WSLUA_WATCH_MAX_PATH_SEGMENTS, drag reorder, error styling, muted em dash
1291 * when no live value.
1292 */
1293 // --- Watch Section ---
1294 watchSection = new CollapsibleSection(tr("Watch"), this);
1295 watchSection->setToolTip(
1296 tr("<p>Each row is a <b>Variables-tree path</b>, not a Lua "
1297 "expression. Accepted forms:</p>"
1298 "<ul>"
1299 "<li>Section-qualified: <code>Locals.<i>name</i></code>, "
1300 "<code>Upvalues.<i>name</i></code>, "
1301 "<code>Globals.<i>name</i></code>.</li>"
1302 "<li>Section root alone: <code>Locals</code>, "
1303 "<code>Upvalues</code>, <code>Globals</code> "
1304 "(<code>_G</code> is an alias for <code>Globals</code>).</li>"
1305 "<li>Unqualified name: resolved in "
1306 "<b>Locals &rarr; Upvalues &rarr; Globals</b> order; the row "
1307 "tooltip shows which section matched.</li>"
1308 "</ul>"
1309 "<p>After the first segment, chain <code>.field</code> or "
1310 "bracket keys &mdash; integer "
1311 "(<code>[1]</code>, <code>[-1]</code>, <code>[0x1F]</code>), "
1312 "boolean (<code>[true]</code>), or short-literal string "
1313 "(<code>[\"key\"]</code>, <code>['k']</code>). Depth is capped "
1314 "at 32 segments. Use the <b>Evaluate</b> panel below for "
1315 "arbitrary Lua expressions.</p>"
1316 "<p>Values are only read while the debugger is "
1317 "<b>paused</b>; otherwise the Value column shows a muted "
1318 "em dash. Child values that changed since the previous pause "
1319 "are shown in <b>bold</b>.</p>"
1320 "<p>Double-click or press <b>F2</b> to edit a row; "
1321 "<b>Delete</b> removes it; drag rows to reorder.</p>"));
1322 watchTree = new WatchTreeWidget(this);
1323 watchTree->setColumnCount(2);
1324 watchTree->setHeaderLabels({tr("Watch"), tr("Value")});
1325 watchTree->setRootIsDecorated(true);
1326 watchTree->setDragDropMode(QAbstractItemView::InternalMove);
1327 watchTree->setDefaultDropAction(Qt::MoveAction);
1328 watchTree->setSelectionMode(QAbstractItemView::ExtendedSelection);
1329 watchTree->setUniformRowHeights(true);
1330 watchTree->setWordWrap(false);
1331 {
1332 auto *watchWrap = new QWidget();
1333 auto *watchOuter = new QVBoxLayout(watchWrap);
1334 watchOuter->setContentsMargins(0, 0, 0, 0);
1335 watchOuter->setSpacing(4);
1336 watchOuter->addWidget(watchTree, 1);
1337 watchSection->setContentWidget(watchWrap);
1338 }
1339 watchSection->setExpanded(true);
1340 splitter->addWidget(watchSection);
1341
1342 // --- Stack Trace Section ---
1343 stackSection = new CollapsibleSection(tr("Stack Trace"), this);
1344 stackTree = new QTreeWidget();
1345 stackTree->setColumnCount(2);
1346 stackTree->setHeaderLabels({tr("Function"), tr("Location")});
1347 stackTree->setRootIsDecorated(true);
1348 stackTree->setToolTip(
1349 tr("Select a row to inspect locals and upvalues for that frame. "
1350 "Double-click a Lua frame to open its source location."));
1351 stackSection->setContentWidget(stackTree);
1352 stackSection->setExpanded(true);
1353 splitter->addWidget(stackSection);
1354
1355 // --- Breakpoints Section ---
1356 breakpointsSection = new CollapsibleSection(tr("Breakpoints"), this);
1357 breakpointsTree = new QTreeWidget();
1358 breakpointsTree->setColumnCount(4);
1359 breakpointsTree->setHeaderLabels(
1360 {tr("Active"), tr("Line"), tr("File"), QString()});
1361 breakpointsTree->setRootIsDecorated(false);
1362 breakpointsSection->setContentWidget(breakpointsTree);
1363 breakpointsSection->setExpanded(true);
1364 splitter->addWidget(breakpointsSection);
1365
1366 // --- Files Section ---
1367 filesSection = new CollapsibleSection(tr("Files"), this);
1368 fileTree = new QTreeWidget();
1369 fileTree->setColumnCount(1);
1370 fileTree->setHeaderLabels({tr("Files")});
1371 fileTree->setRootIsDecorated(false);
1372 filesSection->setContentWidget(fileTree);
1373 filesSection->setExpanded(true);
1374 splitter->addWidget(filesSection);
1375
1376 // --- Evaluate Section ---
1377 evalSection = new CollapsibleSection(tr("Evaluate"), this);
1378 QWidget *evalWidget = new QWidget();
1379 QVBoxLayout *evalMainLayout = new QVBoxLayout(evalWidget);
1380 evalMainLayout->setContentsMargins(0, 0, 0, 0);
1381 evalMainLayout->setSpacing(4);
1382
1383 QSplitter *evalSplitter = new QSplitter(Qt::Vertical);
1384 evalInputEdit = new QPlainTextEdit();
1385 evalInputEdit->setPlaceholderText(
1386 tr("Enter Lua expression (prefix with = to return value)"));
1387 evalInputEdit->setToolTip(
1388 tr("<b>Lua Expression Evaluation</b><br><br>"
1389 "Code is executed using <code>lua_pcall()</code> in a protected "
1390 "environment. "
1391 "Runtime errors are caught and displayed in the output.<br><br>"
1392 "<b>Prefix with <code>=</code></b> to return a value (e.g., "
1393 "<code>=my_var</code>).<br><br>"
1394 "<b>What works:</b><ul>"
1395 "<li>Read/modify global variables (<code>_G.x = 42</code>)</li>"
1396 "<li>Modify table contents (<code>my_table.field = 99</code>)</li>"
1397 "<li>Call functions and inspect return values</li>"
1398 "</ul>"
1399 "<b>Limitations:</b><ul>"
1400 "<li>Local variables cannot be modified directly (use "
1401 "<code>debug.setlocal()</code>)</li>"
1402 "<li>Long-running expressions are automatically aborted</li>"
1403 "<li><b>Warning:</b> Changes to globals persist and can affect "
1404 "ongoing dissection</li>"
1405 "</ul>"));
1406 evalOutputEdit = new QPlainTextEdit();
1407 evalOutputEdit->setReadOnly(true);
1408 evalOutputEdit->setPlaceholderText(tr("Output"));
1409 evalSplitter->addWidget(evalInputEdit);
1410 evalSplitter->addWidget(evalOutputEdit);
1411 evalMainLayout->addWidget(evalSplitter, 1);
1412
1413 QHBoxLayout *evalButtonLayout = new QHBoxLayout();
1414 evalButton = new QPushButton(tr("Evaluate"));
1415 evalButton->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Return));
1416 evalButton->setToolTip(tr("Execute the Lua code (Ctrl+Return)"));
1417 evalClearButton = new QPushButton(tr("Clear"));
1418 evalClearButton->setToolTip(tr("Clear input and output"));
1419 evalButtonLayout->addWidget(evalButton);
1420 evalButtonLayout->addWidget(evalClearButton);
1421 evalButtonLayout->addStretch();
1422 evalMainLayout->addLayout(evalButtonLayout);
1423
1424 evalSection->setContentWidget(evalWidget);
1425 evalSection->setExpanded(false);
1426 splitter->addWidget(evalSection);
1427
1428 // --- Settings Section ---
1429 settingsSection = new CollapsibleSection(tr("Settings"), this);
1430 QWidget *settingsWidget = new QWidget();
1431 QFormLayout *settingsLayout = new QFormLayout(settingsWidget);
1432 settingsLayout->setContentsMargins(4, 4, 4, 4);
1433 settingsLayout->setSpacing(6);
1434
1435 themeComboBox = new QComboBox();
1436 themeComboBox->addItem(tr("Auto (follow color scheme)"),
1437 WSLUA_DEBUGGER_THEME_AUTO);
1438 themeComboBox->addItem(tr("Dark"), WSLUA_DEBUGGER_THEME_DARK);
1439 themeComboBox->addItem(tr("Light"), WSLUA_DEBUGGER_THEME_LIGHT);
1440 themeComboBox->setToolTip(tr("Color theme for the code editor"));
1441 // Theme will be set by applyDialogSettings() later
1442 settingsLayout->addRow(tr("Code View Theme:"), themeComboBox);
1443
1444 connect(themeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
1445 this, &LuaDebuggerDialog::onThemeChanged);
1446
1447 settingsSection->setContentWidget(settingsWidget);
1448 settingsSection->setExpanded(false);
1449 splitter->addWidget(settingsSection);
1450
1451 // Set initial sizes - expanded sections get more space
1452 QList<int> sizes;
1453 int headerH = variablesSection->headerHeight();
1454 sizes << 120 << 70 << 100 << headerH << 80 << headerH
1455 << headerH; // Variables, Watch, Stack, Files(collapsed), Breakpoints,
1456 // Eval(collapsed), Settings(collapsed)
1457 splitter->setSizes(sizes);
1458}
1459
1460LuaDebuggerDialog *LuaDebuggerDialog::instance(QWidget *parent)
1461{
1462 if (!_instance)
1463 {
1464 QWidget *resolved_parent = parent;
1465 if (!resolved_parent && mainApp && mainApp->isInitialized())
1466 {
1467 resolved_parent = mainApp->mainWindow();
1468 }
1469 new LuaDebuggerDialog(resolved_parent);
1470 }
1471 return _instance;
1472}
1473
1474void LuaDebuggerDialog::handlePause(const char *file_path, int64_t line)
1475{
1476 // Prevent deletion while in event loop
1477 setAttribute(Qt::WA_DeleteOnClose, false);
1478
1479 // Bring to front
1480 show();
1481 raise();
1482 activateWindow();
1483
1484 QString normalizedPath = normalizedFilePath(QString::fromUtf8(file_path));
1485 ensureFileTreeEntry(normalizedPath);
1486 LuaDebuggerCodeView *view = loadFile(normalizedPath);
1487 if (view)
1488 {
1489 view->setCurrentLine(static_cast<qint32>(line));
1490 }
1491
1492 debuggerPaused = true;
1493 updateWidgets();
1494
1495 stackSelectionLevel = 0;
1496 updateStack();
1497 variablesTree->clear();
1498 updateVariables(nullptr, QString());
1499 restoreVariablesExpansionState();
1500 refreshWatchDisplay();
1501
1502 /*
1503 * If an event loop is already running (e.g. we were called from a step
1504 * action which triggered an immediate re-pause), reuse it instead of nesting.
1505 * The outer loop.exec() is still on the stack and will return when we
1506 * eventually quit it via Continue or close.
1507 */
1508 if (eventLoop)
1509 {
1510 return;
1511 }
1512
1513 QEventLoop loop;
1514 eventLoop = &loop;
1515
1516 /*
1517 * If the parent window is destroyed while we're paused (e.g. the
1518 * application is shutting down), quit the event loop so the Lua
1519 * call stack can unwind cleanly.
1520 */
1521 QPointer<QWidget> parentGuard(parentWidget());
1522 QMetaObject::Connection parentConn;
1523 if (parentGuard) {
1524 parentConn = connect(parentGuard, &QObject::destroyed, &loop,
1525 &QEventLoop::quit);
1526 }
1527
1528 // Enter event loop - blocks until Continue or dialog close
1529 loop.exec();
1530
1531 if (parentConn) {
1532 disconnect(parentConn);
1533 }
1534
1535 // Restore delete-on-close behavior and clear event loop pointer
1536 eventLoop = nullptr;
1537 setAttribute(Qt::WA_DeleteOnClose, true);
1538
1539 /*
1540 * If a Lua plugin reload was requested while we were paused,
1541 * schedule it now that the Lua/C call stack has fully unwound.
1542 * We must NOT schedule it from inside the event loop (via
1543 * QTimer::singleShot) because the timer can fire before the
1544 * loop exits, running cf_close/wslua_reload_plugins while
1545 * cf_read is still on the C call stack.
1546 */
1547 if (reloadDeferred) {
1548 reloadDeferred = false;
1549 if (mainApp) {
1550 mainApp->reloadLuaPluginsDelayed();
1551 }
1552 }
1553}
1554
1555void LuaDebuggerDialog::onContinue()
1556{
1557 resumeDebuggerAndExitLoop();
1558 updateWidgets();
1559}
1560
1561void LuaDebuggerDialog::runDebuggerStep(void (*step_fn)(void))
1562{
1563 if (!debuggerPaused)
1564 {
1565 return;
1566 }
1567
1568 debuggerPaused = false;
1569 clearPausedStateUi();
1570
1571 /*
1572 * The step function resumes the VM and may synchronously hit handlePause()
1573 * again. handlePause() detects that eventLoop is already set and reuses
1574 * it instead of nesting a new one — so the stack does NOT grow with each
1575 * step.
1576 */
1577 step_fn();
1578
1579 /*
1580 * If handlePause() was NOT called (e.g. step landed in C code
1581 * and the hook didn't fire), we need to quit the event loop so
1582 * the original handlePause() caller can return.
1583 */
1584 if (!debuggerPaused && eventLoop)
1585 {
1586 eventLoop->quit();
1587 }
1588
1589 updateWidgets();
1590}
1591
1592void LuaDebuggerDialog::onStepOver()
1593{
1594 runDebuggerStep(wslua_debugger_step_over);
1595}
1596
1597void LuaDebuggerDialog::onStepIn()
1598{
1599 runDebuggerStep(wslua_debugger_step_in);
1600}
1601
1602void LuaDebuggerDialog::onStepOut()
1603{
1604 runDebuggerStep(wslua_debugger_step_out);
1605}
1606
1607void LuaDebuggerDialog::onDebuggerToggled(bool checked)
1608{
1609 if (!checked && debuggerPaused)
1610 {
1611 onContinue();
1612 }
1613 wslua_debugger_set_enabled(checked);
1614 if (!checked)
1615 {
1616 debuggerPaused = false;
1617 clearPausedStateUi();
1618 }
1619 updateWidgets();
1620}
1621
1622void LuaDebuggerDialog::reject()
1623{
1624 /* Base QDialog::reject() calls done(Rejected), which hides() without
1625 * delivering QCloseEvent, so our closeEvent() unsaved-scripts check does
1626 * not run (e.g. Esc). Synchronous close() from keyPressEvent → reject()
1627 * can fail to finish closing; queue close() so closeEvent() runs on the
1628 * next event-loop turn (same path as the window close control). */
1629 QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection);
1630}
1631
1632void LuaDebuggerDialog::closeEvent(QCloseEvent *event)
1633{
1634 if (!ensureUnsavedChangesHandled(tr("Lua Debugger")))
1635 {
1636 event->ignore();
1637 return;
1638 }
1639
1640 storeDialogSettings();
1641 saveSettingsFile();
1642 luaDebuggerJsonSaved_ = true;
1643
1644 /* Disable the debugger so breakpoints won't fire and reopen the
1645 * dialog after it has been closed. */
1646 wslua_debugger_set_enabled(false);
1647 resumeDebuggerAndExitLoop();
1648
1649 /*
1650 * Do not call QDialog::closeEvent (GeometryStateDialog inherits it):
1651 * QDialog::closeEvent invokes reject(); our reject() queues close()
1652 * asynchronously, so the dialog stays visible and Qt then ignores the
1653 * close event (see qdialog.cpp: if (that && isVisible()) e->ignore()).
1654 * QWidget::closeEvent only accepts the event so the window can close.
1655 */
1656 QWidget::closeEvent(event);
1657}
1658
1659void LuaDebuggerDialog::handleEscapeKey()
1660{
1661 QWidget *const modal = QApplication::activeModalWidget();
1662 if (modal && modal != this)
1663 {
1664 return;
1665 }
1666 if (ui->luaDebuggerFindFrame->isVisible())
1667 {
1668 ui->luaDebuggerFindFrame->animatedHide();
1669 return;
1670 }
1671 if (ui->luaDebuggerGoToLineFrame->isVisible())
1672 {
1673 ui->luaDebuggerGoToLineFrame->animatedHide();
1674 return;
1675 }
1676 QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection);
1677}
1678
1679void LuaDebuggerDialog::installDescendantShortcutFilters()
1680{
1681 installEventFilter(this);
1682 for (QWidget *w : findChildren<QWidget *>())
1683 {
1684 w->installEventFilter(this);
1685 }
1686}
1687
1688void LuaDebuggerDialog::childEvent(QChildEvent *event)
1689{
1690 if (event->added())
1691 {
1692 if (auto *w = qobject_cast<QWidget *>(event->child()))
1693 {
1694 w->installEventFilter(this);
1695 for (QWidget *d : w->findChildren<QWidget *>())
1696 {
1697 d->installEventFilter(this);
1698 }
1699 }
1700 }
1701 QDialog::childEvent(event);
1702}
1703
1704bool LuaDebuggerDialog::eventFilter(QObject *obj, QEvent *event)
1705{
1706 QWidget *const receiver = qobject_cast<QWidget *>(obj);
1707 const bool inDebuggerUi =
1708 receiver && isVisible() && isAncestorOf(receiver);
1709
1710 if (watchTree && obj == watchTree->viewport() &&
1711 event->type() == QEvent::MouseButtonDblClick)
1712 {
1713 auto *me = static_cast<QMouseEvent *>(event);
1714 if (me->button() == Qt::LeftButton && !watchTree->itemAt(me->pos()))
1715 {
1716 insertNewWatchRow(QString(), true);
1717 return true;
1718 }
1719 }
1720
1721 if (inDebuggerUi && event->type() == QEvent::ShortcutOverride)
1722 {
1723 auto *ke = static_cast<QKeyEvent *>(event);
1724 const QKeySequence pressed = luaSeqFromKeyEvent(ke);
1725 if (matchesLuaDebuggerShortcutKeys(ui, pressed))
1726 {
1727 ke->accept();
1728 return false;
1729 }
1730 }
1731
1732 if (inDebuggerUi && event->type() == QEvent::KeyPress)
1733 {
1734 auto *ke = static_cast<QKeyEvent *>(event);
1735 if (watchTree && (obj == watchTree || obj == watchTree->viewport()))
1736 {
1737 if (watchTree->hasFocus() ||
1738 (watchTree->viewport() && watchTree->viewport()->hasFocus()))
1739 {
1740 QTreeWidgetItem *cur = watchTree->currentItem();
1741 if (cur && cur->parent() == nullptr)
1742 {
1743 if ((ke->key() == Qt::Key_Delete ||
1744 ke->key() == Qt::Key_Backspace) &&
1745 ke->modifiers() == Qt::NoModifier)
1746 {
1747 QList<QTreeWidgetItem *> del;
1748 for (QTreeWidgetItem *it : watchTree->selectedItems())
1749 {
1750 if (it && it->parent() == nullptr)
1751 {
1752 del.append(it);
1753 }
1754 }
1755 if (del.isEmpty())
1756 {
1757 del.append(cur);
1758 }
1759 deleteWatchRows(del);
1760 return true;
1761 }
1762 if (ke->key() == Qt::Key_F2 &&
1763 ke->modifiers() == Qt::NoModifier)
1764 {
1765 watchTree->editItem(cur, 0);
1766 return true;
1767 }
1768 }
1769 }
1770 }
1771 /*
1772 * Esc must be handled here: QPlainTextEdit accepts Key_Escape without
1773 * propagating to QDialog::keyPressEvent, so reject() never runs.
1774 * Dismiss inline find/go bars first; then queue close() so closeEvent()
1775 * runs (unsaved-scripts prompt). Skip if a different modal dialog owns
1776 * the event (e.g. nested prompt).
1777 */
1778 if (ke->key() == Qt::Key_Escape && ke->modifiers() == Qt::NoModifier)
1779 {
1780 QWidget *const modal = QApplication::activeModalWidget();
1781 if (modal && modal != this)
1782 {
1783 return QDialog::eventFilter(obj, event);
1784 }
1785 handleEscapeKey();
1786 return true;
1787 }
1788 const QKeySequence pressed = luaSeqFromKeyEvent(ke);
1789 if (triggerLuaDebuggerShortcuts(ui, pressed))
1790 {
1791 return true;
1792 }
1793 }
1794 return QDialog::eventFilter(obj, event);
1795}
1796
1797void LuaDebuggerDialog::onClearBreakpoints()
1798{
1799 // Confirmation dialog
1800 const unsigned count = wslua_debugger_get_breakpoint_count();
1801 if (count == 0)
1802 {
1803 return;
1804 }
1805
1806 QMessageBox::StandardButton reply = QMessageBox::question(
1807 this, tr("Clear All Breakpoints"),
1808 tr("Are you sure you want to remove %Ln breakpoint(s)?", "", count),
1809 QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
1810
1811 if (reply != QMessageBox::Yes)
1812 {
1813 return;
1814 }
1815
1816 wslua_debugger_clear_breakpoints();
1817 updateBreakpoints();
1818 const qint32 tabCount = static_cast<qint32>(ui->codeTabWidget->count());
1819 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1820 {
1821 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(
1822 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
1823 if (view)
1824 view->updateBreakpointMarkers();
1825 }
1826}
1827
1828void LuaDebuggerDialog::updateBreakpoints()
1829{
1830 breakpointsTree->clear();
1831 unsigned count = wslua_debugger_get_breakpoint_count();
1832 bool hasActiveBreakpoint = false;
1833 const bool collectInitialFiles = !breakpointTabsPrimed;
1834 QVector<QString> initialBreakpointFiles;
1835 QSet<QString> seenInitialFiles;
1836 for (unsigned i = 0; i < count; i++)
1837 {
1838 const char *file_path;
1839 int64_t line;
1840 bool active;
1841 if (wslua_debugger_get_breakpoint(i, &file_path, &line, &active))
1842 {
1843 QString normalizedPath =
1844 normalizedFilePath(QString::fromUtf8(file_path));
1845 QTreeWidgetItem *item = new QTreeWidgetItem(breakpointsTree);
1846
1847 /* Check if file exists */
1848 QFileInfo fileInfo(normalizedPath);
1849 bool fileExists = fileInfo.exists() && fileInfo.isFile();
1850
1851 item->setCheckState(0, active ? Qt::Checked : Qt::Unchecked);
1852 item->setData(0, BreakpointFileRole, normalizedPath);
1853 item->setData(0, BreakpointLineRole, static_cast<qlonglong>(line));
1854 item->setToolTip(0, tr("Enable or disable this breakpoint"));
1855 item->setText(1, QString::number(line));
1856 const QString fileDisplayName = fileInfo.fileName();
1857 QString locationText =
1858 QStringLiteral("%1:%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1:%2")))
1859 .arg(fileDisplayName.isEmpty() ? normalizedPath
1860 : fileDisplayName)
1861 .arg(line);
1862 item->setText(2, locationText);
1863 item->setTextAlignment(2, Qt::AlignLeft | Qt::AlignVCenter);
1864
1865 if (!fileExists)
1866 {
1867 /* Mark stale breakpoints with warning icon and gray text */
1868 item->setIcon(2, QIcon::fromTheme("dialog-warning"));
1869 item->setToolTip(2,
1870 tr("File not found: %1").arg(normalizedPath));
1871 item->setForeground(0, QBrush(Qt::gray));
1872 item->setForeground(1, QBrush(Qt::gray));
1873 item->setForeground(2, QBrush(Qt::gray));
1874 /* Disable the checkbox for stale breakpoints */
1875 item->setFlags(item->flags() & ~Qt::ItemIsUserCheckable);
1876 item->setCheckState(0, Qt::Unchecked);
1877 }
1878 else
1879 {
1880 item->setToolTip(
1881 2, tr("%1\nLine %2").arg(normalizedPath).arg(line));
1882 }
1883
1884 item->setIcon(3, QIcon::fromTheme("edit-delete"));
1885 item->setToolTip(3, tr("Remove this breakpoint"));
1886 if (active && fileExists)
1887 {
1888 hasActiveBreakpoint = true;
1889 }
1890
1891 /* Only add to file tree if file exists */
1892 if (fileExists)
1893 {
1894 ensureFileTreeEntry(normalizedPath);
1895 }
1896
1897 /* Only open existing files initially */
1898 if (collectInitialFiles && fileExists &&
1899 !seenInitialFiles.contains(normalizedPath))
1900 {
1901 initialBreakpointFiles.append(normalizedPath);
1902 seenInitialFiles.insert(normalizedPath);
1903 }
1904 }
1905 }
1906
1907 if (hasActiveBreakpoint)
1908 {
1909 ensureDebuggerEnabledForActiveBreakpoints();
1910 }
1911 syncDebuggerToggleWithCore();
1912
1913 if (collectInitialFiles)
1914 {
1915 breakpointTabsPrimed = true;
1916 openInitialBreakpointFiles(initialBreakpointFiles);
1917 }
1918}
1919
1920void LuaDebuggerDialog::updateStack()
1921{
1922 if (!stackTree)
1923 {
1924 return;
1925 }
1926
1927 const bool signalsWereBlocked = stackTree->blockSignals(true);
1928 stackTree->clear();
1929
1930 int32_t frameCount = 0;
1931 wslua_stack_frame_t *stack = wslua_debugger_get_stack(&frameCount);
1932 QTreeWidgetItem *itemToSelect = nullptr;
1933 if (stack && frameCount > 0)
1934 {
1935 const int maxLevel = static_cast<int>(frameCount) - 1;
1936 stackSelectionLevel = qBound(0, stackSelectionLevel, maxLevel);
1937 wslua_debugger_set_variable_stack_level(
1938 static_cast<int32_t>(stackSelectionLevel));
1939
1940 for (int32_t frameIndex = 0; frameIndex < frameCount; ++frameIndex)
1941 {
1942 QTreeWidgetItem *item = new QTreeWidgetItem(stackTree);
1943 item->setData(0, StackItemLevelRole,
1944 static_cast<qlonglong>(frameIndex));
1945 const char *rawSource = stack[frameIndex].source;
1946 const bool isLuaFrame = rawSource && rawSource[0] == '@';
1947 const QString functionName = QString::fromUtf8(
1948 stack[frameIndex].name ? stack[frameIndex].name : "?");
1949 QString locationText;
1950 QString resolvedPath;
1951 if (isLuaFrame)
1952 {
1953 const QString filePath = QString::fromUtf8(rawSource + 1);
1954 resolvedPath = normalizedFilePath(filePath);
1955 if (resolvedPath.isEmpty())
1956 {
1957 resolvedPath = filePath;
1958 }
1959 const QString fileDisplayName =
1960 QFileInfo(resolvedPath).fileName();
1961 locationText = QStringLiteral("%1:%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1:%2")))
1962 .arg(fileDisplayName.isEmpty() ? resolvedPath
1963 : fileDisplayName)
1964 .arg(stack[frameIndex].line);
1965 item->setToolTip(
1966 1, QStringLiteral("%1:%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1:%2")))
1967 .arg(resolvedPath)
1968 .arg(stack[frameIndex].line));
1969 }
1970 else
1971 {
1972 locationText = QString::fromUtf8(rawSource ? rawSource : "[C]");
1973 }
1974
1975 item->setText(0, functionName);
1976 item->setText(1, locationText);
1977
1978 if (isLuaFrame)
1979 {
1980 item->setData(0, StackItemNavigableRole, true);
1981 item->setData(0, StackItemFileRole, resolvedPath);
1982 item->setData(0, StackItemLineRole,
1983 static_cast<qlonglong>(stack[frameIndex].line));
1984 }
1985 else
1986 {
1987 item->setData(0, StackItemNavigableRole, false);
1988 QColor disabledColor =
1989 palette().color(QPalette::Disabled, QPalette::Text);
1990 item->setForeground(0, disabledColor);
1991 item->setForeground(1, disabledColor);
1992 }
1993
1994 if (frameIndex == stackSelectionLevel)
1995 {
1996 itemToSelect = item;
1997 }
1998 }
1999 wslua_debugger_free_stack(stack, frameCount);
2000 }
2001 else
2002 {
2003 stackSelectionLevel = 0;
2004 wslua_debugger_set_variable_stack_level(0);
2005 }
2006
2007 if (itemToSelect)
2008 {
2009 stackTree->setCurrentItem(itemToSelect);
2010 }
2011 stackTree->blockSignals(signalsWereBlocked);
2012}
2013
2014void LuaDebuggerDialog::refreshVariablesForCurrentStackFrame()
2015{
2016 if (!variablesTree
16.1
Field 'variablesTree' is non-null
|| !debuggerPaused
16.2
Field 'debuggerPaused' is true
|| !wslua_debugger_is_paused())
17
Assuming the condition is false
18
Taking false branch
2017 {
2018 return;
2019 }
2020 variablesTree->clear();
2021 updateVariables(nullptr, QString());
19
Calling 'LuaDebuggerDialog::updateVariables'
2022 restoreVariablesExpansionState();
2023 refreshWatchDisplay();
2024}
2025
2026void LuaDebuggerDialog::onStackCurrentItemChanged(QTreeWidgetItem *current,
2027 QTreeWidgetItem *previous)
2028{
2029 Q_UNUSED(previous)(void)previous;;
2030 if (!stackTree || !current || !debuggerPaused ||
2031 !wslua_debugger_is_paused())
2032 {
2033 return;
2034 }
2035
2036 const int level = static_cast<int>(current->data(0, StackItemLevelRole).toLongLong());
2037 if (level < 0 || level == stackSelectionLevel)
2038 {
2039 return;
2040 }
2041
2042 stackSelectionLevel = level;
2043 wslua_debugger_set_variable_stack_level(static_cast<int32_t>(level));
2044 refreshVariablesForCurrentStackFrame();
2045 syncVariablesTreeToCurrentWatch();
2046}
2047
2048// NOLINTNEXTLINE(misc-no-recursion)
2049void LuaDebuggerDialog::updateVariables(QTreeWidgetItem *parent,
2050 const QString &path)
2051{
2052 int32_t variableCount = 0;
2053 wslua_variable_t *variables = wslua_debugger_get_variables(
2054 path.isEmpty() ? NULL__null : path.toUtf8().constData(), &variableCount);
20
Assuming the condition is true
21
'?' condition is true
2055
2056 if (variables)
22
Assuming 'variables' is non-null
23
Taking true branch
2057 {
2058 for (int32_t variableIndex = 0; variableIndex < variableCount;
24
Assuming 'variableIndex' is < 'variableCount'
25
Loop condition is true. Entering loop body
2059 ++variableIndex)
2060 {
2061 QTreeWidgetItem *item;
2062 if (parent
25.1
'parent' is null
)
26
Taking false branch
2063 {
2064 item = new QTreeWidgetItem(parent);
2065 }
2066 else
2067 {
2068 item = new QTreeWidgetItem(variablesTree);
2069 }
2070
2071 const VariableRowFields f =
2072 readVariableRowFields(variables[variableIndex], path);
2073
2074 item->setText(0, f.name);
2075 item->setText(1, f.value);
2076
2077 const QString tooltipSuffix =
2078 f.type.isEmpty() ? QString() : tr("Type: %1").arg(f.type);
27
Assuming the condition is true
28
'?' condition is true
2079 item->setToolTip(
2080 0, tooltipSuffix.isEmpty()
29
Assuming the condition is true
30
'?' condition is true
2081 ? f.name
2082 : QStringLiteral("%1\n%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1\n%2"))).arg(f.name, tooltipSuffix));
2083 item->setToolTip(1, tooltipSuffix.isEmpty()
31
Assuming the condition is true
32
'?' condition is true
2084 ? f.value
2085 : QStringLiteral("%1\n%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1\n%2"))).arg(
2086 f.value, tooltipSuffix));
2087 item->setData(0, VariableTypeRole, f.type);
2088 item->setData(0, VariableCanExpandRole, f.canExpand);
2089 item->setData(0, VariablePathRole, f.childPath);
2090
2091 applyVariableExpansionIndicator(item, f.canExpand,
33
Calling 'applyVariableExpansionIndicator'
2092 /*enabledOnlyPlaceholder=*/false);
2093 }
2094 // Sort Globals alphabetically; preserve declaration order for
2095 // Locals and Upvalues since that is more natural for debugging.
2096 if (variableChildrenShouldSortByName(path))
2097 {
2098 if (parent)
2099 {
2100 parent->sortChildren(0, Qt::AscendingOrder);
2101 }
2102 else
2103 {
2104 variablesTree->sortItems(0, Qt::AscendingOrder);
2105 }
2106 }
2107
2108 wslua_debugger_free_variables(variables, variableCount);
2109 }
2110}
2111
2112void LuaDebuggerDialog::onVariableItemExpanded(QTreeWidgetItem *item)
2113{
2114 if (!item)
2115 {
2116 return;
2117 }
2118 const QString section = variableSectionRootKeyFromItem(item);
2119 if (!item->parent())
2120 {
2121 recordTreeSectionRootExpansion(variablesExpansion_, section, true);
2122 }
2123 else
2124 {
2125 const QString key = item->data(0, VariablePathRole).toString();
2126 recordTreeSectionSubpathExpansion(variablesExpansion_, section, key,
2127 true);
2128 }
2129
2130 if (item->childCount() == 1 && item->child(0)->text(0).isEmpty())
2131 {
2132 // Remove dummy
2133 delete item->takeChild(0);
2134
2135 QString path = item->data(0, VariablePathRole).toString();
2136 updateVariables(item, path);
2137 }
2138}
2139
2140void LuaDebuggerDialog::onVariableItemCollapsed(QTreeWidgetItem *item)
2141{
2142 if (!item)
2143 {
2144 return;
2145 }
2146 const QString section = variableSectionRootKeyFromItem(item);
2147 if (!item->parent())
2148 {
2149 recordTreeSectionRootExpansion(variablesExpansion_, section, false);
2150 }
2151 else
2152 {
2153 const QString key = item->data(0, VariablePathRole).toString();
2154 recordTreeSectionSubpathExpansion(variablesExpansion_, section, key,
2155 false);
2156 }
2157}
2158
2159LuaDebuggerCodeView *LuaDebuggerDialog::loadFile(const QString &file_path)
2160{
2161 QString normalizedPath = normalizedFilePath(file_path);
2162 if (normalizedPath.isEmpty())
2163 {
2164 normalizedPath = file_path;
2165 }
2166
2167 /* Check if file exists before creating a tab */
2168 QFileInfo fileInfo(normalizedPath);
2169 if (!fileInfo.exists() || !fileInfo.isFile())
2170 {
2171 /* File doesn't exist - don't create a tab */
2172 return nullptr;
2173 }
2174
2175 // Check if already open
2176 const qint32 existingTabCount =
2177 static_cast<qint32>(ui->codeTabWidget->count());
2178 for (qint32 tabIndex = 0; tabIndex < existingTabCount; ++tabIndex)
2179 {
2180 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(
2181 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
2182 if (view && view->getFilename() == normalizedPath)
2183 {
2184 ui->codeTabWidget->setCurrentIndex(static_cast<int>(tabIndex));
2185 return view;
2186 }
2187 }
2188
2189 // Create new tab
2190 LuaDebuggerCodeView *codeView = new LuaDebuggerCodeView(ui->codeTabWidget);
2191 codeView->setEditorFont(effectiveMonospaceFont(true));
2192 codeView->setFilename(normalizedPath);
2193
2194 QFile file(normalizedPath);
2195 if (file.open(QIODevice::ReadOnly | QIODevice::Text))
2196 {
2197 codeView->setPlainText(file.readAll());
2198 }
2199 else
2200 {
2201 /* This should not happen since we checked exists() above,
2202 * but handle it gracefully just in case */
2203 delete codeView;
2204 return nullptr;
2205 }
2206
2207 ensureFileTreeEntry(normalizedPath);
2208
2209 // Connect signals
2210 codeView->setContextMenuPolicy(Qt::CustomContextMenu);
2211 connect(codeView, &QWidget::customContextMenuRequested, this,
2212 &LuaDebuggerDialog::onCodeViewContextMenu);
2213
2214 connect(
2215 codeView, &LuaDebuggerCodeView::breakpointToggled,
2216 [this](const QString &file_path, qint32 line)
2217 {
2218 const int32_t state = wslua_debugger_get_breakpoint_state(
2219 file_path.toUtf8().constData(), line);
2220 if (state == -1)
2221 {
2222 wslua_debugger_add_breakpoint(file_path.toUtf8().constData(),
2223 line);
2224 ensureDebuggerEnabledForActiveBreakpoints();
2225 }
2226 else
2227 {
2228 wslua_debugger_remove_breakpoint(file_path.toUtf8().constData(),
2229 line);
2230 syncDebuggerToggleWithCore();
2231 }
2232 updateBreakpoints();
2233 // Update all views as breakpoint might affect them (unlikely but
2234 // safe)
2235 const qint32 tabCount =
2236 static_cast<qint32>(ui->codeTabWidget->count());
2237 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
2238 {
2239 LuaDebuggerCodeView *tabView =
2240 qobject_cast<LuaDebuggerCodeView *>(
2241 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
2242 if (tabView)
2243 tabView->updateBreakpointMarkers();
2244 }
2245 });
2246
2247 connect(codeView->document(), &QTextDocument::modificationChanged, this,
2248 [this, codeView]()
2249 {
2250 updateTabTextForCodeView(codeView);
2251 updateWindowModifiedState();
2252 if (ui->codeTabWidget->currentWidget() == codeView)
2253 {
2254 updateSaveActionState();
2255 }
2256 });
2257
2258 ui->codeTabWidget->addTab(codeView, QFileInfo(normalizedPath).fileName());
2259 updateTabTextForCodeView(codeView);
2260 ui->codeTabWidget->setCurrentWidget(codeView);
2261 ui->codeTabWidget->show();
2262 updateSaveActionState();
2263 updateWindowModifiedState();
2264 updateLuaEditorAuxFrames();
2265 return codeView;
2266}
2267
2268LuaDebuggerCodeView *LuaDebuggerDialog::currentCodeView() const
2269{
2270 return qobject_cast<LuaDebuggerCodeView *>(
2271 ui->codeTabWidget->currentWidget());
2272}
2273
2274qint32 LuaDebuggerDialog::unsavedOpenScriptTabCount() const
2275{
2276 qint32 count = 0;
2277 const qint32 tabCount =
2278 static_cast<qint32>(ui->codeTabWidget->count());
2279 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
2280 {
2281 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(
2282 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
2283 if (view && view->document()->isModified())
2284 {
2285 ++count;
2286 }
2287 }
2288 return count;
2289}
2290
2291bool LuaDebuggerDialog::hasUnsavedChanges() const
2292{
2293 return unsavedOpenScriptTabCount() > 0;
2294}
2295
2296bool LuaDebuggerDialog::ensureUnsavedChangesHandled(const QString &title)
2297{
2298 if (!hasUnsavedChanges())
2299 {
2300 return true;
2301 }
2302
2303 const qint32 unsavedCount = unsavedOpenScriptTabCount();
2304 const QMessageBox::StandardButton reply = QMessageBox::question(
2305 this, title,
2306 tr("There are unsaved changes in %Ln open file(s).", "", unsavedCount),
2307 QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
2308 QMessageBox::Save);
2309
2310 if (reply == QMessageBox::Cancel)
2311 {
2312 return false;
2313 }
2314 if (reply == QMessageBox::Save)
2315 {
2316 return saveAllModified();
2317 }
2318 clearAllDocumentModified();
2319 return true;
2320}
2321
2322void LuaDebuggerDialog::clearAllDocumentModified()
2323{
2324 const qint32 tabCount =
2325 static_cast<qint32>(ui->codeTabWidget->count());
2326 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
2327 {
2328 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(
2329 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
2330 if (view)
2331 {
2332 view->document()->setModified(false);
2333 }
2334 }
2335}
2336
2337bool LuaDebuggerDialog::saveCodeView(LuaDebuggerCodeView *view)
2338{
2339 if (!view)
2340 {
2341 return false;
2342 }
2343 const QString path = view->getFilename();
2344 if (path.isEmpty())
2345 {
2346 return false;
2347 }
2348
2349 QFile file(path);
2350 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
2351 {
2352 QMessageBox::warning(
2353 this, tr("Save Lua Script"),
2354 tr("Could not write to %1:\n%2").arg(path, file.errorString()));
2355 return false;
2356 }
2357 QTextStream out(&file);
2358 out << view->toPlainText();
2359 file.close();
2360 view->document()->setModified(false);
2361 return true;
2362}
2363
2364bool LuaDebuggerDialog::saveAllModified()
2365{
2366 const qint32 tabCount =
2367 static_cast<qint32>(ui->codeTabWidget->count());
2368 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
2369 {
2370 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(
2371 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
2372 if (view && view->document()->isModified())
2373 {
2374 if (!saveCodeView(view))
2375 {
2376 return false;
2377 }
2378 }
2379 }
2380 return true;
2381}
2382
2383void LuaDebuggerDialog::updateTabTextForCodeView(LuaDebuggerCodeView *view)
2384{
2385 if (!view)
2386 {
2387 return;
2388 }
2389 const int tabIndex = ui->codeTabWidget->indexOf(view);
2390 if (tabIndex < 0)
2391 {
2392 return;
2393 }
2394 QString label = QFileInfo(view->getFilename()).fileName();
2395 if (view->document()->isModified())
2396 {
2397 label += QStringLiteral(" *")(QString(QtPrivate::qMakeStringPrivate(u"" " *")));
2398 }
2399 ui->codeTabWidget->setTabText(tabIndex, label);
2400}
2401
2402void LuaDebuggerDialog::updateSaveActionState()
2403{
2404 LuaDebuggerCodeView *view = currentCodeView();
2405 ui->actionSaveFile->setEnabled(view && view->document()->isModified());
2406}
2407
2408void LuaDebuggerDialog::updateWindowModifiedState()
2409{
2410 setWindowModified(hasUnsavedChanges());
2411}
2412
2413void LuaDebuggerDialog::showAccordionFrame(AccordionFrame *show_frame,
2414 bool toggle)
2415{
2416 QList<AccordionFrame *> frame_list =
2417 QList<AccordionFrame *>() << ui->luaDebuggerFindFrame
2418 << ui->luaDebuggerGoToLineFrame;
2419 frame_list.removeAll(show_frame);
2420 for (AccordionFrame *af : frame_list)
2421 {
2422 if (af)
2423 {
2424 af->animatedHide();
2425 }
2426 }
2427 if (!show_frame)
2428 {
2429 return;
2430 }
2431 if (toggle && show_frame->isVisible())
2432 {
2433 show_frame->animatedHide();
2434 return;
2435 }
2436 LuaDebuggerGoToLineFrame *const goto_frame =
2437 qobject_cast<LuaDebuggerGoToLineFrame *>(show_frame);
2438 if (goto_frame)
2439 {
2440 goto_frame->syncLineFieldFromEditor();
2441 }
2442 show_frame->animatedShow();
2443 if (LuaDebuggerFindFrame *const find_frame =
2444 qobject_cast<LuaDebuggerFindFrame *>(show_frame))
2445 {
2446 find_frame->scheduleFindFieldFocus();
2447 }
2448 else if (goto_frame)
2449 {
2450 goto_frame->scheduleLineFieldFocus();
2451 }
2452}
2453
2454void LuaDebuggerDialog::updateLuaEditorAuxFrames()
2455{
2456 QPlainTextEdit *ed = currentCodeView();
2457 ui->luaDebuggerFindFrame->setTargetEditor(ed);
2458 ui->luaDebuggerGoToLineFrame->setTargetEditor(ed);
2459}
2460
2461void LuaDebuggerDialog::onEditorFind()
2462{
2463 updateLuaEditorAuxFrames();
2464 showAccordionFrame(ui->luaDebuggerFindFrame, true);
2465}
2466
2467void LuaDebuggerDialog::onEditorGoToLine()
2468{
2469 updateLuaEditorAuxFrames();
2470 showAccordionFrame(ui->luaDebuggerGoToLineFrame, true);
2471}
2472
2473void LuaDebuggerDialog::onSaveFile()
2474{
2475 LuaDebuggerCodeView *view = currentCodeView();
2476 if (!view || !view->document()->isModified())
2477 {
2478 return;
2479 }
2480 saveCodeView(view);
2481 updateSaveActionState();
2482}
2483
2484void LuaDebuggerDialog::onCodeTabCloseRequested(int idx)
2485{
2486 QWidget *widget = ui->codeTabWidget->widget(idx);
2487 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(widget);
2488 if (view && view->document()->isModified())
2489 {
2490 const QMessageBox::StandardButton reply = QMessageBox::question(
2491 this, tr("Lua Debugger"),
2492 tr("Save changes to %1 before closing?")
2493 .arg(QFileInfo(view->getFilename()).fileName()),
2494 QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
2495 QMessageBox::Save);
2496 if (reply == QMessageBox::Cancel)
2497 {
2498 return;
2499 }
2500 if (reply == QMessageBox::Save)
2501 {
2502 if (!saveCodeView(view))
2503 {
2504 return;
2505 }
2506 }
2507 else
2508 {
2509 view->document()->setModified(false);
2510 }
2511 }
2512
2513 ui->codeTabWidget->removeTab(idx);
2514 delete widget;
2515 updateSaveActionState();
2516 updateWindowModifiedState();
2517}
2518
2519void LuaDebuggerDialog::onBreakpointItemChanged(QTreeWidgetItem *item,
2520 int column)
2521{
2522 if (column == 0)
2523 {
2524 const QString file = item->data(0, BreakpointFileRole).toString();
2525 const int64_t lineNumber =
2526 item->data(0, BreakpointLineRole).toLongLong();
2527 const bool active = item->checkState(0) == Qt::Checked;
2528 wslua_debugger_set_breakpoint_active(file.toUtf8().constData(),
2529 lineNumber, active);
2530 if (active)
2531 {
2532 ensureDebuggerEnabledForActiveBreakpoints();
2533 }
2534 else
2535 {
2536 syncDebuggerToggleWithCore();
2537 }
2538
2539 const qint32 tabCount = static_cast<qint32>(ui->codeTabWidget->count());
2540 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
2541 {
2542 LuaDebuggerCodeView *tabView = qobject_cast<LuaDebuggerCodeView *>(
2543 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
2544 if (tabView && tabView->getFilename() == file)
2545 tabView->updateBreakpointMarkers();
2546 }
2547 }
2548}
2549
2550void LuaDebuggerDialog::onBreakpointItemClicked(QTreeWidgetItem *item,
2551 int column)
2552{
2553 if (column == 3)
2554 {
2555 const QString file = item->data(0, BreakpointFileRole).toString();
2556 const int64_t lineNumber =
2557 item->data(0, BreakpointLineRole).toLongLong();
2558 wslua_debugger_remove_breakpoint(file.toUtf8().constData(), lineNumber);
2559 updateBreakpoints();
2560
2561 const qint32 tabCount = static_cast<qint32>(ui->codeTabWidget->count());
2562 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
2563 {
2564 LuaDebuggerCodeView *tabView = qobject_cast<LuaDebuggerCodeView *>(
2565 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
2566 if (tabView && tabView->getFilename() == file)
2567 tabView->updateBreakpointMarkers();
2568 }
2569 }
2570}
2571
2572void LuaDebuggerDialog::onBreakpointItemDoubleClicked(QTreeWidgetItem *item,
2573 int column)
2574{
2575 Q_UNUSED(column)(void)column;;
2576 if (!item)
2577 {
2578 return;
2579 }
2580
2581 const QString file = item->data(0, BreakpointFileRole).toString();
2582 const int64_t lineNumber = item->data(0, BreakpointLineRole).toLongLong();
2583 LuaDebuggerCodeView *view = loadFile(file);
2584 if (view)
2585 {
2586 view->moveCaretToLineStart(static_cast<qint32>(lineNumber));
2587 }
2588}
2589
2590void LuaDebuggerDialog::onCodeViewContextMenu(const QPoint &pos)
2591{
2592 LuaDebuggerCodeView *codeView =
2593 qobject_cast<LuaDebuggerCodeView *>(sender());
2594 if (!codeView)
2595 return;
2596
2597 QMenu menu(this);
2598
2599 QAction *undoAct = menu.addAction(tr("Undo"));
2600 undoAct->setEnabled(codeView->document()->isUndoAvailable());
2601 connect(undoAct, &QAction::triggered, codeView, &QPlainTextEdit::undo);
2602
2603 QAction *redoAct = menu.addAction(tr("Redo"));
2604 redoAct->setEnabled(codeView->document()->isRedoAvailable());
2605 connect(redoAct, &QAction::triggered, codeView, &QPlainTextEdit::redo);
2606
2607 menu.addSeparator();
2608
2609 QAction *cutAct = menu.addAction(tr("Cut"));
2610 cutAct->setEnabled(codeView->textCursor().hasSelection());
2611 connect(cutAct, &QAction::triggered, codeView, &QPlainTextEdit::cut);
2612
2613 QAction *copyAct = menu.addAction(tr("Copy"));
2614 copyAct->setEnabled(codeView->textCursor().hasSelection());
2615 connect(copyAct, &QAction::triggered, codeView, &QPlainTextEdit::copy);
2616
2617 QAction *pasteAct = menu.addAction(tr("Paste"));
2618 pasteAct->setEnabled(codeView->canPaste());
2619 connect(pasteAct, &QAction::triggered, codeView, &QPlainTextEdit::paste);
2620
2621 QAction *selAllAct = menu.addAction(tr("Select All"));
2622 connect(selAllAct, &QAction::triggered, codeView, &QPlainTextEdit::selectAll);
2623
2624 menu.addSeparator();
2625 menu.addAction(ui->actionFind);
2626 menu.addAction(ui->actionGoToLine);
2627
2628 menu.addSeparator();
2629
2630 QTextCursor cursor = codeView->cursorForPosition(pos);
2631 const qint32 lineNumber = static_cast<qint32>(cursor.blockNumber() + 1);
2632
2633 // Check if breakpoint exists
2634 const int32_t state = wslua_debugger_get_breakpoint_state(
2635 codeView->getFilename().toUtf8().constData(), lineNumber);
2636
2637 if (state == -1)
2638 {
2639 QAction *addBp = menu.addAction(tr("Add Breakpoint"));
2640 connect(addBp, &QAction::triggered,
2641 [this, codeView, lineNumber]()
2642 {
2643 wslua_debugger_add_breakpoint(
2644 codeView->getFilename().toUtf8().constData(),
2645 lineNumber);
2646 updateBreakpoints();
2647 codeView->updateBreakpointMarkers();
2648 });
2649 }
2650 else
2651 {
2652 QAction *removeBp = menu.addAction(tr("Remove Breakpoint"));
2653 connect(removeBp, &QAction::triggered,
2654 [this, codeView, lineNumber]()
2655 {
2656 wslua_debugger_remove_breakpoint(
2657 codeView->getFilename().toUtf8().constData(),
2658 lineNumber);
2659 updateBreakpoints();
2660 codeView->updateBreakpointMarkers();
2661 });
2662 }
2663
2664 if (eventLoop)
2665 { // Only if paused
2666 QAction *runToLine = menu.addAction(tr("Run to this line"));
2667 connect(runToLine, &QAction::triggered,
2668 [this, codeView, lineNumber]()
2669 {
2670 ensureDebuggerEnabledForActiveBreakpoints();
2671 wslua_debugger_run_to_line(
2672 codeView->getFilename().toUtf8().constData(),
2673 lineNumber);
2674 if (eventLoop)
2675 eventLoop->quit();
2676 debuggerPaused = false;
2677 updateWidgets();
2678 clearPausedStateUi();
2679 });
2680
2681 // Evaluate selection if there is selected text
2682 QString selectedText = codeView->textCursor().selectedText();
2683 if (!selectedText.isEmpty())
2684 {
2685 menu.addSeparator();
2686 QAction *addWatch =
2687 menu.addAction(tr("Add Watch…"));
2688 connect(addWatch, &QAction::triggered,
2689 [this, selectedText]()
2690 {
2691 const QString t = selectedText.trimmed();
2692 if (!watchSpecUsesPathResolution(t))
2693 {
2694 showPathOnlyVariablePathWatchMessage();
2695 return;
2696 }
2697 insertNewWatchRow(t, false);
2698 });
2699 }
2700 }
2701
2702 menu.exec(codeView->mapToGlobal(pos));
2703}
2704
2705void LuaDebuggerDialog::onStackItemDoubleClicked(QTreeWidgetItem *item,
2706 int column)
2707{
2708 Q_UNUSED(column)(void)column;;
2709 if (!item)
2710 {
2711 return;
2712 }
2713 if (!item->data(0, StackItemNavigableRole).toBool())
2714 {
2715 return;
2716 }
2717 const QString file = item->data(0, StackItemFileRole).toString();
2718 const qint64 line = item->data(0, StackItemLineRole).toLongLong();
2719 if (file.isEmpty() || line <= 0)
2720 {
2721 return;
2722 }
2723 LuaDebuggerCodeView *view = loadFile(file);
2724 if (view)
2725 {
2726 view->moveCaretToLineStart(static_cast<qint32>(line));
2727 }
2728}
2729
2730void LuaDebuggerDialog::onMonospaceFontUpdated(const QFont &font)
2731{
2732 applyCodeEditorFonts(font);
2733}
2734
2735void LuaDebuggerDialog::onMainAppInitialized()
2736{
2737 applyMonospaceFonts();
2738}
2739
2740void LuaDebuggerDialog::onPreferencesChanged()
2741{
2742 applyCodeViewThemes();
2743 applyMonospaceFonts();
2744 refreshWatchDisplay();
2745}
2746
2747void LuaDebuggerDialog::onThemeChanged(int idx)
2748{
2749 if (themeComboBox)
2750 {
2751 int32_t theme = themeComboBox->itemData(idx).toInt();
2752
2753 /* Update static theme for CodeView syntax highlighting */
2754 currentTheme_ = theme;
2755
2756 /* Store theme in our JSON settings */
2757 if (theme == WSLUA_DEBUGGER_THEME_DARK)
2758 settings_["theme"] = "dark";
2759 else if (theme == WSLUA_DEBUGGER_THEME_LIGHT)
2760 settings_["theme"] = "light";
2761 else
2762 settings_["theme"] = "auto";
2763
2764 applyCodeViewThemes();
2765 }
2766}
2767
2768void LuaDebuggerDialog::onColorsChanged()
2769{
2770 /*
2771 * When Wireshark's color scheme changes and the debugger theme is set to
2772 * "Auto (follow color scheme)", we need to re-apply themes to all code
2773 * views. The applyCodeViewThemes() function will query
2774 * ColorUtils::themeIsDark() to determine the effective theme.
2775 */
2776 applyCodeViewThemes();
2777 refreshWatchDisplay();
2778}
2779
2780/**
2781 * @brief Static callback invoked before Lua plugins are reloaded.
2782 *
2783 * This callback is registered with wslua_debugger_register_reload_callback()
2784 * and is called by wslua_reload_plugins() BEFORE any Lua scripts are
2785 * unloaded or reloaded.
2786 *
2787 * The callback forwards to the dialog instance to reload all open
2788 * script files from disk. This ensures that when a breakpoint is hit
2789 * during the reload, the debugger displays the current version of
2790 * the script (which the user may have edited externally).
2791 */
2792void LuaDebuggerDialog::onLuaReloadCallback()
2793{
2794 LuaDebuggerDialog *dialog = _instance;
2795 if (dialog)
2796 {
2797 /*
2798 * If the debugger was paused, the UI layer called
2799 * wslua_debugger_notify_reload() which disabled the debugger
2800 * (continuing execution) and invoked this callback.
2801 * Exit the nested event loop so the Lua call stack can unwind.
2802 * handlePause() will schedule a deferred reload afterwards.
2803 */
2804 if (dialog->debuggerPaused && dialog->eventLoop)
2805 {
2806 dialog->debuggerPaused = false;
2807 dialog->clearPausedStateUi();
2808 dialog->reloadDeferred = true;
2809 dialog->eventLoop->quit();
2810 return;
2811 }
2812
2813 /*
2814 * Reload all script files from disk.
2815 * This must happen BEFORE Lua executes any code.
2816 */
2817 dialog->reloadAllScriptFiles();
2818
2819 /*
2820 * Update breakpoint markers in all open code views.
2821 * This ensures the gutter shows correct breakpoint indicators.
2822 *
2823 * Note: refreshAvailableScripts() and updateBreakpoints() are now
2824 * called in onLuaPostReloadCallback() AFTER plugins are loaded,
2825 * so new scripts appear in the file tree.
2826 */
2827 if (dialog->ui->codeTabWidget)
2828 {
2829 const qint32 tabCount =
2830 static_cast<qint32>(dialog->ui->codeTabWidget->count());
2831 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
2832 {
2833 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(
2834 dialog->ui->codeTabWidget->widget(
2835 static_cast<int>(tabIndex)));
2836 if (view)
2837 {
2838 view->updateBreakpointMarkers();
2839 }
2840 }
2841 }
2842 }
2843}
2844
2845/**
2846 * @brief Static callback invoked AFTER Lua plugins are reloaded.
2847 *
2848 * This callback refreshes the file tree with newly loaded scripts.
2849 * It is called after wslua_init() completes, so the plugin list
2850 * now contains all the new scripts.
2851 */
2852void LuaDebuggerDialog::onLuaPostReloadCallback()
2853{
2854 LuaDebuggerDialog *dialog = _instance;
2855 if (dialog)
2856 {
2857 /*
2858 * Refresh the file tree with newly loaded scripts.
2859 * This is the correct place to do it because we're called
2860 * AFTER wslua_init() has loaded all plugins.
2861 */
2862 dialog->refreshAvailableScripts();
2863 dialog->updateBreakpoints();
2864 }
2865}
2866
2867/**
2868 * @brief Static callback invoked when a Lua script is loaded.
2869 *
2870 * This callback is called by the Lua loader for each script that is
2871 * successfully loaded. We add the script to the file tree.
2872 */
2873void LuaDebuggerDialog::onScriptLoadedCallback(const char *file_path)
2874{
2875 LuaDebuggerDialog *dialog = _instance;
2876 if (dialog && file_path)
2877 {
2878 dialog->ensureFileTreeEntry(QString::fromUtf8(file_path));
2879 dialog->fileTree->sortItems(0, Qt::AscendingOrder);
2880 }
2881}
2882
2883void LuaDebuggerDialog::reloadAllScriptFiles()
2884{
2885 if (!ui->codeTabWidget)
2886 {
2887 return;
2888 }
2889
2890 const qint32 tabCount = static_cast<qint32>(ui->codeTabWidget->count());
2891 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
2892 {
2893 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(
2894 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
2895 if (view)
2896 {
2897 if (view->document()->isModified())
2898 {
2899 /* Keep in-memory edits when this reload was not preceded by a
2900 * save/discard prompt (e.g. Analyze → Reload Lua Plugins). */
2901 continue;
2902 }
2903 QString filePath = view->getFilename();
2904 if (!filePath.isEmpty())
2905 {
2906 QFile file(filePath);
2907 if (file.open(QIODevice::ReadOnly | QIODevice::Text))
2908 {
2909 QTextStream in(&file);
2910 QString content = in.readAll();
2911 file.close();
2912 view->setPlainText(content);
2913 }
2914 }
2915 }
2916 }
2917}
2918
2919void LuaDebuggerDialog::applyCodeViewThemes()
2920{
2921 ui->luaDebuggerFindFrame->updateStyleSheet();
2922 ui->luaDebuggerGoToLineFrame->updateStyleSheet();
2923 if (!ui->codeTabWidget)
2924 {
2925 return;
2926 }
2927 const qint32 tabCount = static_cast<qint32>(ui->codeTabWidget->count());
2928 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
2929 {
2930 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(
2931 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
2932 if (view)
2933 {
2934 view->applyTheme();
2935 }
2936 }
2937}
2938
2939/**
2940 * @brief Callback function for wslua_debugger_foreach_loaded_script.
2941 *
2942 * This C callback receives each loaded script path from the Lua subsystem
2943 * and adds it to the file tree via the dialog instance.
2944 */
2945static void loaded_script_callback(const char *file_path, void *user_data)
2946{
2947 LuaDebuggerDialog *dialog = static_cast<LuaDebuggerDialog *>(user_data);
2948 if (dialog && file_path)
2949 {
2950 dialog->ensureFileTreeEntry(QString::fromUtf8(file_path));
2951 }
2952}
2953
2954void LuaDebuggerDialog::refreshAvailableScripts()
2955{
2956 /* Clear existing file tree entries */
2957 fileTree->clear();
2958
2959 /*
2960 * First, scan the plugin directories to show all available .lua files.
2961 * This includes files that may not be loaded yet.
2962 */
2963 const char *envPrefix = application_configuration_environment_prefix();
2964 if (envPrefix)
2965 {
2966 const char *personal = get_plugins_pers_dir(envPrefix);
2967 const char *global = get_plugins_dir(envPrefix);
2968 if (personal && personal[0])
2969 {
2970 scanScriptDirectory(QString::fromUtf8(personal));
2971 }
2972 if (global && global[0])
2973 {
2974 scanScriptDirectory(QString::fromUtf8(global));
2975 }
2976 }
2977
2978 /*
2979 * Then, add any loaded scripts that might be outside the plugin
2980 * directories (e.g., command-line scripts).
2981 */
2982 wslua_debugger_foreach_loaded_script(loaded_script_callback, this);
2983
2984 fileTree->sortItems(0, Qt::AscendingOrder);
2985 fileTree->expandAll();
2986}
2987
2988void LuaDebuggerDialog::scanScriptDirectory(const QString &dir_path)
2989{
2990 if (dir_path.isEmpty())
2991 {
2992 return;
2993 }
2994
2995 QDir scriptDirectory(dir_path);
2996 if (!scriptDirectory.exists())
2997 {
2998 return;
2999 }
3000
3001 QDirIterator scriptIterator(dir_path, QStringList() << "*.lua", QDir::Files,
3002 QDirIterator::Subdirectories);
3003 while (scriptIterator.hasNext())
3004 {
3005 ensureFileTreeEntry(scriptIterator.next());
3006 }
3007}
3008
3009bool LuaDebuggerDialog::ensureFileTreeEntry(const QString &file_path)
3010{
3011 QString normalized = normalizedFilePath(file_path);
3012 if (normalized.isEmpty())
3013 {
3014 return false;
3015 }
3016
3017 QVector<QPair<QString, QString>> components;
3018 if (!appendPathComponents(normalized, components))
3019 {
3020 return false;
3021 }
3022
3023 QTreeWidgetItem *parent = nullptr;
3024 bool createdLeaf = false;
3025 const qint32 componentCount = static_cast<qint32>(components.size());
3026 for (qint32 componentIndex = 0; componentIndex < componentCount;
3027 ++componentIndex)
3028 {
3029 const bool isLeaf = (componentIndex == componentCount - 1);
3030 const QString displayName =
3031 components.at(static_cast<int>(componentIndex)).first;
3032 const QString absolutePath =
3033 components.at(static_cast<int>(componentIndex)).second;
3034 QTreeWidgetItem *item = findChildItemByPath(parent, absolutePath);
3035 if (!item)
3036 {
3037 item = parent ? new QTreeWidgetItem(parent)
3038 : new QTreeWidgetItem(fileTree);
3039 item->setText(0, displayName);
3040 item->setToolTip(0, absolutePath);
3041 item->setData(0, FileTreePathRole, absolutePath);
3042 item->setData(0, FileTreeIsDirectoryRole, !isLeaf);
3043 item->setIcon(0, isLeaf ? fileIcon : folderIcon);
3044 if (parent)
3045 {
3046 parent->sortChildren(0, Qt::AscendingOrder);
3047 }
3048 else
3049 {
3050 fileTree->sortItems(0, Qt::AscendingOrder);
3051 }
3052 if (isLeaf)
3053 {
3054 createdLeaf = true;
3055 }
3056 }
3057 parent = item;
3058 }
3059
3060 if (createdLeaf)
3061 {
3062 fileTree->expandAll();
3063 }
3064
3065 return createdLeaf;
3066}
3067
3068QString LuaDebuggerDialog::normalizedFilePath(const QString &file_path) const
3069{
3070 QString trimmed = file_path.trimmed();
3071 if (trimmed.startsWith("@"))
3072 {
3073 trimmed = trimmed.mid(1);
3074 }
3075
3076 QFileInfo info(trimmed);
3077 QString absolutePath = info.absoluteFilePath();
3078
3079 if (info.exists())
3080 {
3081 QString canonical = info.canonicalFilePath();
3082 if (!canonical.isEmpty())
3083 {
3084 return canonical;
3085 }
3086 return QDir::cleanPath(absolutePath);
3087 }
3088
3089 if (!absolutePath.isEmpty())
3090 {
3091 return QDir::cleanPath(absolutePath);
3092 }
3093
3094 return trimmed;
3095}
3096
3097QTreeWidgetItem *
3098LuaDebuggerDialog::findChildItemByPath(QTreeWidgetItem *parent,
3099 const QString &path) const
3100{
3101 if (parent)
3102 {
3103 const qint32 childCount = static_cast<qint32>(parent->childCount());
3104 for (qint32 childIndex = 0; childIndex < childCount; ++childIndex)
3105 {
3106 QTreeWidgetItem *child =
3107 parent->child(static_cast<int>(childIndex));
3108 if (child->data(0, FileTreePathRole).toString() == path)
3109 {
3110 return child;
3111 }
3112 }
3113 return nullptr;
3114 }
3115
3116 const qint32 topLevelCount =
3117 static_cast<qint32>(fileTree->topLevelItemCount());
3118 for (qint32 topLevelIndex = 0; topLevelIndex < topLevelCount;
3119 ++topLevelIndex)
3120 {
3121 QTreeWidgetItem *item =
3122 fileTree->topLevelItem(static_cast<int>(topLevelIndex));
3123 if (item->data(0, FileTreePathRole).toString() == path)
3124 {
3125 return item;
3126 }
3127 }
3128 return nullptr;
3129}
3130
3131bool LuaDebuggerDialog::appendPathComponents(
3132 const QString &absolute_path,
3133 QVector<QPair<QString, QString>> &components) const
3134{
3135 QString forwardPath = QDir::fromNativeSeparators(absolute_path);
3136 QStringList segments = forwardPath.split('/', Qt::SkipEmptyParts);
3137 const qint32 segmentCount = static_cast<qint32>(segments.size());
3138 QString currentForward;
3139 qint32 segmentStartIndex = 0;
3140
3141 if (absolute_path.startsWith("\\\\") || absolute_path.startsWith("//"))
3142 {
3143 if (segmentCount < 2)
3144 {
3145 return false;
3146 }
3147 currentForward =
3148 QStringLiteral("//%1/%2")(QString(QtPrivate::qMakeStringPrivate(u"" "//%1/%2"))).arg(segments.at(0), segments.at(1));
3149 QString display =
3150 QStringLiteral("\\\\%1\\%2")(QString(QtPrivate::qMakeStringPrivate(u"" "\\\\%1\\%2"))).arg(segments.at(0), segments.at(1));
3151 components.append({display, QDir::toNativeSeparators(currentForward)});
3152 segmentStartIndex = 2;
3153 }
3154 else if (segmentCount > 0 && segments.first().endsWith(QLatin1Char(':')))
3155 {
3156 currentForward = segments.first();
3157 QString storedRoot = currentForward;
3158 if (!storedRoot.endsWith(QLatin1Char('/')))
3159 {
3160 storedRoot += QLatin1Char('/');
3161 }
3162 components.append(
3163 {currentForward, QDir::toNativeSeparators(storedRoot)});
3164 segmentStartIndex = 1;
3165 }
3166 else if (absolute_path.startsWith('/'))
3167 {
3168 currentForward = QStringLiteral("/")(QString(QtPrivate::qMakeStringPrivate(u"" "/")));
3169 components.append({currentForward, currentForward});
3170 }
3171 else if (segmentCount > 0)
3172 {
3173 currentForward = segments.first();
3174 components.append(
3175 {currentForward, QDir::toNativeSeparators(currentForward)});
3176 segmentStartIndex = 1;
3177 }
3178
3179 if (currentForward.isEmpty() && segmentCount > 0)
3180 {
3181 currentForward = segments.first();
3182 components.append(
3183 {currentForward, QDir::toNativeSeparators(currentForward)});
3184 segmentStartIndex = 1;
3185 }
3186
3187 for (qint32 segmentIndex = segmentStartIndex; segmentIndex < segmentCount;
3188 ++segmentIndex)
3189 {
3190 const QString &segment = segments.at(static_cast<int>(segmentIndex));
3191 if (currentForward.isEmpty() || currentForward == "/")
3192 {
3193 currentForward = currentForward == "/"
3194 ? QStringLiteral("/%1")(QString(QtPrivate::qMakeStringPrivate(u"" "/%1"))).arg(segment)
3195 : segment;
3196 }
3197 else
3198 {
3199 currentForward += "/" + segment;
3200 }
3201 components.append({segment, QDir::toNativeSeparators(currentForward)});
3202 }
3203
3204 return !components.isEmpty();
3205}
3206
3207void LuaDebuggerDialog::openInitialBreakpointFiles(
3208 const QVector<QString> &files)
3209{
3210 for (const QString &path : files)
3211 {
3212 loadFile(path);
3213 }
3214}
3215
3216void LuaDebuggerDialog::configureVariablesTreeColumns()
3217{
3218 if (!variablesTree || !variablesTree->header())
3219 {
3220 return;
3221 }
3222 variablesTree->setColumnCount(2);
3223 QHeaderView *header = variablesTree->header();
3224 header->setStretchLastSection(true);
3225 header->setSectionsMovable(false);
3226 header->setSectionResizeMode(0, QHeaderView::Interactive);
3227 header->setSectionResizeMode(1, QHeaderView::Stretch);
3228 // Initial width for Name column - Value column stretches to fill the rest
3229 header->resizeSection(0, 150);
3230}
3231
3232void LuaDebuggerDialog::configureWatchTreeColumns()
3233{
3234 if (!watchTree || !watchTree->header())
3235 {
3236 return;
3237 }
3238 QHeaderView *header = watchTree->header();
3239 header->setStretchLastSection(true);
3240 header->setSectionsMovable(false);
3241 header->setSectionResizeMode(0, QHeaderView::Interactive);
3242 header->setSectionResizeMode(1, QHeaderView::Stretch);
3243 header->resizeSection(0, 200);
3244}
3245
3246void LuaDebuggerDialog::configureStackTreeColumns()
3247{
3248 if (!stackTree || !stackTree->header())
3249 {
3250 return;
3251 }
3252 QHeaderView *header = stackTree->header();
3253 header->setStretchLastSection(true);
3254 header->setSectionsMovable(false);
3255 header->setSectionResizeMode(0, QHeaderView::Interactive);
3256 header->setSectionResizeMode(1, QHeaderView::Stretch);
3257 // Initial width for Function column - Location column stretches to fill the
3258 // rest
3259 header->resizeSection(0, 150);
3260}
3261
3262void LuaDebuggerDialog::clearPausedStateUi()
3263{
3264 if (variablesTree)
3265 {
3266 variablesTree->clear();
3267 }
3268 if (stackTree)
3269 {
3270 stackTree->clear();
3271 }
3272 clearAllCodeHighlights();
3273}
3274
3275void LuaDebuggerDialog::resumeDebuggerAndExitLoop()
3276{
3277 if (debuggerPaused)
3278 {
3279 wslua_debugger_continue();
3280 debuggerPaused = false;
3281 clearPausedStateUi();
3282 }
3283
3284 if (eventLoop)
3285 {
3286 eventLoop->quit();
3287 }
3288}
3289
3290void LuaDebuggerDialog::onVariablesContextMenuRequested(const QPoint &pos)
3291{
3292 if (!variablesTree)
3293 {
3294 return;
3295 }
3296
3297 QTreeWidgetItem *item = variablesTree->itemAt(pos);
3298 if (!item)
3299 {
3300 return;
3301 }
3302
3303 const QString nameText = item->text(0);
3304 const QString valueText = item->text(1);
3305 const QString bothText =
3306 valueText.isEmpty() ? nameText : tr("%1 = %2").arg(nameText, valueText);
3307
3308 QMenu menu(this);
3309 QAction *copyName = menu.addAction(tr("Copy Name"));
3310 QAction *copyValue = menu.addAction(tr("Copy Value"));
3311 QAction *copyBoth = menu.addAction(tr("Copy Name && Value"));
3312
3313 auto copyToClipboard = [](const QString &text)
3314 {
3315 if (QClipboard *clipboard = QGuiApplication::clipboard())
3316 {
3317 clipboard->setText(text);
3318 }
3319 };
3320
3321 connect(copyName, &QAction::triggered, this,
3322 [copyToClipboard, nameText]() { copyToClipboard(nameText); });
3323 connect(copyValue, &QAction::triggered, this,
3324 [copyToClipboard, valueText]() { copyToClipboard(valueText); });
3325 connect(copyBoth, &QAction::triggered, this,
3326 [copyToClipboard, bothText]() { copyToClipboard(bothText); });
3327
3328 if (eventLoop)
3329 {
3330 menu.addSeparator();
3331 const QString varPath = item->data(0, VariablePathRole).toString();
3332 if (!varPath.isEmpty())
3333 {
3334 QAction *addWatch =
3335 menu.addAction(tr("Add Watch: \"%1\"")
3336 .arg(varPath.length() > 48
3337 ? varPath.left(48) +
3338 QStringLiteral("…")(QString(QtPrivate::qMakeStringPrivate(u"" "…")))
3339 : varPath));
3340 connect(addWatch, &QAction::triggered, this,
3341 [this, varPath]() { addPathWatch(varPath); });
3342 }
3343 }
3344
3345 menu.exec(variablesTree->viewport()->mapToGlobal(pos));
3346}
3347
3348void LuaDebuggerDialog::clearAllCodeHighlights()
3349{
3350 if (!ui->codeTabWidget)
3351 {
3352 return;
3353 }
3354 const qint32 tabCount = static_cast<qint32>(ui->codeTabWidget->count());
3355 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
3356 {
3357 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(
3358 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
3359 if (view)
3360 {
3361 view->clearCurrentLineHighlight();
3362 }
3363 }
3364}
3365
3366void LuaDebuggerDialog::applyMonospaceFonts()
3367{
3368 applyCodeEditorFonts(effectiveMonospaceFont(true));
3369 applyMonospacePanelFonts();
3370}
3371
3372void LuaDebuggerDialog::applyCodeEditorFonts(const QFont &monoFont)
3373{
3374 QFont font = monoFont;
3375 if (font.family().isEmpty())
3376 {
3377 font = effectiveMonospaceFont(true);
3378 }
3379
3380 if (!ui->codeTabWidget)
3381 {
3382 return;
3383 }
3384 const qint32 tabCount = static_cast<qint32>(ui->codeTabWidget->count());
3385 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
3386 {
3387 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(
3388 ui->codeTabWidget->widget(static_cast<int>(tabIndex)));
3389 if (view)
3390 {
3391 view->setEditorFont(font);
3392 }
3393 }
3394}
3395
3396void LuaDebuggerDialog::applyMonospacePanelFonts()
3397{
3398 const QFont panelMono = effectiveMonospaceFont(false);
3399 const QFont headerFont = effectiveRegularFont();
3400
3401 const QList<QWidget *> widgets = {variablesTree, watchTree, stackTree,
3402 breakpointsTree, evalInputEdit,
3403 evalOutputEdit};
3404 for (QWidget *widget : widgets)
3405 {
3406 if (widget)
3407 {
3408 widget->setFont(panelMono);
3409 }
3410 }
3411
3412 const QList<QTreeWidget *> treesWithStandardHeaders = {
3413 variablesTree, watchTree, stackTree, fileTree, breakpointsTree};
3414 for (QTreeWidget *tree : treesWithStandardHeaders)
3415 {
3416 if (tree && tree->header())
3417 {
3418 tree->header()->setFont(headerFont);
3419 }
3420 }
3421}
3422
3423QFont LuaDebuggerDialog::effectiveMonospaceFont(bool zoomed) const
3424{
3425 /* Monospace font for panels and the script editor. */
3426 if (mainApp && mainApp->isInitialized())
3427 {
3428 return mainApp->monospaceFont(zoomed);
3429 }
3430
3431 /* Fall back to system fixed font */
3432 return QFontDatabase::systemFont(QFontDatabase::FixedFont);
3433}
3434
3435QFont LuaDebuggerDialog::effectiveRegularFont() const
3436{
3437 if (mainApp && mainApp->isInitialized())
3438 {
3439 return mainApp->font();
3440 }
3441 return QGuiApplication::font();
3442}
3443
3444void LuaDebuggerDialog::syncDebuggerToggleWithCore()
3445{
3446 if (!enabledCheckBox)
3447 {
3448 return;
3449 }
3450 const bool debuggerEnabled = wslua_debugger_is_enabled();
3451 bool previousState = enabledCheckBox->blockSignals(true);
3452 enabledCheckBox->setChecked(debuggerEnabled);
3453 enabledCheckBox->blockSignals(previousState);
3454 updateWidgets();
3455}
3456
3457void LuaDebuggerDialog::updateEnabledCheckboxIcon()
3458{
3459 if (!enabledCheckBox)
3460 {
3461 return;
3462 }
3463
3464 const bool debuggerEnabled = wslua_debugger_is_enabled();
3465
3466 // Create a colored circle icon to indicate enabled/disabled state
3467 QPixmap pixmap(16, 16);
3468 pixmap.fill(Qt::transparent);
3469 QPainter painter(&pixmap);
3470 painter.setRenderHint(QPainter::Antialiasing);
3471
3472 if (debuggerEnabled)
3473 {
3474 // Green circle for enabled
3475 painter.setBrush(QColor("#28A745"));
3476 painter.setPen(Qt::NoPen);
3477 enabledCheckBox->setToolTip(
3478 tr("Debugger is enabled. Uncheck to disable."));
3479 }
3480 else
3481 {
3482 // Gray circle for disabled
3483 painter.setBrush(QColor("#808080"));
3484 painter.setPen(Qt::NoPen);
3485 enabledCheckBox->setToolTip(
3486 tr("Debugger is disabled. Check to enable."));
3487 }
3488 painter.drawEllipse(2, 2, 12, 12);
3489 painter.end();
3490
3491 enabledCheckBox->setIcon(QIcon(pixmap));
3492}
3493
3494void LuaDebuggerDialog::updateStatusLabel()
3495{
3496 const bool debuggerEnabled = wslua_debugger_is_enabled();
3497 /* [*] is required for setWindowModified() to show an unsaved
3498 * indicator in the title. */
3499 QString title = QStringLiteral("[*]%1")(QString(QtPrivate::qMakeStringPrivate(u"" "[*]%1"))).arg(tr("Lua Debugger"));
3500
3501#ifdef Q_OS_MAC
3502 // On macOS we separate with a unicode em dash
3503 title += QString(" " UTF8_EM_DASH"\u2014" " ");
3504#else
3505 title += QString(" - ");
3506#endif
3507
3508 if (!debuggerEnabled)
3509 {
3510 title += tr("Disabled");
3511 }
3512 else if (debuggerPaused)
3513 {
3514 title += tr("Paused");
3515 }
3516 else
3517 {
3518 title += tr("Running");
3519 }
3520
3521 setWindowTitle(title);
3522 updateWindowModifiedState();
3523}
3524
3525void LuaDebuggerDialog::updateContinueActionState()
3526{
3527 const bool allowContinue = wslua_debugger_is_enabled() && debuggerPaused;
3528 ui->actionContinue->setEnabled(allowContinue);
3529 ui->actionStepOver->setEnabled(allowContinue);
3530 ui->actionStepIn->setEnabled(allowContinue);
3531 ui->actionStepOut->setEnabled(allowContinue);
3532}
3533
3534void LuaDebuggerDialog::updateWidgets()
3535{
3536 updateEnabledCheckboxIcon();
3537 updateStatusLabel();
3538 updateContinueActionState();
3539 updateEvalPanelState();
3540 refreshWatchDisplay();
3541}
3542
3543void LuaDebuggerDialog::ensureDebuggerEnabledForActiveBreakpoints()
3544{
3545 if (!wslua_debugger_is_enabled())
3546 {
3547 wslua_debugger_set_enabled(true);
3548 syncDebuggerToggleWithCore();
3549 }
3550}
3551
3552void LuaDebuggerDialog::onOpenFile()
3553{
3554 QString startDir = lastOpenDirectory;
3555 if (startDir.isEmpty())
3556 {
3557 startDir = QDir::homePath();
3558 }
3559
3560 const QString filePath = WiresharkFileDialog::getOpenFileName(
3561 this, tr("Open Lua Script"), startDir,
3562 tr("Lua Scripts (*.lua);;All Files (*)"));
3563
3564 if (filePath.isEmpty())
3565 {
3566 return;
3567 }
3568
3569 lastOpenDirectory = QFileInfo(filePath).absolutePath();
3570 loadFile(filePath);
3571}
3572
3573void LuaDebuggerDialog::onReloadLuaPlugins()
3574{
3575 if (!ensureUnsavedChangesHandled(tr("Reload Lua Plugins")))
3576 {
3577 return;
3578 }
3579
3580 // Confirmation dialog
3581 QMessageBox::StandardButton reply = QMessageBox::question(
3582 this, tr("Reload Lua Plugins"),
3583 tr("Are you sure you want to reload all Lua plugins?\n\nThis will "
3584 "restart all Lua "
3585 "scripts and may affect capture analysis."),
3586 QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
3587
3588 if (reply != QMessageBox::Yes)
3589 {
3590 return;
3591 }
3592
3593 /*
3594 * If the debugger is currently paused, disable it (which continues
3595 * execution), signal the event loop to exit, and let handlePause()
3596 * schedule a deferred reload after the Lua call stack unwinds.
3597 */
3598 if (debuggerPaused)
3599 {
3600 wslua_debugger_notify_reload();
3601 /* onLuaReloadCallback() has already set reloadDeferred,
3602 * cleared paused UI, and quit the event loop. */
3603 updateWidgets();
3604 return;
3605 }
3606
3607 /*
3608 * Not paused — trigger the reload directly via the delayed
3609 * path so it runs after this dialog method returns.
3610 */
3611 if (mainApp)
3612 {
3613 mainApp->reloadLuaPluginsDelayed();
3614 }
3615}
3616
3617void LuaDebuggerDialog::updateEvalPanelState()
3618{
3619 const bool canEvaluate = debuggerPaused && wslua_debugger_is_paused();
3620 evalInputEdit->setEnabled(canEvaluate);
3621 evalButton->setEnabled(canEvaluate);
3622
3623 if (!canEvaluate)
3624 {
3625 evalInputEdit->setPlaceholderText(
3626 tr("Evaluation available when debugger is paused"));
3627 }
3628 else
3629 {
3630 evalInputEdit->setPlaceholderText(
3631 tr("Enter Lua expression (prefix with = to return value)"));
3632 }
3633}
3634
3635void LuaDebuggerDialog::onEvaluate()
3636{
3637 if (!debuggerPaused || !wslua_debugger_is_paused())
3638 {
3639 return;
3640 }
3641
3642 QString expression = evalInputEdit->toPlainText().trimmed();
3643 if (expression.isEmpty())
3644 {
3645 return;
3646 }
3647
3648 char *error_msg = nullptr;
3649 char *result =
3650 wslua_debugger_evaluate(expression.toUtf8().constData(), &error_msg);
3651
3652 QString output;
3653 if (result)
3654 {
3655 output = QString::fromUtf8(result);
3656 g_free(result);
3657 }
3658 else if (error_msg)
3659 {
3660 output = tr("Error: %1").arg(QString::fromUtf8(error_msg));
3661 g_free(error_msg);
3662 }
3663 else
3664 {
3665 output = tr("Error: Unknown error");
3666 }
3667
3668 // Append to output with separator
3669 QString currentOutput = evalOutputEdit->toPlainText();
3670 if (!currentOutput.isEmpty())
3671 {
3672 currentOutput += QStringLiteral("\n")(QString(QtPrivate::qMakeStringPrivate(u"" "\n")));
3673 }
3674 currentOutput += QStringLiteral("> %1\n%2")(QString(QtPrivate::qMakeStringPrivate(u"" "> %1\n%2"))).arg(expression, output);
3675 evalOutputEdit->setPlainText(currentOutput);
3676
3677 // Scroll to bottom
3678 QTextCursor cursor = evalOutputEdit->textCursor();
3679 cursor.movePosition(QTextCursor::End);
3680 evalOutputEdit->setTextCursor(cursor);
3681
3682 // Update all views in case the expression modified state
3683 updateStack();
3684 variablesTree->clear();
3685 updateVariables(nullptr, QString());
3686 restoreVariablesExpansionState();
3687 refreshAvailableScripts();
3688 refreshWatchDisplay();
3689}
3690
3691void LuaDebuggerDialog::onEvalClear()
3692{
3693 evalInputEdit->clear();
3694 evalOutputEdit->clear();
3695}
3696
3697void LuaDebuggerDialog::storeWatchList()
3698{
3699 if (!watchTree)
3700 {
3701 return;
3702 }
3703 /* On disk, "watches" is a flat array of canonical watch spec strings in
3704 * visual order. Per-row expansion, editor origin, and other runtime state
3705 * are tracked in QTreeWidgetItem data roles only and are not persisted. */
3706 QStringList specs;
3707 const int n = watchTree->topLevelItemCount();
3708 for (int i = 0; i < n; ++i)
3709 {
3710 QTreeWidgetItem *it = watchTree->topLevelItem(i);
3711 if (!it)
3712 {
3713 continue;
3714 }
3715 const QString spec = it->data(0, WatchSpecRole).toString();
3716 if (spec.isEmpty())
3717 {
3718 continue;
3719 }
3720 specs.append(spec);
3721 }
3722 settings_[SettingsKeys::Watches] = specs;
3723 /* The runtime expansion map is keyed by root spec; drop entries for
3724 * specs that no longer exist in the tree. `storeWatchList` only runs
3725 * when the dialog is closing, which is also the last chance to avoid
3726 * persisting stale expansion data for specs that have since been
3727 * deleted or renamed. */
3728 pruneWatchExpansionMap();
3729}
3730
3731void LuaDebuggerDialog::storeBreakpointsList()
3732{
3733 QVariantList list;
3734 const unsigned count = wslua_debugger_get_breakpoint_count();
3735 for (unsigned i = 0; i < count; i++)
3736 {
3737 const char *file = nullptr;
3738 int64_t line = 0;
3739 bool active = false;
3740 if (wslua_debugger_get_breakpoint(i, &file, &line, &active))
3741 {
3742 QJsonObject bp;
3743 bp[QStringLiteral("file")(QString(QtPrivate::qMakeStringPrivate(u"" "file")))] = QString::fromUtf8(file);
3744 bp[QStringLiteral("line")(QString(QtPrivate::qMakeStringPrivate(u"" "line")))] = static_cast<qint64>(line);
3745 bp[QStringLiteral("active")(QString(QtPrivate::qMakeStringPrivate(u"" "active")))] = active;
3746 list.append(bp.toVariantMap());
3747 }
3748 }
3749 settings_[SettingsKeys::Breakpoints] = list;
3750}
3751
3752void LuaDebuggerDialog::rebuildWatchTreeFromSettings()
3753{
3754 if (!watchTree)
3755 {
3756 return;
3757 }
3758 watchTree->clear();
3759 /* The watch list on disk is a flat array of canonical spec strings.
3760 * Values that are not a valid path watch, or that are not strings, are
3761 * silently dropped (see wslua_debugger_watch_spec_uses_path_resolution). */
3762 const QVariantList rawList =
3763 settings_.value(QString::fromUtf8(SettingsKeys::Watches)).toList();
3764 for (const QVariant &entry : rawList)
3765 {
3766 /* Container QVariants (QVariantMap / QVariantList) toString() to an
3767 * empty string and are dropped here. Scalar-like values (numbers,
3768 * booleans) convert to a non-empty string but are then rejected by
3769 * watchSpecUsesPathResolution below. */
3770 const QString spec = entry.toString();
3771 if (spec.isEmpty())
3772 {
3773 continue;
3774 }
3775 if (!watchSpecUsesPathResolution(spec))
3776 {
3777 continue;
3778 }
3779 QTreeWidgetItem *row = new QTreeWidgetItem(watchTree);
3780 setupWatchRootItemFromSpec(row, spec);
3781 }
3782 refreshWatchDisplay();
3783 restoreWatchExpansionState();
3784}
3785
3786namespace
3787{
3788// NOLINTNEXTLINE(misc-no-recursion)
3789static QTreeWidgetItem *findVariableItemByPathRecursive(QTreeWidgetItem *node,
3790 const QString &path)
3791{
3792 if (!node)
3793 {
3794 return nullptr;
3795 }
3796 if (node->data(0, VariablePathRole).toString() == path)
3797 {
3798 return node;
3799 }
3800 const int n = node->childCount();
3801 for (int i = 0; i < n; ++i)
3802 {
3803 QTreeWidgetItem *r =
3804 findVariableItemByPathRecursive(node->child(i), path);
3805 if (r)
3806 {
3807 return r;
3808 }
3809 }
3810 return nullptr;
3811}
3812} // namespace
3813
3814void LuaDebuggerDialog::deleteWatchRows(const QList<QTreeWidgetItem *> &items)
3815{
3816 if (!watchTree || items.isEmpty())
3817 {
3818 return;
3819 }
3820 QVector<int> indices;
3821 indices.reserve(items.size());
3822 for (QTreeWidgetItem *it : items)
3823 {
3824 if (!it || it->parent() != nullptr)
3825 {
3826 continue;
3827 }
3828 const int idx = watchTree->indexOfTopLevelItem(it);
3829 if (idx >= 0)
3830 {
3831 indices.append(idx);
3832 }
3833 }
3834 if (indices.isEmpty())
3835 {
3836 return;
3837 }
3838 /* Delete highest-index first so earlier indices remain valid. */
3839 std::sort(indices.begin(), indices.end(), std::greater<int>());
3840 for (int idx : indices)
3841 {
3842 delete watchTree->takeTopLevelItem(idx);
3843 }
3844 refreshWatchDisplay();
3845}
3846
3847QTreeWidgetItem *
3848LuaDebuggerDialog::findVariablesItemByPath(const QString &path) const
3849{
3850 if (!variablesTree || path.isEmpty())
3851 {
3852 return nullptr;
3853 }
3854 const int top = variablesTree->topLevelItemCount();
3855 for (int i = 0; i < top; ++i)
3856 {
3857 QTreeWidgetItem *r =
3858 findVariableItemByPathRecursive(variablesTree->topLevelItem(i),
3859 path);
3860 if (r)
3861 {
3862 return r;
3863 }
3864 }
3865 return nullptr;
3866}
3867
3868QTreeWidgetItem *
3869LuaDebuggerDialog::findWatchRootForVariablePath(const QString &path) const
3870{
3871 if (!watchTree || path.isEmpty())
3872 {
3873 return nullptr;
3874 }
3875 const int n = watchTree->topLevelItemCount();
3876 for (int i = 0; i < n; ++i)
3877 {
3878 QTreeWidgetItem *w = watchTree->topLevelItem(i);
3879 const QString spec = w->data(0, WatchSpecRole).toString();
3880 QString vp = watchResolvedVariablePathForTooltip(spec);
3881 if (vp.isEmpty())
3882 {
3883 vp = watchVariablePathForSpec(spec);
3884 }
3885 if (!vp.isEmpty() && vp == path)
3886 {
3887 return w;
3888 }
3889 if (w->data(0, VariablePathRole).toString() == path)
3890 {
3891 return w;
3892 }
3893 }
3894 return nullptr;
3895}
3896
3897void LuaDebuggerDialog::expandAncestorsOf(QTreeWidget *tree,
3898 QTreeWidgetItem *item)
3899{
3900 if (!tree || !item)
3901 {
3902 return;
3903 }
3904 QList<QTreeWidgetItem *> chain;
3905 for (QTreeWidgetItem *p = item->parent(); p; p = p->parent())
3906 {
3907 chain.prepend(p);
3908 }
3909 for (QTreeWidgetItem *a : chain)
3910 {
3911 tree->expandItem(a);
3912 }
3913}
3914
3915void LuaDebuggerDialog::onVariablesCurrentItemChanged(
3916 QTreeWidgetItem *current, QTreeWidgetItem *previous)
3917{
3918 Q_UNUSED(previous)(void)previous;;
3919 if (syncWatchVariablesSelection_ || !watchTree || !variablesTree || !current)
3920 {
3921 return;
3922 }
3923 const QString path = current->data(0, VariablePathRole).toString();
3924 if (path.isEmpty())
3925 {
3926 return;
3927 }
3928 QTreeWidgetItem *watch = findWatchRootForVariablePath(path);
3929 if (!watch)
3930 {
3931 return;
3932 }
3933 syncWatchVariablesSelection_ = true;
3934 watchTree->setCurrentItem(watch);
3935 watchTree->scrollToItem(watch);
3936 syncWatchVariablesSelection_ = false;
3937}
3938
3939void LuaDebuggerDialog::syncVariablesTreeToCurrentWatch()
3940{
3941 if (syncWatchVariablesSelection_ || !watchTree || !variablesTree)
3942 {
3943 return;
3944 }
3945 QTreeWidgetItem *const cur = watchTree->currentItem();
3946 if (!cur || cur->parent() != nullptr)
3947 {
3948 return;
3949 }
3950 const QString spec = cur->data(0, WatchSpecRole).toString();
3951 if (spec.isEmpty())
3952 {
3953 return;
3954 }
3955 QString path = cur->data(0, VariablePathRole).toString();
3956 if (path.isEmpty())
3957 {
3958 path = watchResolvedVariablePathForTooltip(spec);
3959 if (path.isEmpty())
3960 {
3961 path = watchVariablePathForSpec(spec);
3962 }
3963 }
3964 if (path.isEmpty())
3965 {
3966 return;
3967 }
3968 QTreeWidgetItem *v = findVariablesItemByPath(path);
3969 if (!v)
3970 {
3971 return;
3972 }
3973 syncWatchVariablesSelection_ = true;
3974 expandAncestorsOf(variablesTree, v);
3975 variablesTree->setCurrentItem(v);
3976 variablesTree->scrollToItem(v);
3977 syncWatchVariablesSelection_ = false;
3978}
3979
3980void LuaDebuggerDialog::onWatchCurrentItemChanged(QTreeWidgetItem *current,
3981 QTreeWidgetItem *previous)
3982{
3983 Q_UNUSED(previous)(void)previous;;
3984 if (syncWatchVariablesSelection_ || !watchTree || !variablesTree ||
1
Assuming field 'syncWatchVariablesSelection_' is false
2
Assuming field 'watchTree' is non-null
3
Assuming field 'variablesTree' is non-null
5
Taking false branch
3985 !current)
4
Assuming 'current' is non-null
3986 {
3987 return;
3988 }
3989 if (current->parent() != nullptr)
6
Assuming the condition is false
7
Taking false branch
3990 {
3991 return;
3992 }
3993 const QString spec = current->data(0, WatchSpecRole).toString();
3994 if (spec.isEmpty())
8
Assuming the condition is false
3995 {
3996 return;
3997 }
3998
3999 const bool live = wslua_debugger_is_enabled() && debuggerPaused &&
9
Assuming the condition is true
10
Assuming field 'debuggerPaused' is true
4000 wslua_debugger_is_paused();
4001 if (live)
11
Assuming 'live' is true
12
Taking true branch
4002 {
4003 const int32_t desired = wslua_debugger_find_stack_level_for_watch_spec(
4004 spec.toUtf8().constData());
4005 if (desired >= 0 && desired != stackSelectionLevel)
13
Assuming 'desired' is >= 0
14
Assuming 'desired' is not equal to field 'stackSelectionLevel'
15
Taking true branch
4006 {
4007 stackSelectionLevel = static_cast<int>(desired);
4008 wslua_debugger_set_variable_stack_level(desired);
4009 refreshVariablesForCurrentStackFrame();
16
Calling 'LuaDebuggerDialog::refreshVariablesForCurrentStackFrame'
4010 updateStack();
4011 }
4012 }
4013
4014 syncVariablesTreeToCurrentWatch();
4015}
4016
4017void LuaDebuggerDialog::refreshWatchDisplay()
4018{
4019 if (!watchTree)
4020 {
4021 return;
4022 }
4023 const bool liveContext = wslua_debugger_is_enabled() && debuggerPaused &&
4024 wslua_debugger_is_paused();
4025 const QString muted = QStringLiteral("\u2014")(QString(QtPrivate::qMakeStringPrivate(u"" "\u2014")));
4026 const int n = watchTree->topLevelItemCount();
4027 for (int i = 0; i < n; ++i)
4028 {
4029 QTreeWidgetItem *root = watchTree->topLevelItem(i);
4030 applyWatchItemState(root, liveContext, muted);
4031 if (liveContext && root && root->isExpanded())
4032 {
4033 refreshWatchBranch(root);
4034 }
4035 }
4036}
4037
4038void LuaDebuggerDialog::applyWatchItemEmpty(QTreeWidgetItem *item,
4039 const QString &muted,
4040 const QString &watchTipExtra)
4041{
4042 clearWatchFilterErrorChrome(item, watchTree);
4043 item->setText(1, muted);
4044 item->setToolTip(0, watchTipExtra);
4045 /* Explain the muted em dash instead of leaving an empty tooltip: a blank
4046 * row has no variable path to evaluate, so there is nothing to show in
4047 * the Value column. */
4048 item->setToolTip(
4049 1,
4050 capWatchTooltipText(
4051 tr("No watch path entered yet — enter a variable path in the "
4052 "Watch column to see a value here.")));
4053 QFont f1 = item->font(1);
4054 f1.setBold(false);
4055 item->setFont(1, f1);
4056 item->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
4057 while (item->childCount() > 0)
4058 {
4059 delete item->takeChild(0);
4060 }
4061}
4062
4063void LuaDebuggerDialog::applyWatchItemNonPath(QTreeWidgetItem *item,
4064 const QString &watchTipExtra)
4065{
4066 /* Defensive: normal entry points reject non-path specs before a row is
4067 * created, but hand-edited lua_debugger.json could still supply one. */
4068 applyWatchFilterErrorChrome(item, watchTree);
4069 item->setText(1, tr("Not a variable path"));
4070 item->setToolTip(
4071 0,
4072 capWatchTooltipText(
4073 QStringLiteral("%1\n%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1\n%2")))
4074 .arg(item->text(0),
4075 tr("Use a Variables-style path (e.g. Locals.x, "
4076 "Globals.t.k, t[1], t[\"k\"], or a single identifier).")) +
4077 watchTipExtra));
4078 item->setToolTip(
4079 1, capWatchTooltipText(tr("Only variable paths can be watched.")));
4080 QFont fe = item->font(1);
4081 fe.setBold(false);
4082 item->setFont(1, fe);
4083 item->setData(0, WatchChildSnapRole, QVariant());
4084 item->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
4085 while (item->childCount() > 0)
4086 {
4087 delete item->takeChild(0);
4088 }
4089}
4090
4091void LuaDebuggerDialog::applyWatchItemNoLiveContext(QTreeWidgetItem *item,
4092 const QString &muted,
4093 const QString &watchTipExtra)
4094{
4095 item->setText(1, muted);
4096 item->setForeground(1, watchTree->palette().brush(QPalette::PlaceholderText));
4097 /* Replace the previous `muted \n Type: muted` tooltip (which just
4098 * repeated the em dash) with a short explanation so the user knows
4099 * *why* there is no value: watches are only evaluated while the
4100 * debugger is paused. */
4101 const QString mutedReason =
4102 wslua_debugger_is_enabled()
4103 ? tr("Value shown only while the debugger is paused.")
4104 : tr("Value shown only while the debugger is paused. "
4105 "The debugger is currently disabled.");
4106 const QString ttSuf = tr("Type: %1").arg(muted);
4107 item->setToolTip(
4108 0,
4109 capWatchTooltipText(
4110 QStringLiteral("%1\n%2\n%3")(QString(QtPrivate::qMakeStringPrivate(u"" "%1\n%2\n%3")))
4111 .arg(item->text(0), mutedReason, ttSuf) +
4112 watchTipExtra));
4113 item->setToolTip(1, capWatchTooltipText(mutedReason));
4114 QFont f1 = item->font(1);
4115 f1.setBold(false);
4116 item->setFont(1, f1);
4117 item->setData(0, WatchLastValueRole, QVariant());
4118 item->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
4119 if (item->parent() == nullptr)
4120 {
4121 item->setData(0, WatchChildSnapRole, QVariant());
4122 while (item->childCount() > 0)
4123 {
4124 delete item->takeChild(0);
4125 }
4126 }
4127}
4128
4129void LuaDebuggerDialog::applyWatchItemError(QTreeWidgetItem *item,
4130 const QString &errStr,
4131 const QString &watchTipExtra)
4132{
4133 applyWatchFilterErrorChrome(item, watchTree);
4134 item->setText(1, errStr);
4135 const QString ttSuf = tr("Type: %1").arg(tr("error"));
4136 item->setToolTip(
4137 0,
4138 capWatchTooltipText(
4139 QStringLiteral("%1\n%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1\n%2"))).arg(item->text(0), ttSuf) + watchTipExtra));
4140 item->setToolTip(
4141 1,
4142 capWatchTooltipText(
4143 QStringLiteral("%1\n%2\n%3")(QString(QtPrivate::qMakeStringPrivate(u"" "%1\n%2\n%3")))
4144 .arg(tr("Invalid watch path."), errStr, ttSuf)));
4145 item->setData(0, WatchChildSnapRole, QVariant());
4146 item->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
4147 while (item->childCount() > 0)
4148 {
4149 delete item->takeChild(0);
4150 }
4151}
4152
4153void LuaDebuggerDialog::applyWatchItemSuccess(QTreeWidgetItem *item,
4154 const QString &spec,
4155 const char *val, const char *typ,
4156 bool can_expand,
4157 const QString &watchTipExtra)
4158{
4159 if (item->parent() == nullptr)
4160 {
4161 watchRootSetVariablePathRoleFromSpec(item, spec);
4162 }
4163 const QString v = val ? QString::fromUtf8(val) : QString();
4164 const QString typStr = typ ? QString::fromUtf8(typ) : QString();
4165 const QString prev = item->data(0, WatchLastValueRole).toString();
4166 item->setText(1, v);
4167 const QString ttSuf =
4168 typStr.isEmpty() ? QString() : tr("Type: %1").arg(typStr);
4169 item->setToolTip(
4170 0,
4171 capWatchTooltipText(
4172 (ttSuf.isEmpty()
4173 ? item->text(0)
4174 : QStringLiteral("%1\n%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1\n%2"))).arg(item->text(0), ttSuf)) +
4175 watchTipExtra));
4176 item->setToolTip(
4177 1,
4178 capWatchTooltipText(
4179 ttSuf.isEmpty() ? v : QStringLiteral("%1\n%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1\n%2"))).arg(v, ttSuf)));
4180 QFont f1 = item->font(1);
4181 f1.setBold(!prev.isEmpty() && prev != v);
4182 item->setFont(1, f1);
4183 item->setData(0, WatchLastValueRole, v);
4184
4185 if (can_expand)
4186 {
4187 item->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);
4188 if (item->childCount() == 0)
4189 {
4190 QTreeWidgetItem *const ph = new QTreeWidgetItem(item);
4191 ph->setFlags(Qt::ItemIsEnabled);
4192 }
4193 }
4194 else
4195 {
4196 item->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
4197 while (item->childCount() > 0)
4198 {
4199 delete item->takeChild(0);
4200 }
4201 }
4202}
4203
4204void LuaDebuggerDialog::applyWatchItemState(QTreeWidgetItem *item,
4205 bool liveContext,
4206 const QString &muted)
4207{
4208 if (!item)
4209 {
4210 return;
4211 }
4212
4213 const QString spec = item->data(0, WatchSpecRole).toString();
4214 const QString watchTipExtra = watchPathOriginSuffix(item, spec);
4215
4216 if (item->parent() == nullptr && spec.isEmpty())
4217 {
4218 applyWatchItemEmpty(item, muted, watchTipExtra);
4219 return;
4220 }
4221
4222 if (!watchSpecUsesPathResolution(spec))
4223 {
4224 applyWatchItemNonPath(item, watchTipExtra);
4225 return;
4226 }
4227
4228 clearWatchFilterErrorChrome(item, watchTree);
4229 item->setForeground(1, watchTree->palette().brush(QPalette::Text));
4230
4231 if (!liveContext)
4232 {
4233 applyWatchItemNoLiveContext(item, muted, watchTipExtra);
4234 return;
4235 }
4236
4237 char *val = nullptr;
4238 char *typ = nullptr;
4239 bool can_expand = false;
4240 char *err = nullptr;
4241 const bool ok = wslua_debugger_watch_read_root(
4242 spec.toUtf8().constData(), &val, &typ, &can_expand, &err);
4243 if (!ok)
4244 {
4245 const QString errStr = err ? QString::fromUtf8(err) : muted;
4246 applyWatchItemError(item, errStr, watchTipExtra);
4247 g_free(err);
4248 return;
4249 }
4250
4251 applyWatchItemSuccess(item, spec, val, typ, can_expand, watchTipExtra);
4252 g_free(val);
4253 g_free(typ);
4254}
4255
4256void LuaDebuggerDialog::fillWatchPathChildren(QTreeWidgetItem *parent,
4257 const QString &path)
4258{
4259 /* Path watches drill down with wslua_debugger_get_variables (same tree as
4260 * Variables); expression watches use wslua_debugger_watch_* elsewhere. */
4261 if (watchSubpathBoundaryCount(path) >= WSLUA_WATCH_MAX_PATH_SEGMENTS32)
4262 {
4263 QTreeWidgetItem *sent = new QTreeWidgetItem(parent);
4264 sent->setText(0, QStringLiteral("\u2026")(QString(QtPrivate::qMakeStringPrivate(u"" "\u2026"))));
4265 sent->setText(1, tr("Maximum watch depth reached"));
4266 sent->setFlags(Qt::ItemIsEnabled);
4267 QTreeWidget *tw = parent->treeWidget();
4268 if (tw)
4269 {
4270 sent->setForeground(
4271 1, tw->palette().brush(QPalette::PlaceholderText));
4272 }
4273 sent->setToolTip(
4274 1, capWatchTooltipText(tr("Maximum watch depth reached.")));
4275 return;
4276 }
4277
4278 int32_t variableCount = 0;
4279 wslua_variable_t *variables = wslua_debugger_get_variables(
4280 path.isEmpty() ? NULL__null : path.toUtf8().constData(), &variableCount);
4281
4282 if (!variables)
4283 {
4284 return;
4285 }
4286
4287 for (int32_t variableIndex = 0; variableIndex < variableCount;
4288 ++variableIndex)
4289 {
4290 QTreeWidgetItem *item = new QTreeWidgetItem(parent);
4291
4292 const VariableRowFields f =
4293 readVariableRowFields(variables[variableIndex], path);
4294
4295 item->setText(0, f.name);
4296 item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
4297 item->setData(0, VariableTypeRole, f.type);
4298 item->setData(0, VariableCanExpandRole, f.canExpand);
4299 item->setData(0, VariablePathRole, f.childPath);
4300
4301 applyWatchChildRowPresentation(item, f.childPath, f.value, f.type);
4302
4303 applyVariableExpansionIndicator(item, f.canExpand,
4304 /*enabledOnlyPlaceholder=*/true);
4305 }
4306
4307 if (variableChildrenShouldSortByName(path))
4308 {
4309 parent->sortChildren(0, Qt::AscendingOrder);
4310 }
4311
4312 wslua_debugger_free_variables(variables, variableCount);
4313}
4314
4315namespace
4316{
4317/** Subpath / variable-path key used to address @p item inside a watch root. */
4318static QString watchItemExpansionKey(const QTreeWidgetItem *item)
4319{
4320 if (!item || !item->parent())
4321 {
4322 return QString();
4323 }
4324 const QString sp = item->data(0, WatchSubpathRole).toString();
4325 if (!sp.isEmpty())
4326 {
4327 return sp;
4328 }
4329 return item->data(0, VariablePathRole).toString();
4330}
4331} // namespace
4332
4333void LuaDebuggerDialog::recordTreeSectionRootExpansion(
4334 QHash<QString, TreeSectionExpansionState> &map, const QString &rootKey,
4335 bool expanded)
4336{
4337 if (rootKey.isEmpty())
4338 {
4339 return;
4340 }
4341 if (!expanded && !map.contains(rootKey))
4342 {
4343 return;
4344 }
4345 TreeSectionExpansionState &e = map[rootKey];
4346 e.rootExpanded = expanded;
4347 if (!expanded && e.subpaths.isEmpty())
4348 {
4349 map.remove(rootKey);
4350 }
4351}
4352
4353void LuaDebuggerDialog::recordTreeSectionSubpathExpansion(
4354 QHash<QString, TreeSectionExpansionState> &map, const QString &rootKey,
4355 const QString &key, bool expanded)
4356{
4357 if (rootKey.isEmpty() || key.isEmpty())
4358 {
4359 return;
4360 }
4361 if (expanded)
4362 {
4363 TreeSectionExpansionState &e = map[rootKey];
4364 if (!e.subpaths.contains(key))
4365 {
4366 e.subpaths.append(key);
4367 }
4368 }
4369 else
4370 {
4371 auto it = map.find(rootKey);
4372 if (it == map.end())
4373 {
4374 return;
4375 }
4376 it->subpaths.removeAll(key);
4377 if (!it->rootExpanded && it->subpaths.isEmpty())
4378 {
4379 map.erase(it);
4380 }
4381 }
4382}
4383
4384QStringList LuaDebuggerDialog::treeSectionExpandedSubpaths(
4385 const QHash<QString, TreeSectionExpansionState> &map,
4386 const QString &rootKey) const
4387{
4388 if (rootKey.isEmpty())
4389 {
4390 return QStringList();
4391 }
4392 const auto it = map.constFind(rootKey);
4393 if (it == map.constEnd())
4394 {
4395 return QStringList();
4396 }
4397 return it->subpaths;
4398}
4399
4400void LuaDebuggerDialog::recordWatchRootExpansion(const QString &rootSpec,
4401 bool expanded)
4402{
4403 recordTreeSectionRootExpansion(watchExpansion_, rootSpec, expanded);
4404}
4405
4406void LuaDebuggerDialog::recordWatchSubpathExpansion(const QString &rootSpec,
4407 const QString &key,
4408 bool expanded)
4409{
4410 recordTreeSectionSubpathExpansion(watchExpansion_, rootSpec, key, expanded);
4411}
4412
4413QStringList
4414LuaDebuggerDialog::watchExpandedSubpathsForSpec(const QString &rootSpec) const
4415{
4416 return treeSectionExpandedSubpaths(watchExpansion_, rootSpec);
4417}
4418
4419void LuaDebuggerDialog::pruneWatchExpansionMap()
4420{
4421 if (!watchTree || watchExpansion_.isEmpty())
4422 {
4423 return;
4424 }
4425 QSet<QString> liveSpecs;
4426 const int n = watchTree->topLevelItemCount();
4427 for (int i = 0; i < n; ++i)
4428 {
4429 const QTreeWidgetItem *it = watchTree->topLevelItem(i);
4430 if (!it)
4431 {
4432 continue;
4433 }
4434 const QString spec = it->data(0, WatchSpecRole).toString();
4435 if (!spec.isEmpty())
4436 {
4437 liveSpecs.insert(spec);
4438 }
4439 }
4440 for (auto it = watchExpansion_.begin(); it != watchExpansion_.end();)
4441 {
4442 if (!liveSpecs.contains(it.key()))
4443 {
4444 it = watchExpansion_.erase(it);
4445 }
4446 else
4447 {
4448 ++it;
4449 }
4450 }
4451}
4452
4453void LuaDebuggerDialog::onWatchItemExpanded(QTreeWidgetItem *item)
4454{
4455 if (!item)
4456 {
4457 return;
4458 }
4459 /* Track expansion in the runtime map. This fires for both user-driven
4460 * expansion and the programmatic setExpanded(true) calls made by
4461 * reexpandWatchDescendantsByPathKeys; re-recording an already-tracked
4462 * key is idempotent. */
4463 const QTreeWidgetItem *const rootWatch = watchRootItem(item);
4464 const QString rootSpec =
4465 rootWatch ? rootWatch->data(0, WatchSpecRole).toString() : QString();
4466 if (!item->parent())
4467 {
4468 recordWatchRootExpansion(rootSpec, true);
4469 }
4470 else
4471 {
4472 recordWatchSubpathExpansion(rootSpec, watchItemExpansionKey(item),
4473 true);
4474 }
4475
4476 if (item->childCount() == 1 && item->child(0)->text(0).isEmpty())
4477 {
4478 delete item->takeChild(0);
4479 }
4480 else if (item->childCount() > 0)
4481 {
4482 return;
4483 }
4484
4485 refillWatchChildren(item);
4486}
4487
4488void LuaDebuggerDialog::onWatchItemCollapsed(QTreeWidgetItem *item)
4489{
4490 if (!item)
4491 {
4492 return;
4493 }
4494 const QTreeWidgetItem *const rootWatch = watchRootItem(item);
4495 const QString rootSpec =
4496 rootWatch ? rootWatch->data(0, WatchSpecRole).toString() : QString();
4497 if (!item->parent())
4498 {
4499 recordWatchRootExpansion(rootSpec, false);
4500 }
4501 else
4502 {
4503 recordWatchSubpathExpansion(rootSpec, watchItemExpansionKey(item),
4504 false);
4505 }
4506}
4507
4508void LuaDebuggerDialog::refillWatchChildren(QTreeWidgetItem *item)
4509{
4510 if (!item)
4511 {
4512 return;
4513 }
4514 while (item->childCount() > 0)
4515 {
4516 delete item->takeChild(0);
4517 }
4518
4519 const QTreeWidgetItem *const rootWatch = watchRootItem(item);
4520 const QString rootSpec = rootWatch->data(0, WatchSpecRole).toString();
4521 QString path = item->data(0, VariablePathRole).toString();
4522 if (path.isEmpty())
4523 {
4524 path = watchResolvedVariablePathForTooltip(rootSpec);
4525 if (path.isEmpty())
4526 {
4527 path = watchVariablePathForSpec(rootSpec);
4528 }
4529 }
4530 fillWatchPathChildren(item, path);
4531}
4532
4533void LuaDebuggerDialog::refreshWatchBranch(QTreeWidgetItem *item)
4534{
4535 if (!item || !item->isExpanded())
4536 {
4537 return;
4538 }
4539 /* refillWatchChildren deletes and re-creates every descendant, so the
4540 * tree alone cannot remember which sub-elements were expanded. Instead,
4541 * consult the dialog-level runtime expansion map (watchExpansion_),
4542 * which is kept up to date by onWatchItemExpanded / onWatchItemCollapsed
4543 * and survives both refills and the children-clearing that happens while
4544 * the debugger is not paused. This lets deep subtrees survive stepping,
4545 * pause / resume, and Variables tree refreshes without being tied to
4546 * transient QTreeWidgetItem lifetimes. */
4547 const QTreeWidgetItem *const rootWatch = watchRootItem(item);
4548 const QString rootSpec =
4549 rootWatch ? rootWatch->data(0, WatchSpecRole).toString() : QString();
4550 refillWatchChildren(item);
4551 reexpandWatchDescendantsByPathKeys(
4552 item, watchExpandedSubpathsForSpec(rootSpec));
4553}
4554
4555namespace
4556{
4557/** Pointers into the context menu built by buildWatchContextMenu(). */
4558struct WatchContextMenuActions
4559{
4560 QAction *addWatch = nullptr;
4561 QAction *copyValue = nullptr;
4562 QAction *remove = nullptr;
4563 QAction *duplicate = nullptr;
4564 QAction *moveUp = nullptr;
4565 QAction *moveDown = nullptr;
4566};
4567} /* namespace */
4568
4569/**
4570 * Populate @a menu with the watch context-menu actions appropriate for
4571 * @a item (may be null / a child row), returning pointers to each action
4572 * so the caller can dispatch on the chosen QAction.
4573 *
4574 * Sub-element rows (descendants of a watch root) only expose `Add Watch…`
4575 * and `Copy Value`; the remaining entries (Remove, Duplicate, Move
4576 * Up/Down) operate on the watch list itself and therefore only make
4577 * sense on watch roots.
4578 */
4579static void buildWatchContextMenu(QMenu &menu, QTreeWidgetItem *item,
4580 WatchContextMenuActions *acts)
4581{
4582 acts->addWatch = menu.addAction(QObject::tr("Add Watch…"));
4583 if (!item)
4584 {
4585 return;
4586 }
4587
4588 menu.addSeparator();
4589 acts->copyValue = menu.addAction(QObject::tr("Copy Value"));
4590
4591 if (item->parent() != nullptr)
4592 {
4593 return;
4594 }
4595
4596 menu.addSeparator();
4597 acts->moveUp = menu.addAction(QObject::tr("Move Up"));
4598 acts->moveDown = menu.addAction(QObject::tr("Move Down"));
4599 acts->duplicate = menu.addAction(QObject::tr("Duplicate Watch"));
4600 acts->remove = menu.addAction(QObject::tr("Remove"));
4601}
4602
4603void LuaDebuggerDialog::onWatchContextMenuRequested(const QPoint &pos)
4604{
4605 if (!watchTree)
4606 {
4607 return;
4608 }
4609
4610 QTreeWidgetItem *item = watchTree->itemAt(pos);
4611
4612 QMenu menu(this);
4613 WatchContextMenuActions acts;
4614 buildWatchContextMenu(menu, item, &acts);
4615
4616 QAction *chosen = menu.exec(watchTree->viewport()->mapToGlobal(pos));
4617 if (!chosen)
4618 {
4619 return;
4620 }
4621
4622 if (chosen == acts.addWatch)
4623 {
4624 insertNewWatchRow(QString(), true);
4625 return;
4626 }
4627 if (!item)
4628 {
4629 return;
4630 }
4631
4632 auto copyToClipboard = [](const QString &text)
4633 {
4634 if (QClipboard *clipboard = QGuiApplication::clipboard())
4635 {
4636 clipboard->setText(text);
4637 }
4638 };
4639
4640 /* Copy value works on both watch roots and sub-element rows — the
4641 * value column is populated uniformly by applyWatchItemState (roots)
4642 * and applyWatchChildRowPresentation (descendants).
4643 *
4644 * The tree's value column shows a truncated preview
4645 * (WSLUA_DEBUGGER_PREVIEW_MAX_BYTES in the engine); for "Copy Value"
4646 * we re-read the live, untruncated stringification via
4647 * wslua_debugger_read_variable_value_full so long strings and
4648 * Tvb / ByteArray dumps copy in full. If the debugger is not paused
4649 * we fall back to whatever the tree currently shows — the engine has
4650 * no live state to re-query then. */
4651 if (chosen == acts.copyValue)
4652 {
4653 QString value;
4654 const QString varPath = item->data(0, VariablePathRole).toString();
4655 if (!varPath.isEmpty() && debuggerPaused &&
4656 wslua_debugger_is_enabled() && wslua_debugger_is_paused())
4657 {
4658 char *val = nullptr;
4659 char *err = nullptr;
4660 if (wslua_debugger_read_variable_value_full(
4661 varPath.toUtf8().constData(), &val, &err))
4662 {
4663 value = QString::fromUtf8(val ? val : "");
4664 }
4665 g_free(val);
4666 g_free(err);
4667 }
4668 if (value.isNull())
4669 {
4670 value = item->text(1);
4671 }
4672 copyToClipboard(value);
4673 return;
4674 }
4675
4676 if (item->parent() != nullptr)
4677 {
4678 return;
4679 }
4680
4681 if (chosen == acts.remove)
4682 {
4683 QList<QTreeWidgetItem *> del;
4684 for (QTreeWidgetItem *it : watchTree->selectedItems())
4685 {
4686 if (it && it->parent() == nullptr)
4687 {
4688 del.append(it);
4689 }
4690 }
4691 if (del.isEmpty())
4692 {
4693 del.append(item);
4694 }
4695 deleteWatchRows(del);
4696 return;
4697 }
4698
4699 const int idx = watchTree->indexOfTopLevelItem(item);
4700 if (chosen == acts.duplicate)
4701 {
4702 QTreeWidgetItem *copy = new QTreeWidgetItem();
4703 copy->setFlags(copy->flags() | Qt::ItemIsEditable | Qt::ItemIsEnabled |
4704 Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
4705 copy->setText(0, item->text(0));
4706 for (int r = WatchSpecRole; r <= WatchPendingNewRole; ++r)
4707 {
4708 copy->setData(0, r, item->data(0, r));
4709 }
4710 copy->setData(0, WatchPendingNewRole, QVariant(false));
4711 copy->setData(0, VariablePathRole, item->data(0, VariablePathRole));
4712 copy->setData(0, VariableTypeRole, item->data(0, VariableTypeRole));
4713 copy->setData(0, VariableCanExpandRole,
4714 item->data(0, VariableCanExpandRole));
4715 copy->setData(0, WatchLastValueRole, QVariant());
4716 copy->setData(0, WatchChildSnapRole, QVariant());
4717 copy->setExpanded(false);
4718 if (copy->childCount() == 0)
4719 {
4720 QTreeWidgetItem *const ph = new QTreeWidgetItem(copy);
4721 ph->setFlags(Qt::ItemIsEnabled);
4722 }
4723 watchTree->insertTopLevelItem(idx + 1, copy);
4724 refreshWatchDisplay();
4725 return;
4726 }
4727 if (chosen == acts.moveUp && idx > 0)
4728 {
4729 QTreeWidgetItem *taken = watchTree->takeTopLevelItem(idx);
4730 watchTree->insertTopLevelItem(idx - 1, taken);
4731 return;
4732 }
4733 if (chosen == acts.moveDown && idx >= 0 &&
4734 idx < watchTree->topLevelItemCount() - 1)
4735 {
4736 QTreeWidgetItem *taken = watchTree->takeTopLevelItem(idx);
4737 watchTree->insertTopLevelItem(idx + 1, taken);
4738 return;
4739 }
4740}
4741
4742void LuaDebuggerDialog::addPathWatch(const QString &debuggerPath)
4743{
4744 insertNewWatchRow(debuggerPath, false);
4745}
4746
4747void LuaDebuggerDialog::showPathOnlyVariablePathWatchMessage()
4748{
4749 QMessageBox::information(
4750 this, tr("Lua Debugger"),
4751 tr("Only variable paths can be watched (e.g. Locals.name, Globals.x, "
4752 "or a single identifier for Locals.name)."));
4753}
4754
4755void LuaDebuggerDialog::commitWatchRootSpec(QTreeWidgetItem *item,
4756 const QString &text)
4757{
4758 if (!watchTree || !item || item->parent() != nullptr)
4759 {
4760 return;
4761 }
4762
4763 const QString t = text.trimmed();
4764 if (t.isEmpty())
4765 {
4766 /* Clearing the text of a brand-new row discards it (no persisted
4767 * entry ever existed); clearing an existing row removes it. */
4768 if (item->data(0, WatchPendingNewRole).toBool())
4769 {
4770 delete item;
4771 refreshWatchDisplay();
4772 }
4773 else
4774 {
4775 deleteWatchRows({item});
4776 }
4777 return;
4778 }
4779
4780 if (t.size() > WATCH_EXPR_MAX_CHARS)
4781 {
4782 QMessageBox::warning(
4783 this, tr("Lua Debugger"),
4784 tr("Watch path is too long (maximum %Ln characters).", "",
4785 static_cast<qlonglong>(WATCH_EXPR_MAX_CHARS)));
4786 return;
4787 }
4788
4789 if (!watchSpecUsesPathResolution(t))
4790 {
4791 showPathOnlyVariablePathWatchMessage();
4792 return;
4793 }
4794
4795 item->setData(0, WatchSpecRole, t);
4796 item->setText(0, t);
4797 item->setData(0, WatchPendingNewRole, QVariant(false));
4798 watchRootSetVariablePathRoleFromSpec(item, t);
4799 if (item->childCount() == 0)
4800 {
4801 QTreeWidgetItem *const ph = new QTreeWidgetItem(item);
4802 ph->setFlags(Qt::ItemIsEnabled);
4803 }
4804 refreshWatchDisplay();
4805}
4806
4807void LuaDebuggerDialog::insertNewWatchRow(const QString &initialSpec,
4808 bool openEditor)
4809{
4810 if (!watchTree)
4811 {
4812 return;
4813 }
4814
4815 const QString init = initialSpec.trimmed();
4816 if (!init.isEmpty() && !watchSpecUsesPathResolution(init))
4817 {
4818 showPathOnlyVariablePathWatchMessage();
4819 return;
4820 }
4821
4822 QTreeWidgetItem *row = new QTreeWidgetItem(watchTree);
4823 row->setFlags(row->flags() | Qt::ItemIsEditable | Qt::ItemIsEnabled |
4824 Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
4825 row->setData(0, WatchSpecRole, init);
4826 row->setText(0, init);
4827 row->setData(0, WatchSubpathRole, QString());
4828 row->setData(0, WatchPendingNewRole, init.isEmpty());
4829 if (!init.isEmpty())
4830 {
4831 watchRootSetVariablePathRoleFromSpec(row, init);
4832 }
4833 {
4834 QTreeWidgetItem *const ph = new QTreeWidgetItem(row);
4835 ph->setFlags(Qt::ItemIsEnabled);
4836 }
4837 refreshWatchDisplay();
4838
4839 if (openEditor)
4840 {
4841 QTimer::singleShot(0, this, [this, row]()
4842 {
4843 watchTree->scrollToItem(row);
4844 watchTree->setCurrentItem(row);
4845 watchTree->editItem(row, 0);
4846 });
4847 }
4848}
4849
4850void LuaDebuggerDialog::restoreWatchExpansionState()
4851{
4852 if (!watchTree)
4853 {
4854 return;
4855 }
4856 /* Re-apply each root's expansion from the runtime map. After a fresh load
4857 * from lua_debugger.json the map is empty (rows open collapsed). */
4858 for (int i = 0; i < watchTree->topLevelItemCount(); ++i)
4859 {
4860 QTreeWidgetItem *root = watchTree->topLevelItem(i);
4861 const QString spec = root->data(0, WatchSpecRole).toString();
4862 bool rootExpanded = false;
4863 QStringList subpaths;
4864 const auto it = watchExpansion_.constFind(spec);
4865 if (it != watchExpansion_.cend())
4866 {
4867 rootExpanded = it->rootExpanded;
4868 subpaths = it->subpaths;
4869 }
4870 if (rootExpanded != root->isExpanded())
4871 {
4872 root->setExpanded(rootExpanded);
4873 }
4874 if (rootExpanded)
4875 {
4876 reexpandTreeDescendantsByPathKeys(root, subpaths,
4877 findWatchItemBySubpathOrPathKey);
4878 }
4879 }
4880}
4881
4882void LuaDebuggerDialog::restoreVariablesExpansionState()
4883{
4884 if (!variablesTree)
4885 {
4886 return;
4887 }
4888 for (int i = 0; i < variablesTree->topLevelItemCount(); ++i)
4889 {
4890 QTreeWidgetItem *root = variablesTree->topLevelItem(i);
4891 const QString section = root->data(0, VariablePathRole).toString();
4892 if (section.isEmpty())
4893 {
4894 continue;
4895 }
4896 bool rootExpanded = false;
4897 QStringList subpaths;
4898 const auto it = variablesExpansion_.constFind(section);
4899 if (it == variablesExpansion_.cend())
4900 {
4901 if (section == QLatin1String("Locals"))
4902 {
4903 rootExpanded = true;
4904 }
4905 }
4906 else
4907 {
4908 rootExpanded = it->rootExpanded;
4909 subpaths = it->subpaths;
4910 }
4911 if (rootExpanded != root->isExpanded())
4912 {
4913 root->setExpanded(rootExpanded);
4914 }
4915 if (rootExpanded)
4916 {
4917 reexpandTreeDescendantsByPathKeys(root, subpaths,
4918 findVariableTreeItemByPathKey);
4919 }
4920 }
4921}
4922
4923// Qt-based JSON Settings Persistence
4924void LuaDebuggerDialog::loadSettingsFile()
4925{
4926 const QString path = luaDebuggerSettingsFilePath();
4927 QFileInfo fi(path);
4928 if (!fi.exists() || !fi.isFile())
4929 {
4930 return;
4931 }
4932
4933 QFile loadFile(path);
4934 if (!loadFile.open(QIODevice::ReadOnly))
4935 {
4936 return;
4937 }
4938
4939 QByteArray loadData = loadFile.readAll();
4940 if (loadData.startsWith("\xef\xbb\xbf"))
4941 {
4942 loadData = loadData.mid(3);
4943 }
4944 loadData = loadData.trimmed();
4945
4946 QJsonParseError parseError;
4947 const QJsonDocument document =
4948 QJsonDocument::fromJson(loadData, &parseError);
4949 if (parseError.error != QJsonParseError::NoError || !document.isObject())
4950 {
4951 return;
4952 }
4953 settings_ = document.object().toVariantMap();
4954}
4955
4956void LuaDebuggerDialog::saveSettingsFile()
4957{
4958 /*
4959 * Always merge live watch rows and engine breakpoints before writing so
4960 * callers that only touch theme/splitters (or watches alone) do not persist
4961 * stale or empty breakpoint/watch entries.
4962 */
4963 if (watchTree)
4964 {
4965 storeWatchList();
4966 }
4967 storeBreakpointsList();
4968
4969 const QString savePath = luaDebuggerSettingsFilePath();
4970 QFileInfo fileInfo(savePath);
4971
4972 QFile saveFile(savePath);
4973 if (fileInfo.exists() && !fileInfo.isFile())
4974 {
4975 return;
4976 }
4977
4978 if (saveFile.open(QIODevice::WriteOnly))
4979 {
4980 QJsonDocument document(QJsonObject::fromVariantMap(settings_));
4981 QByteArray saveData = document.toJson(QJsonDocument::Indented);
4982 saveFile.write(saveData);
4983 }
4984}
4985
4986void LuaDebuggerDialog::applyDialogSettings()
4987{
4988 loadSettingsFile();
4989
4990 /*
4991 * Load JSON into the engine and watch tree. JSON is read only here (dialog
4992 * construction); it is written only from closeEvent() (see saveSettingsFile).
4993 * Apply breakpoints first so that list is never empty before rebuild.
4994 */
4995 QJsonArray breakpointsArray =
4996 jsonArrayFromSettingsMap(settings_, SettingsKeys::Breakpoints);
4997 for (const QJsonValue &val : breakpointsArray)
4998 {
4999 QJsonObject bp = val.toObject();
5000 QString file = bp.value("file").toString();
5001 int64_t line = bp.value("line").toVariant().toLongLong();
5002 bool active = bp.value("active").toBool(true);
5003
5004 if (!file.isEmpty() && line > 0)
5005 {
5006 int32_t state = wslua_debugger_get_breakpoint_state(
5007 file.toUtf8().constData(), line);
5008 if (state < 0)
5009 {
5010 wslua_debugger_add_breakpoint(file.toUtf8().constData(), line);
5011 }
5012 wslua_debugger_set_breakpoint_active(file.toUtf8().constData(),
5013 line, active);
5014 }
5015 }
5016
5017 rebuildWatchTreeFromSettings();
5018
5019 // Apply theme setting
5020 QString themeStr = settings_.value(SettingsKeys::Theme, "auto").toString();
5021 int32_t theme = WSLUA_DEBUGGER_THEME_AUTO;
5022 if (themeStr == "dark")
5023 theme = WSLUA_DEBUGGER_THEME_DARK;
5024 else if (themeStr == "light")
5025 theme = WSLUA_DEBUGGER_THEME_LIGHT;
5026 currentTheme_ = theme;
5027
5028 if (themeComboBox)
5029 {
5030 int idx = themeComboBox->findData(theme);
5031 if (idx >= 0)
5032 themeComboBox->setCurrentIndex(idx);
5033 }
5034
5035 QString mainSplitterHex =
5036 settings_.value(SettingsKeys::MainSplitter).toString();
5037 QString leftSplitterHex =
5038 settings_.value(SettingsKeys::LeftSplitter).toString();
5039
5040 bool splittersRestored = false;
5041 if (!mainSplitterHex.isEmpty() && ui->mainSplitter)
5042 {
5043 ui->mainSplitter->restoreState(
5044 QByteArray::fromHex(mainSplitterHex.toLatin1()));
5045 splittersRestored = true;
5046 }
5047 if (!leftSplitterHex.isEmpty() && ui->leftSplitter)
5048 {
5049 ui->leftSplitter->restoreState(
5050 QByteArray::fromHex(leftSplitterHex.toLatin1()));
5051 splittersRestored = true;
5052 }
5053
5054 if (!splittersRestored && ui->mainSplitter)
5055 {
5056 ui->mainSplitter->setStretchFactor(0, 1);
5057 ui->mainSplitter->setStretchFactor(1, 2);
5058 QList<int> sizes;
5059 sizes << 300 << 600;
5060 ui->mainSplitter->setSizes(sizes);
5061 }
5062
5063 if (variablesSection)
5064 variablesSection->setExpanded(
5065 settings_.value(SettingsKeys::SectionVariables, true).toBool());
5066 if (stackSection)
5067 stackSection->setExpanded(
5068 settings_.value(SettingsKeys::SectionStack, true).toBool());
5069 if (breakpointsSection)
5070 breakpointsSection->setExpanded(
5071 settings_.value(SettingsKeys::SectionBreakpoints, true).toBool());
5072 if (filesSection)
5073 filesSection->setExpanded(
5074 settings_.value(SettingsKeys::SectionFiles, false).toBool());
5075 if (evalSection)
5076 evalSection->setExpanded(
5077 settings_.value(SettingsKeys::SectionEval, false).toBool());
5078 if (settingsSection)
5079 settingsSection->setExpanded(
5080 settings_.value(SettingsKeys::SectionSettings, false).toBool());
5081 if (watchSection)
5082 watchSection->setExpanded(
5083 settings_.value(SettingsKeys::SectionWatch, true).toBool());
5084}
5085
5086void LuaDebuggerDialog::storeDialogSettings()
5087{
5088 /*
5089 * Refresh settings_ from UI only (no disk I/O). JSON is written from
5090 * closeEvent() via saveSettingsFile().
5091 */
5092 // Store theme from combo box (or current C-side value)
5093 int32_t theme = WSLUA_DEBUGGER_THEME_AUTO;
5094 if (themeComboBox)
5095 {
5096 theme = themeComboBox->itemData(themeComboBox->currentIndex()).toInt();
5097 }
5098 if (theme == WSLUA_DEBUGGER_THEME_DARK)
5099 settings_[SettingsKeys::Theme] = "dark";
5100 else if (theme == WSLUA_DEBUGGER_THEME_LIGHT)
5101 settings_[SettingsKeys::Theme] = "light";
5102 else
5103 settings_[SettingsKeys::Theme] = "auto";
5104
5105 // Store splitter states as hex strings
5106 if (ui->mainSplitter)
5107 {
5108 settings_[SettingsKeys::MainSplitter] =
5109 QString::fromLatin1(ui->mainSplitter->saveState().toHex());
5110 }
5111 if (ui->leftSplitter)
5112 {
5113 settings_[SettingsKeys::LeftSplitter] =
5114 QString::fromLatin1(ui->leftSplitter->saveState().toHex());
5115 }
5116
5117 // Store section expanded states
5118 settings_[SettingsKeys::SectionVariables] =
5119 variablesSection ? variablesSection->isExpanded() : true;
5120 settings_[SettingsKeys::SectionStack] =
5121 stackSection ? stackSection->isExpanded() : true;
5122 settings_[SettingsKeys::SectionBreakpoints] =
5123 breakpointsSection ? breakpointsSection->isExpanded() : true;
5124 settings_[SettingsKeys::SectionFiles] =
5125 filesSection ? filesSection->isExpanded() : false;
5126 settings_[SettingsKeys::SectionEval] =
5127 evalSection ? evalSection->isExpanded() : false;
5128 settings_[SettingsKeys::SectionSettings] =
5129 settingsSection ? settingsSection->isExpanded() : false;
5130 settings_[SettingsKeys::SectionWatch] =
5131 watchSection ? watchSection->isExpanded() : true;
5132
5133 if (watchTree)
5134 {
5135 storeWatchList();
5136 }
5137}