| 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' |
Press '?' to see keyboard shortcuts
Keyboard shortcuts:
| 1 | /* lua_debugger_dialog.cpp | ||||
| 2 | * | ||||
| 3 | * Wireshark - Network traffic analyzer | ||||
| 4 | * By Gerald Combs <[email protected]> | ||||
| 5 | * Copyright 1998 Gerald Combs | ||||
| 6 | * | ||||
| 7 | * SPDX-License-Identifier: GPL-2.0-or-later | ||||
| 8 | */ | ||||
| 9 | |||||
| 10 | #include "lua_debugger_dialog.h" | ||||
| 11 | #include "accordion_frame.h" | ||||
| 12 | #include <QApplication> | ||||
| 13 | #include "lua_debugger_code_view.h" | ||||
| 14 | #include "lua_debugger_find_frame.h" | ||||
| 15 | #include "lua_debugger_goto_line_frame.h" | ||||
| 16 | #include "main_application.h" | ||||
| 17 | #include "main_window.h" | ||||
| 18 | #include "ui_lua_debugger_dialog.h" | ||||
| 19 | #include "utils/stock_icon.h" | ||||
| 20 | #include "widgets/collapsible_section.h" | ||||
| 21 | |||||
| 22 | #include <QAction> | ||||
| 23 | #include <QCheckBox> | ||||
| 24 | #include <QChildEvent> | ||||
| 25 | #include <QClipboard> | ||||
| 26 | #include <QCloseEvent> | ||||
| 27 | #include <QEvent> | ||||
| 28 | #if QT_VERSION((6<<16)|(4<<8)|(2)) >= QT_VERSION_CHECK(6, 0, 0)((6<<16)|(0<<8)|(0)) | ||||
| 29 | #include <QKeyCombination> | ||||
| 30 | #endif | ||||
| 31 | #include <QKeyEvent> | ||||
| 32 | #include <QColor> | ||||
| 33 | #include <QComboBox> | ||||
| 34 | #include <QDir> | ||||
| 35 | #include <QAbstractItemView> | ||||
| 36 | #include <QDirIterator> | ||||
| 37 | #include <QDragMoveEvent> | ||||
| 38 | #include <QDropEvent> | ||||
| 39 | #include <QFile> | ||||
| 40 | #include <QFileInfo> | ||||
| 41 | #include <QFont> | ||||
| 42 | #include <QFontDatabase> | ||||
| 43 | #include <QFormLayout> | ||||
| 44 | #include <QGuiApplication> | ||||
| 45 | #include <QHeaderView> | ||||
| 46 | #include <QIcon> | ||||
| 47 | #include <QJsonArray> | ||||
| 48 | #include <QJsonParseError> | ||||
| 49 | #include <QLineEdit> | ||||
| 50 | #include <QJsonDocument> | ||||
| 51 | #include <QJsonObject> | ||||
| 52 | #include <QKeySequence> | ||||
| 53 | #include <QList> | ||||
| 54 | #include <QMenu> | ||||
| 55 | #include <QMessageBox> | ||||
| 56 | #include <QMouseEvent> | ||||
| 57 | #include <QMetaObject> | ||||
| 58 | #include <QPainter> | ||||
| 59 | #include <QPalette> | ||||
| 60 | #include <QPlainTextEdit> | ||||
| 61 | #include <QPointer> | ||||
| 62 | #include <QSet> | ||||
| 63 | #include <QStandardPaths> | ||||
| 64 | #include <QStyle> | ||||
| 65 | #include <QTextDocument> | ||||
| 66 | #include <QSplitter> | ||||
| 67 | #include <QTimer> | ||||
| 68 | #include <QStyledItemDelegate> | ||||
| 69 | #include <QStyleOptionViewItem> | ||||
| 70 | #include <QAbstractItemModel> | ||||
| 71 | #include <QModelIndex> | ||||
| 72 | #include <QTreeWidgetItem> | ||||
| 73 | #include <QTextStream> | ||||
| 74 | #include <QVBoxLayout> | ||||
| 75 | #include <QToolButton> | ||||
| 76 | #include <QHBoxLayout> | ||||
| 77 | #include <algorithm> | ||||
| 78 | |||||
| 79 | #include <glib.h> | ||||
| 80 | |||||
| 81 | #include "ui/recent.h" | ||||
| 82 | #include "app/application_flavor.h" | ||||
| 83 | #include "wsutil/filesystem.h" | ||||
| 84 | #include <epan/prefs.h> | ||||
| 85 | #include <ui/qt/utils/color_utils.h> | ||||
| 86 | #include <ui/qt/widgets/wireshark_file_dialog.h> | ||||
| 87 | #include <ui/qt/utils/qt_ui_utils.h> | ||||
| 88 | |||||
| 89 | #define LUA_DEBUGGER_SETTINGS_FILE"lua_debugger.json" "lua_debugger.json" | ||||
| 90 | |||||
| 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) | ||||
| 362 | { | ||||
| 363 | item->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); | ||||
| 364 | QTreeWidgetItem *const ph = new QTreeWidgetItem(item); | ||||
| 365 | if (enabledOnlyPlaceholder
| ||||
| 366 | { | ||||
| 367 | ph->setFlags(Qt::ItemIsEnabled); | ||||
| 368 | } | ||||
| 369 | } | ||||
| 370 | else | ||||
| 371 | { | ||||
| 372 | item->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator); | ||||
| 373 | } | ||||
| 374 | } | ||||
| |||||
| 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 → Upvalues → 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 — 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
| ||||
| 2017 | { | ||||
| 2018 | return; | ||||
| 2019 | } | ||||
| 2020 | variablesTree->clear(); | ||||
| 2021 | updateVariables(nullptr, QString()); | ||||
| 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); | ||||
| 2055 | |||||
| 2056 | if (variables) | ||||
| 2057 | { | ||||
| 2058 | for (int32_t variableIndex = 0; variableIndex < variableCount; | ||||
| 2059 | ++variableIndex) | ||||
| 2060 | { | ||||
| 2061 | QTreeWidgetItem *item; | ||||
| 2062 | if (parent
| ||||
| 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); | ||||
| 2079 | item->setToolTip( | ||||
| 2080 | 0, tooltipSuffix.isEmpty() | ||||
| 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() | ||||
| 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, | ||||
| 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 || | ||||
| |||||
| 3985 | !current) | ||||
| 3986 | { | ||||
| 3987 | return; | ||||
| 3988 | } | ||||
| 3989 | if (current->parent() != nullptr) | ||||
| 3990 | { | ||||
| 3991 | return; | ||||
| 3992 | } | ||||
| 3993 | const QString spec = current->data(0, WatchSpecRole).toString(); | ||||
| 3994 | if (spec.isEmpty()) | ||||
| 3995 | { | ||||
| 3996 | return; | ||||
| 3997 | } | ||||
| 3998 | |||||
| 3999 | const bool live = wslua_debugger_is_enabled() && debuggerPaused && | ||||
| 4000 | wslua_debugger_is_paused(); | ||||
| 4001 | if (live) | ||||
| 4002 | { | ||||
| 4003 | const int32_t desired = wslua_debugger_find_stack_level_for_watch_spec( | ||||
| 4004 | spec.toUtf8().constData()); | ||||
| 4005 | if (desired >= 0 && desired != stackSelectionLevel) | ||||
| 4006 | { | ||||
| 4007 | stackSelectionLevel = static_cast<int>(desired); | ||||
| 4008 | wslua_debugger_set_variable_stack_level(desired); | ||||
| 4009 | 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 | } |