### 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

Show analyzer invocation
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-23-100342-3640-1 -x c++ /builds/wireshark/wireshark/ui/qt/lua_debugger/lua_debugger_dialog.cpp

Keyboard shortcuts:

- Use 'j/k' keys for keyboard navigation
- Use 'Shift+S' to show/hide relevant lines
- Use '?' to toggle this window

Close
Show only relevant lines Show control flow arrows

1 | /* lua_debugger_dialog.cpp
2 | *
3 | * Wireshark - Network traffic analyzer
4 | * By Gerald Combs <gerald@wireshark.org>
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 |
91 | namespace
92 | {
93 | /** Global personal config path — debugger settings are not profile-specific. */
94 | QString
95 | luaDebuggerSettingsFilePath()
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 |
104 | extern "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 |
113 | LuaDebuggerDialog *LuaDebuggerDialog::_instance = nullptr;
114 | int32_t LuaDebuggerDialog::currentTheme_ = WSLUA_DEBUGGER_THEME_AUTO;
115 |
116 | int32_t LuaDebuggerDialog::currentTheme() {
117 | return currentTheme_;
118 | }
119 |
120 | namespace
121 | {
122 | // ============================================================================
123 | // Settings Keys (for JSON persistence)
124 | // ============================================================================
125 | namespace SettingsKeys
126 | {
127 | constexpr const char *Theme = "theme";
128 | constexpr const char *MainSplitter = "mainSplitterState";
129 | constexpr const char *LeftSplitter = "leftSplitterState";
130 | constexpr const char *SectionVariables = "sectionVariables";
131 | constexpr const char *SectionStack = "sectionStack";
132 | constexpr const char *SectionFiles = "sectionFiles";
133 | constexpr const char *SectionBreakpoints = "sectionBreakpoints";
134 | constexpr const char *SectionEval = "sectionEval";
135 | constexpr const char *SectionSettings = "sectionSettings";
136 | constexpr const char *SectionWatch = "sectionWatch";
137 | constexpr const char *Breakpoints = "breakpoints";
138 | constexpr const char *Watches = "watches";
139 | } // namespace SettingsKeys
140 |
141 | /** QVariantMap values for JSON arrays are typically QVariantList of QVariantMap. */
142 | static QJsonArray
143 | jsonArrayFromSettingsMap(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 | // ============================================================================
156 | constexpr qint32 FileTreePathRole = static_cast<qint32>(Qt::UserRole);
157 | constexpr qint32 FileTreeIsDirectoryRole = static_cast<qint32>(Qt::UserRole + 1);
158 | constexpr qint32 BreakpointFileRole = static_cast<qint32>(Qt::UserRole + 2);
159 | constexpr qint32 BreakpointLineRole = static_cast<qint32>(Qt::UserRole + 3);
160 | constexpr qint32 StackItemFileRole = static_cast<qint32>(Qt::UserRole + 4);
161 | constexpr qint32 StackItemLineRole = static_cast<qint32>(Qt::UserRole + 5);
162 | constexpr qint32 StackItemNavigableRole = static_cast<qint32>(Qt::UserRole + 6);
163 | constexpr qint32 StackItemLevelRole = static_cast<qint32>(Qt::UserRole + 7);
164 | constexpr qint32 VariablePathRole = static_cast<qint32>(Qt::UserRole + 8);
165 | constexpr qint32 VariableTypeRole = static_cast<qint32>(Qt::UserRole + 9);
166 | constexpr qint32 VariableCanExpandRole = static_cast<qint32>(Qt::UserRole + 10);
167 | constexpr qint32 WatchSpecRole = static_cast<qint32>(Qt::UserRole + 11);
168 | constexpr qint32 WatchSubpathRole = static_cast<qint32>(Qt::UserRole + 13);
169 | constexpr qint32 WatchLastValueRole = static_cast<qint32>(Qt::UserRole + 14);
170 | constexpr 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). */
178 | constexpr qint32 WatchChildSnapRole =
179 | static_cast<qint32>(Qt::UserRole + 20);
180 |
181 | constexpr qsizetype WATCH_TOOLTIP_MAX_CHARS = 4096;
182 | constexpr int WATCH_EXPR_MAX_CHARS = 65536;
183 |
184 | /** @brief Registers the UI callback with the Lua debugger core at load time. */
185 | class 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 |
199 | static LuaDebuggerUiCallbackRegistrar g_luaDebuggerUiCallbackRegistrar;
200 |
201 | /** @brief Build a key sequence from a key event for matching QAction shortcuts. */
202 | static 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 | */
215 | static 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 | */
235 | static 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 |
274 | static 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`). */
284 | static 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 |
298 | static 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). */
305 | static 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. */
320 | static 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 | */
331 | struct VariableRowFields
332 | {
333 | QString name;
334 | QString value;
335 | QString type;
336 | bool canExpand = false;
337 | QString childPath;
338 | };
339 |
340 | static 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 | */
357 | static 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"). */
377 | static 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 | */
394 | static 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). */
412 | static 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). */
443 | static 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 |
477 | static 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). */
488 | static 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 |
523 | static 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 |
559 | static 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 |
577 | static 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. */
604 | static 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 |
628 | using 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 | */
643 | static 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 |
685 | static void reexpandWatchDescendantsByPathKeys(QTreeWidgetItem *subtree,
686 | QStringList pathKeys)
687 | {
688 | reexpandTreeDescendantsByPathKeys(subtree, std::move(pathKeys),
689 | findWatchItemBySubpathOrPathKey);
690 | }
691 |
692 | static 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 |
701 | static 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). */
715 | static 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 | */
738 | class 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). */
800 | class 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.
835 | class TreeWidgetItemFromIndex : public QTreeWidget
836 | {
837 | public:
838 | using QTreeWidget::itemFromIndex;
839 | };
840 |
841 | static inline QTreeWidgetItem *
842 | itemFromIndexHelper(const QTreeWidget *tree, const QModelIndex &index)
843 | {
844 | return static_cast<const TreeWidgetItemFromIndex *>(tree)->itemFromIndex(
845 | index);
846 | }
847 | #endif
848 |
849 | class 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 |
869 | QWidget *
870 | WatchRootDelegate::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 |
891 | void 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 |
916 | void 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 |
939 | LuaDebuggerDialog::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 |
1242 | LuaDebuggerDialog::~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 |
1265 | void 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 |
1460 | LuaDebuggerDialog *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 |
1474 | void 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 |
1555 | void LuaDebuggerDialog::onContinue()
1556 | {
1557 | resumeDebuggerAndExitLoop();
1558 | updateWidgets();
1559 | }
1560 |
1561 | void 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 |
1592 | void LuaDebuggerDialog::onStepOver()
1593 | {
1594 | runDebuggerStep(wslua_debugger_step_over);
1595 | }
1596 |
1597 | void LuaDebuggerDialog::onStepIn()
1598 | {
1599 | runDebuggerStep(wslua_debugger_step_in);
1600 | }
1601 |
1602 | void LuaDebuggerDialog::onStepOut()
1603 | {
1604 | runDebuggerStep(wslua_debugger_step_out);
1605 | }
1606 |
1607 | void 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 |
1622 | void 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 |
1632 | void 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 |
1659 | void 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 |
1679 | void LuaDebuggerDialog::installDescendantShortcutFilters()
1680 | {
1681 | installEventFilter(this);
1682 | for (QWidget *w : findChildren<QWidget *>())
1683 | {
1684 | w->installEventFilter(this);
1685 | }
1686 | }
1687 |
1688 | void 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 |
1704 | bool 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 |
1797 | void 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 |
1828 | void 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 |
1920 | void 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 |
2014 | void 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 |
2026 | void 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)
2049 | void 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 |
2112 | void 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 |
2140 | void 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 |
2159 | LuaDebuggerCodeView *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 |
2268 | LuaDebuggerCodeView *LuaDebuggerDialog::currentCodeView() const
2269 | {
2270 | return qobject_cast<LuaDebuggerCodeView *>(
2271 | ui->codeTabWidget->currentWidget());
2272 | }
2273 |
2274 | qint32 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 |
2291 | bool LuaDebuggerDialog::hasUnsavedChanges() const
2292 | {
2293 | return unsavedOpenScriptTabCount() > 0;
2294 | }
2295 |
2296 | bool 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 |
2322 | void 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 |
2337 | bool 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 |
2364 | bool 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 |
2383 | void 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 |
2402 | void LuaDebuggerDialog::updateSaveActionState()
2403 | {
2404 | LuaDebuggerCodeView *view = currentCodeView();
2405 | ui->actionSaveFile->setEnabled(view && view->document()->isModified());
2406 | }
2407 |
2408 | void LuaDebuggerDialog::updateWindowModifiedState()
2409 | {
2410 | setWindowModified(hasUnsavedChanges());
2411 | }
2412 |
2413 | void 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 |
2454 | void LuaDebuggerDialog::updateLuaEditorAuxFrames()
2455 | {
2456 | QPlainTextEdit *ed = currentCodeView();
2457 | ui->luaDebuggerFindFrame->setTargetEditor(ed);
2458 | ui->luaDebuggerGoToLineFrame->setTargetEditor(ed);
2459 | }
2460 |
2461 | void LuaDebuggerDialog::onEditorFind()
2462 | {
2463 | updateLuaEditorAuxFrames();
2464 | showAccordionFrame(ui->luaDebuggerFindFrame, true);
2465 | }
2466 |
2467 | void LuaDebuggerDialog::onEditorGoToLine()
2468 | {
2469 | updateLuaEditorAuxFrames();
2470 | showAccordionFrame(ui->luaDebuggerGoToLineFrame, true);
2471 | }
2472 |
2473 | void LuaDebuggerDialog::onSaveFile()
2474 | {
2475 | LuaDebuggerCodeView *view = currentCodeView();
2476 | if (!view || !view->document()->isModified())
2477 | {
2478 | return;
2479 | }
2480 | saveCodeView(view);
2481 | updateSaveActionState();
2482 | }
2483 |
2484 | void 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 |
2519 | void 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 |
2550 | void 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 |
2572 | void 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 |
2590 | void 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 |
2705 | void 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 |
2730 | void LuaDebuggerDialog::onMonospaceFontUpdated(const QFont &font)
2731 | {
2732 | applyCodeEditorFonts(font);
2733 | }
2734 |
2735 | void LuaDebuggerDialog::onMainAppInitialized()
2736 | {
2737 | applyMonospaceFonts();
2738 | }
2739 |
2740 | void LuaDebuggerDialog::onPreferencesChanged()
2741 | {
2742 | applyCodeViewThemes();
2743 | applyMonospaceFonts();
2744 | refreshWatchDisplay();
2745 | }
2746 |
2747 | void 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 |
2768 | void 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 | */
2792 | void 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 | */
2852 | void 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 | */
2873 | void 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 |
2883 | void 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 |
2919 | void 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 | */
2945 | static 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 |
2954 | void 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 |
2988 | void 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 |
3009 | bool 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 |
3068 | QString 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 |
3097 | QTreeWidgetItem *
3098 | LuaDebuggerDialog::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 |
3131 | bool 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 |
3207 | void LuaDebuggerDialog::openInitialBreakpointFiles(
3208 | const QVector<QString> &files)
3209 | {
3210 | for (const QString &path : files)
3211 | {
3212 | loadFile(path);
3213 | }
3214 | }
3215 |
3216 | void 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 |
3232 | void 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 |
3246 | void 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 |
3262 | void LuaDebuggerDialog::clearPausedStateUi()
3263 | {
3264 | if (variablesTree)
3265 | {
3266 | variablesTree->clear();
3267 | }
3268 | if (stackTree)
3269 | {
3270 | stackTree->clear();
3271 | }
3272 | clearAllCodeHighlights();
3273 | }
3274 |
3275 | void 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 |
3290 | void 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 |
3348 | void 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 |
3366 | void LuaDebuggerDialog::applyMonospaceFonts()
3367 | {
3368 | applyCodeEditorFonts(effectiveMonospaceFont(true));
3369 | applyMonospacePanelFonts();
3370 | }
3371 |
3372 | void 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 |
3396 | void 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 |
3423 | QFont 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 |
3435 | QFont LuaDebuggerDialog::effectiveRegularFont() const
3436 | {
3437 | if (mainApp && mainApp->isInitialized())
3438 | {
3439 | return mainApp->font();
3440 | }
3441 | return QGuiApplication::font();
3442 | }
3443 |
3444 | void 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 |
3457 | void 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 |
3494 | void 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 |
3525 | void 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 |
3534 | void LuaDebuggerDialog::updateWidgets()
3535 | {
3536 | updateEnabledCheckboxIcon();
3537 | updateStatusLabel();
3538 | updateContinueActionState();
3539 | updateEvalPanelState();
3540 | refreshWatchDisplay();
3541 | }
3542 |
3543 | void LuaDebuggerDialog::ensureDebuggerEnabledForActiveBreakpoints()
3544 | {
3545 | if (!wslua_debugger_is_enabled())
3546 | {
3547 | wslua_debugger_set_enabled(true);
3548 | syncDebuggerToggleWithCore();
3549 | }
3550 | }
3551 |
3552 | void 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 |
3573 | void 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 |
3617 | void 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 |
3635 | void 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 |
3691 | void LuaDebuggerDialog::onEvalClear()
3692 | {
3693 | evalInputEdit->clear();
3694 | evalOutputEdit->clear();
3695 | }
3696 |
3697 | void 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 |
3731 | void 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 |
3752 | void 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 |
3786 | namespace
3787 | {
3788 | // NOLINTNEXTLINE(misc-no-recursion)
3789 | static 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 |
3814 | void 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 |
3847 | QTreeWidgetItem *
3848 | LuaDebuggerDialog::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 |
3868 | QTreeWidgetItem *
3869 | LuaDebuggerDialog::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 |
3897 | void 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 |
3915 | void 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 |
3939 | void 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 |
3980 | void 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 |
4017 | void 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 |
4038 | void 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 |
4063 | void 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 |
4091 | void 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 |
4129 | void 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 |
4153 | void 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 |
4204 | void 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 |
4256 | void 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 |
4315 | namespace
4316 | {
4317 | /** Subpath / variable-path key used to address @p item inside a watch root. */
4318 | static 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 |
4333 | void 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 |
4353 | void 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 |
4384 | QStringList 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 |
4400 | void LuaDebuggerDialog::recordWatchRootExpansion(const QString &rootSpec,
4401 | bool expanded)
4402 | {
4403 | recordTreeSectionRootExpansion(watchExpansion_, rootSpec, expanded);
4404 | }
4405 |
4406 | void LuaDebuggerDialog::recordWatchSubpathExpansion(const QString &rootSpec,
4407 | const QString &key,
4408 | bool expanded)
4409 | {
4410 | recordTreeSectionSubpathExpansion(watchExpansion_, rootSpec, key, expanded);
4411 | }
4412 |
4413 | QStringList
4414 | LuaDebuggerDialog::watchExpandedSubpathsForSpec(const QString &rootSpec) const
4415 | {
4416 | return treeSectionExpandedSubpaths(watchExpansion_, rootSpec);
4417 | }
4418 |
4419 | void 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 |
4453 | void 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 |
4488 | void 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 |
4508 | void 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 |
4533 | void 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 |
4555 | namespace
4556 | {
4557 | /** Pointers into the context menu built by buildWatchContextMenu(). */
4558 | struct 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 | */
4579 | static 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 |
4603 | void 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 |
4742 | void LuaDebuggerDialog::addPathWatch(const QString &debuggerPath)
4743 | {
4744 | insertNewWatchRow(debuggerPath, false);
4745 | }
4746 |
4747 | void 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 |
4755 | void 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 |
4807 | void 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 |
4850 | void 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 |
4882 | void 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
4924 | void 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 |
4956 | void 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 |
4986 | void 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 |
5086 | void 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 | }