### Bug Summary

File: | builds/wireshark/wireshark/ui/qt/lua_debugger/lua_debugger_breakpoints.cpp
Warning: | line 2327, column 21 Value stored to 'modeStr' during its initialization is never read

### Annotated Source Code

Press '?' to see keyboard shortcuts

Show analyzer invocation
clang -cc1 -cc1 -triple x86_64-pc-linux-gnu -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name lua_debugger_breakpoints.cpp -analyzer-checker=core -analyzer-checker=apiModeling -analyzer-checker=unix -analyzer-checker=deadcode -analyzer-checker=cplusplus -analyzer-checker=security.insecureAPI.UncheckedReturn -analyzer-checker=security.insecureAPI.getpw -analyzer-checker=security.insecureAPI.gets -analyzer-checker=security.insecureAPI.mktemp -analyzer-checker=security.insecureAPI.mkstemp -analyzer-checker=security.insecureAPI.vfork -analyzer-checker=nullability.NullPassedToNonnull -analyzer-checker=nullability.NullReturnedFromNonnull -analyzer-output plist -w -setup-static-analyzer -mrelocation-model pic -pic-level 2 -fhalf-no-semantic-interposition -fno-delete-null-pointer-checks -mframe-pointer=all -relaxed-aliasing -fmath-errno -ffp-contract=on -fno-rounding-math -ffloat16-excess-precision=fast -fbfloat16-excess-precision=fast -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -tune-cpu generic -debugger-tuning=gdb -fdebug-compilation-dir=/builds/wireshark/wireshark/build -fcoverage-compilation-dir=/builds/wireshark/wireshark/build -resource-dir /usr/lib/llvm-21/lib/clang/21 -isystem /usr/include/glib-2.0 -isystem /usr/lib/x86_64-linux-gnu/glib-2.0/include -isystem /builds/wireshark/wireshark/build/ui/qt -isystem /builds/wireshark/wireshark/ui/qt -isystem /builds/wireshark/wireshark/ui/qt/lua_debugger -isystem /usr/include/x86_64-linux-gnu/qt6/QtWidgets -isystem /usr/include/x86_64-linux-gnu/qt6 -isystem /usr/include/x86_64-linux-gnu/qt6/QtCore -isystem /usr/lib/x86_64-linux-gnu/qt6/mkspecs/linux-g++ -isystem /usr/include/x86_64-linux-gnu/qt6/QtGui -isystem /usr/include/x86_64-linux-gnu/qt6/QtCore5Compat -isystem /usr/include/x86_64-linux-gnu/qt6/QtConcurrent -isystem /usr/include/x86_64-linux-gnu/qt6/QtPrintSupport -isystem /usr/include/x86_64-linux-gnu/qt6/QtNetwork -isystem /usr/include/x86_64-linux-gnu/qt6/QtMultimedia -isystem /usr/include/x86_64-linux-gnu/qt6/QtDBus -D G_DISABLE_DEPRECATED -D G_DISABLE_SINGLE_INCLUDES -D QT_CONCURRENT_LIB -D QT_CORE5COMPAT_LIB -D QT_CORE_LIB -D QT_DBUS_LIB -D QT_GUI_LIB -D QT_MULTIMEDIA_LIB -D QT_NETWORK_LIB -D QT_PRINTSUPPORT_LIB -D QT_WIDGETS_LIB -D WS_DEBUG -D WS_DEBUG_UTF_8 -I /builds/wireshark/wireshark/build/ui/qt/qtui_autogen/include -I /builds/wireshark/wireshark/build -I /builds/wireshark/wireshark -I /builds/wireshark/wireshark/include -D _GLIBCXX_ASSERTIONS -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/x86_64-linux-gnu/c++/14 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14/backward -internal-isystem /usr/lib/llvm-21/lib/clang/21/include -internal-isystem /usr/local/include -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/14/../../../../x86_64-linux-gnu/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -fmacro-prefix-map=/builds/wireshark/wireshark/= -fmacro-prefix-map=/builds/wireshark/wireshark/build/= -fmacro-prefix-map=../= -Wno-format-nonliteral -std=c++17 -fdeprecated-macro -ferror-limit 19 -fwrapv -fwrapv-pointer -fstrict-flex-arrays=3 -stack-protector 2 -fstack-clash-protection -fcf-protection=full -fgnuc-version=4.2.1 -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions -fcolor-diagnostics -analyzer-output=html -faddrsig -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o /builds/wireshark/wireshark/sbout/2026-05-06-100446-3641-1 -x c++ /builds/wireshark/wireshark/ui/qt/lua_debugger/lua_debugger_breakpoints.cpp

Keyboard shortcuts:

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

Close
Show only relevant lines Show control flow arrows

1 | /* lua_debugger_breakpoints.cpp
2 | *
3 | * Wireshark - Network traffic analyzer
4 | * By Gerald Combs <gerald@wireshark.org>
5 | * Copyright 1998 Gerald Combs
6 | *
7 | * SPDX-License-Identifier: GPL-2.0-or-later
8 | */
9 |
10 | /**
11 | * @file
12 | * Breakpoints panel: list/model, inline editor + mode picker,
13 | * gutter integration, and persistence.
14 | */
15 |
16 | #include "lua_debugger_breakpoints.h"
17 |
18 | #include <QAbstractItemDelegate>
19 | #include <QAbstractItemModel>
20 | #include <QAbstractItemView>
21 | #include <QAction>
22 | #include <QApplication>
23 | #include <QBrush>
24 | #include <QByteArray>
25 | #include <QChar>
26 | #include <QComboBox>
27 | #include <QCoreApplication>
28 | #include <QEvent>
29 | #include <QFileInfo>
30 | #include <QFont>
31 | #include <QGuiApplication>
32 | #include <QHBoxLayout>
33 | #include <QHeaderView>
34 | #include <QIcon>
35 | #include <QIntValidator>
36 | #include <QItemSelectionModel>
37 | #include <QJsonArray>
38 | #include <QJsonObject>
39 | #include <QJsonValue>
40 | #include <QKeyEvent>
41 | #include <QKeySequence>
42 | #include <QLineEdit>
43 | #include <QListView>
44 | #include <QMenu>
45 | #include <QMessageBox>
46 | #include <QModelIndex>
47 | #include <QObject>
48 | #include <QPaintEvent>
49 | #include <QPainter>
50 | #include <QPalette>
51 | #include <QPen>
52 | #include <QPixmap>
53 | #include <QPoint>
54 | #include <QPointer>
55 | #include <QRect>
56 | #include <QRectF>
57 | #include <QResizeEvent>
58 | #include <QShowEvent>
59 | #include <QSignalBlocker>
60 | #include <QSize>
61 | #include <QStandardItem>
62 | #include <QStandardItemModel>
63 | #include <QString>
64 | #include <QStringList>
65 | #include <QStyle>
66 | #include <QStyleOptionFrame>
67 | #include <QStyleOptionViewItem>
68 | #include <QStyledItemDelegate>
69 | #include <QTabWidget>
70 | #include <QTimer>
71 | #include <QToolButton>
72 | #include <QTreeView>
73 | #include <QVariant>
74 | #include <QWidget>
75 |
76 | #include <climits>
77 | #include <glib.h>
78 |
79 | #include "lua_debugger_code_editor.h"
80 | #include "lua_debugger_dialog.h"
81 | #include "lua_debugger_files.h"
82 | #include "lua_debugger_settings.h"
83 | #include "lua_debugger_utils.h"
84 | #include "widgets/collapsible_section.h"
85 | #include <epan/wslua/wslua_debugger.h>
86 |
87 | /* ===== breakpoint_modes ===== */
88 |
89 | namespace LuaDbgBreakpointModes
90 | {
91 |
92 | const ModeSpec kBreakpointEditModes[kModeCount] = {
93 | {Mode::Expression, QT_TRANSLATE_NOOP("BreakpointConditionDelegate", "Expression")"Expression",
94 | QT_TRANSLATE_NOOP("BreakpointConditionDelegate", "Lua expression — pause when truthy")"Lua expression — pause when truthy",
95 | QT_TRANSLATE_NOOP("BreakpointConditionDelegate", "Evaluated each time control reaches this line; locals, ""Evaluated each time control reaches this line; locals, " "upvalues, and globals are visible like Watch / Evaluate.\n" "Runtime errors are treated as false (silent) and surface as " "a warning icon on the row."
96 | "upvalues, and globals are visible like Watch / Evaluate.\n""Evaluated each time control reaches this line; locals, " "upvalues, and globals are visible like Watch / Evaluate.\n" "Runtime errors are treated as false (silent) and surface as " "a warning icon on the row."
97 | "Runtime errors are treated as false (silent) and surface as ""Evaluated each time control reaches this line; locals, " "upvalues, and globals are visible like Watch / Evaluate.\n" "Runtime errors are treated as false (silent) and surface as " "a warning icon on the row."
98 | "a warning icon on the row.")"Evaluated each time control reaches this line; locals, " "upvalues, and globals are visible like Watch / Evaluate.\n" "Runtime errors are treated as false (silent) and surface as " "a warning icon on the row."},
99 | {Mode::HitCount, QT_TRANSLATE_NOOP("BreakpointConditionDelegate", "Hit Count")"Hit Count",
100 | QT_TRANSLATE_NOOP("BreakpointConditionDelegate", "Pause after N hits (0 disables)")"Pause after N hits (0 disables)",
101 | QT_TRANSLATE_NOOP("BreakpointConditionDelegate", "Gate the pause on a hit counter. The dropdown next to N ""Gate the pause on a hit counter. The dropdown next to N " "picks the comparison mode: from pauses on every hit " "from N onwards (default); every pauses on hits N, 2N, " "3N, \xe2\x80\xa6; once pauses on the N-th hit and " "deactivates the breakpoint. Use 0 to disable the gate. The " "counter is preserved across edits to Expression / Hit " "Count / Log Message; lowering the target below the current " "count rolls the counter back to 0 so the breakpoint can " "wait for the next N hits. Right-click the row to reset it " "explicitly. Combined with an Expression on the same row, " "the hit-count gate runs first."
102 | "picks the comparison mode: from pauses on every hit ""Gate the pause on a hit counter. The dropdown next to N " "picks the comparison mode: from pauses on every hit " "from N onwards (default); every pauses on hits N, 2N, " "3N, \xe2\x80\xa6; once pauses on the N-th hit and " "deactivates the breakpoint. Use 0 to disable the gate. The " "counter is preserved across edits to Expression / Hit " "Count / Log Message; lowering the target below the current " "count rolls the counter back to 0 so the breakpoint can " "wait for the next N hits. Right-click the row to reset it " "explicitly. Combined with an Expression on the same row, " "the hit-count gate runs first."
103 | "from N onwards (default); every pauses on hits N, 2N, ""Gate the pause on a hit counter. The dropdown next to N " "picks the comparison mode: from pauses on every hit " "from N onwards (default); every pauses on hits N, 2N, " "3N, \xe2\x80\xa6; once pauses on the N-th hit and " "deactivates the breakpoint. Use 0 to disable the gate. The " "counter is preserved across edits to Expression / Hit " "Count / Log Message; lowering the target below the current " "count rolls the counter back to 0 so the breakpoint can " "wait for the next N hits. Right-click the row to reset it " "explicitly. Combined with an Expression on the same row, " "the hit-count gate runs first."
104 | "3N, \xe2\x80\xa6; once pauses on the N-th hit and ""Gate the pause on a hit counter. The dropdown next to N " "picks the comparison mode: from pauses on every hit " "from N onwards (default); every pauses on hits N, 2N, " "3N, \xe2\x80\xa6; once pauses on the N-th hit and " "deactivates the breakpoint. Use 0 to disable the gate. The " "counter is preserved across edits to Expression / Hit " "Count / Log Message; lowering the target below the current " "count rolls the counter back to 0 so the breakpoint can " "wait for the next N hits. Right-click the row to reset it " "explicitly. Combined with an Expression on the same row, " "the hit-count gate runs first."
105 | "deactivates the breakpoint. Use 0 to disable the gate. The ""Gate the pause on a hit counter. The dropdown next to N " "picks the comparison mode: from pauses on every hit " "from N onwards (default); every pauses on hits N, 2N, " "3N, \xe2\x80\xa6; once pauses on the N-th hit and " "deactivates the breakpoint. Use 0 to disable the gate. The " "counter is preserved across edits to Expression / Hit " "Count / Log Message; lowering the target below the current " "count rolls the counter back to 0 so the breakpoint can " "wait for the next N hits. Right-click the row to reset it " "explicitly. Combined with an Expression on the same row, " "the hit-count gate runs first."
106 | "counter is preserved across edits to Expression / Hit ""Gate the pause on a hit counter. The dropdown next to N " "picks the comparison mode: from pauses on every hit " "from N onwards (default); every pauses on hits N, 2N, " "3N, \xe2\x80\xa6; once pauses on the N-th hit and " "deactivates the breakpoint. Use 0 to disable the gate. The " "counter is preserved across edits to Expression / Hit " "Count / Log Message; lowering the target below the current " "count rolls the counter back to 0 so the breakpoint can " "wait for the next N hits. Right-click the row to reset it " "explicitly. Combined with an Expression on the same row, " "the hit-count gate runs first."
107 | "Count / Log Message; lowering the target below the current ""Gate the pause on a hit counter. The dropdown next to N " "picks the comparison mode: from pauses on every hit " "from N onwards (default); every pauses on hits N, 2N, " "3N, \xe2\x80\xa6; once pauses on the N-th hit and " "deactivates the breakpoint. Use 0 to disable the gate. The " "counter is preserved across edits to Expression / Hit " "Count / Log Message; lowering the target below the current " "count rolls the counter back to 0 so the breakpoint can " "wait for the next N hits. Right-click the row to reset it " "explicitly. Combined with an Expression on the same row, " "the hit-count gate runs first."
108 | "count rolls the counter back to 0 so the breakpoint can ""Gate the pause on a hit counter. The dropdown next to N " "picks the comparison mode: from pauses on every hit " "from N onwards (default); every pauses on hits N, 2N, " "3N, \xe2\x80\xa6; once pauses on the N-th hit and " "deactivates the breakpoint. Use 0 to disable the gate. The " "counter is preserved across edits to Expression / Hit " "Count / Log Message; lowering the target below the current " "count rolls the counter back to 0 so the breakpoint can " "wait for the next N hits. Right-click the row to reset it " "explicitly. Combined with an Expression on the same row, " "the hit-count gate runs first."
109 | "wait for the next N hits. Right-click the row to reset it ""Gate the pause on a hit counter. The dropdown next to N " "picks the comparison mode: from pauses on every hit " "from N onwards (default); every pauses on hits N, 2N, " "3N, \xe2\x80\xa6; once pauses on the N-th hit and " "deactivates the breakpoint. Use 0 to disable the gate. The " "counter is preserved across edits to Expression / Hit " "Count / Log Message; lowering the target below the current " "count rolls the counter back to 0 so the breakpoint can " "wait for the next N hits. Right-click the row to reset it " "explicitly. Combined with an Expression on the same row, " "the hit-count gate runs first."
110 | "explicitly. Combined with an Expression on the same row, ""Gate the pause on a hit counter. The dropdown next to N " "picks the comparison mode: from pauses on every hit " "from N onwards (default); every pauses on hits N, 2N, " "3N, \xe2\x80\xa6; once pauses on the N-th hit and " "deactivates the breakpoint. Use 0 to disable the gate. The " "counter is preserved across edits to Expression / Hit " "Count / Log Message; lowering the target below the current " "count rolls the counter back to 0 so the breakpoint can " "wait for the next N hits. Right-click the row to reset it " "explicitly. Combined with an Expression on the same row, " "the hit-count gate runs first."
111 | "the hit-count gate runs first.")"Gate the pause on a hit counter. The dropdown next to N " "picks the comparison mode: from pauses on every hit " "from N onwards (default); every pauses on hits N, 2N, " "3N, \xe2\x80\xa6; once pauses on the N-th hit and " "deactivates the breakpoint. Use 0 to disable the gate. The " "counter is preserved across edits to Expression / Hit " "Count / Log Message; lowering the target below the current " "count rolls the counter back to 0 so the breakpoint can " "wait for the next N hits. Right-click the row to reset it " "explicitly. Combined with an Expression on the same row, " "the hit-count gate runs first."},
112 | {Mode::LogMessage, QT_TRANSLATE_NOOP("BreakpointConditionDelegate", "Log Message")"Log Message",
113 | QT_TRANSLATE_NOOP("BreakpointConditionDelegate", "Log message — supports {expr} and tags such as {filename}, ""Log message — supports {expr} and tags such as {filename}, " "{basename}, {line}, {function}, {hits}, {timestamp}, " "{delta}\xe2\x80\xa6"
114 | "{basename}, {line}, {function}, {hits}, {timestamp}, ""Log message — supports {expr} and tags such as {filename}, " "{basename}, {line}, {function}, {hits}, {timestamp}, " "{delta}\xe2\x80\xa6"
115 | "{delta}\xe2\x80\xa6")"Log message — supports {expr} and tags such as {filename}, " "{basename}, {line}, {function}, {hits}, {timestamp}, " "{delta}\xe2\x80\xa6",
116 | QT_TRANSLATE_NOOP("BreakpointConditionDelegate", "Logpoints write a message to the Evaluate output (and ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
117 | "Wireshark's info log) each time the line is reached. By ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
118 | "default execution continues without pausing; tick the ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
119 | "Pause box on this editor to also pause after emitting ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
120 | "(useful for log-then-inspect without duplicating the ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
121 | "breakpoint). The line is emitted verbatim — there is no ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
122 | "automatic file:line prefix. Inside {} the text is ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
123 | "evaluated as a Lua expression in this frame and ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
124 | "converted to text the same way tostring() does; ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
125 | "reserved tags below shadow any same-named Lua local / ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
126 | "upvalue / global. ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
127 | "Origin: {filename}, {basename}, {line}, {function}, ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
128 | "{what}. Counters and scope: {hits}, {depth}, {thread}. ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
129 | "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
130 | "{elapsed}, {delta}. Use {{ and }} for literal { and }. ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
131 | "Per-placeholder errors substitute '<error: ...>' without ""Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."
132 | "aborting the line.")"Logpoints write a message to the Evaluate output (and " "Wireshark's info log) each time the line is reached. By " "default execution continues without pausing; tick the " "Pause box on this editor to also pause after emitting " "(useful for log-then-inspect without duplicating the " "breakpoint). The line is emitted verbatim — there is no " "automatic file:line prefix. Inside {} the text is " "evaluated as a Lua expression in this frame and " "converted to text the same way tostring() does; " "reserved tags below shadow any same-named Lua local / " "upvalue / global. " "Origin: {filename}, {basename}, {line}, {function}, " "{what}. Counters and scope: {hits}, {depth}, {thread}. " "Time: {timestamp}, {datetime}, {epoch}, {epoch_ms}, " "{elapsed}, {delta}. Use {{ and }} for literal { and }. " "Per-placeholder errors substitute '<error: ...>' without " "aborting the line."},
133 | };
134 |
135 | QString translatedLabel(const ModeSpec &spec)
136 | {
137 | return QCoreApplication::translate("BreakpointConditionDelegate", spec.label);
138 | }
139 |
140 | const char *draftPropertyName(Mode m)
141 | {
142 | switch (m)
143 | {
144 | case Mode::Expression:
145 | return "luaDbgDraftExpression";
146 | case Mode::HitCount:
147 | return "luaDbgDraftHitCount";
148 | case Mode::LogMessage:
149 | return "luaDbgDraftLogMessage";
150 | }
151 | return "luaDbgDraftExpression";
152 | }
153 |
154 | QComboBox *editorHitModeCombo(QWidget *editor)
155 | {
156 | if (!editor)
157 | {
158 | return nullptr;
159 | }
160 | return qobject_cast<QComboBox *>(editor->property("luaDbgHitModeCombo").value<QObject *>());
161 | }
162 |
163 | QToolButton *editorPauseToggle(QWidget *editor)
164 | {
165 | if (!editor)
166 | {
167 | return nullptr;
168 | }
169 | return qobject_cast<QToolButton *>(editor->property("luaDbgPauseCheckBox").value<QObject *>());
170 | }
171 |
172 | void applyEditorMode(QWidget *editor, int modeIndex)
173 | {
174 | if (!editor || modeIndex < 0 || modeIndex >= kModeCount)
175 | {
176 | return;
177 | }
178 | QLineEdit *valueEdit = qobject_cast<QLineEdit *>(editor);
179 | if (!valueEdit)
180 | {
181 | return;
182 | }
183 |
184 | const ModeSpec &spec = kBreakpointEditModes[modeIndex];
185 | const Mode newMode = spec.mode;
186 | const int prevModeRaw = editor->property("luaDbgCurrentMode").toInt();
187 |
188 | /* Stash whatever was in the line edit under the OLD mode's
189 | * draft slot before we overwrite it. -1 (the createEditor
190 | * sentinel) means "first call, nothing to stash yet". */
191 | if (prevModeRaw >= 0)
192 | {
193 | const auto prevMode = static_cast<Mode>(prevModeRaw);
194 | editor->setProperty(draftPropertyName(prevMode), valueEdit->text());
195 | }
196 |
197 | /* Restore (or seed, on the very first call) the new mode's
198 | * draft into the line edit. */
199 | const QString draft = editor->property(draftPropertyName(newMode)).toString();
200 | valueEdit->setText(draft);
201 |
202 | /* Validator: only the Hit Count mode constrains input. The
203 | * old validator (if any) is owned by the line edit, so
204 | * setValidator(nullptr) lets Qt clean it up on next attach. */
205 | if (newMode == Mode::HitCount)
206 | {
207 | valueEdit->setValidator(new QIntValidator(0, INT_MAX2147483647, valueEdit));
208 | }
209 | else
210 | {
211 | valueEdit->setValidator(nullptr);
212 | }
213 |
214 | if (spec.placeholder)
215 | {
216 | valueEdit->setPlaceholderText(QCoreApplication::translate("BreakpointConditionDelegate", spec.placeholder));
217 | }
218 | else
219 | {
220 | valueEdit->setPlaceholderText(QString());
221 | }
222 | if (spec.valueTooltip)
223 | {
224 | valueEdit->setToolTip(QCoreApplication::translate("BreakpointConditionDelegate", spec.valueTooltip));
225 | }
226 | else
227 | {
228 | valueEdit->setToolTip(QString());
229 | }
230 |
231 | if (QComboBox *hitModeCombo = editorHitModeCombo(editor))
232 | {
233 | hitModeCombo->setVisible(newMode == Mode::HitCount);
234 | }
235 | if (QToolButton *pauseChk = editorPauseToggle(editor))
236 | {
237 | pauseChk->setVisible(newMode == Mode::LogMessage);
238 | }
239 |
240 | editor->setProperty("luaDbgCurrentMode", static_cast<int>(newMode));
241 |
242 | /* The auxiliary visibility just changed; have the line edit
243 | * re-run its embedded-widget layout so the right-side text
244 | * margin matches what's currently shown. (BreakpointInlineLineEdit
245 | * has no Q_OBJECT — it adds no signals/slots/Q_PROPERTYs over
246 | * QLineEdit — so we use dynamic_cast rather than qobject_cast.) */
247 | if (auto *bple = dynamic_cast<BreakpointInlineLineEdit *>(editor))
248 | {
249 | bple->relayout();
250 | }
251 |
252 | valueEdit->selectAll();
253 | }
254 |
255 | QIcon makePauseIcon(const QPalette &palette)
256 | {
257 | const int side = 16;
258 | const qreal dpr = 2.0;
259 |
260 | /* Layout: two bars, 3 px wide, with a 2 px gap, occupying the
261 | * central 8 px of a 16 px square. Rounded corners (1 px radius)
262 | * match the visual weight of macOS / Windows 11 media glyphs. */
263 | const qreal barW = 3.0;
264 | const qreal gap = 2.0;
265 | const qreal totalW = barW * 2 + gap;
266 | const qreal x0 = (side - totalW) / 2.0;
267 | const qreal y0 = 3.0;
268 | const qreal h = side - 6.0;
269 | const QRectF leftBar(x0, y0, barW, h);
270 | const QRectF rightBar(x0 + barW + gap, y0, barW, h);
271 |
272 | const auto drawBars = [&](QPainter *p, const QColor &color)
273 | {
274 | p->setPen(Qt::NoPen);
275 | p->setBrush(color);
276 | p->drawRoundedRect(leftBar, 1.0, 1.0);
277 | p->drawRoundedRect(rightBar, 1.0, 1.0);
278 | };
279 |
280 | const auto makePixmap = [&]()
281 | {
282 | QPixmap pm(int(side * dpr), int(side * dpr));
283 | pm.setDevicePixelRatio(dpr);
284 | pm.fill(Qt::transparent);
285 | return pm;
286 | };
287 |
288 | QIcon out;
289 |
290 | /* Off: bars in regular text color on transparent background. */
291 | {
292 | QPixmap pm = makePixmap();
293 | QPainter p(&pm);
294 | p.setRenderHint(QPainter::Antialiasing, true);
295 | drawBars(&p, palette.color(QPalette::Active, QPalette::ButtonText));
296 | p.end();
297 | out.addPixmap(pm, QIcon::Normal, QIcon::Off);
298 | }
299 |
300 | /* On: white bars on transparent background. The stylesheet on
301 | * the QToolButton supplies the colored rounded background that
302 | * the bars sit on. */
303 | {
304 | QPixmap pm = makePixmap();
305 | QPainter p(&pm);
306 | p.setRenderHint(QPainter::Antialiasing, true);
307 | drawBars(&p, palette.color(QPalette::Active, QPalette::HighlightedText));
308 | p.end();
309 | out.addPixmap(pm, QIcon::Normal, QIcon::On);
310 | }
311 |
312 | return out;
313 | }
314 |
315 | QString pauseToggleStyleSheet()
316 | {
317 | return QStringLiteral("QToolButton {"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
318 | " border: none;"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
319 | " background: transparent;"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
320 | " padding: 2px;"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
321 | "}"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
322 | "QToolButton:checked {"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
323 | " background-color: palette(highlight);"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
324 | " border-radius: 4px;"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
325 | "}"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
326 | "QToolButton:!checked:hover {"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
327 | " background-color: palette(midlight);"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
328 | " border-radius: 4px;"(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")))
329 | "}")(QString(QtPrivate::qMakeStringPrivate(u"" "QToolButton {" " border: none;" " background: transparent;" " padding: 2px;" "}" "QToolButton:checked {" " background-color: palette(highlight);" " border-radius: 4px;" "}" "QToolButton:!checked:hover {" " background-color: palette(midlight);" " border-radius: 4px;" "}")));
330 | }
331 |
332 | } // namespace LuaDbgBreakpointModes
333 |
334 | /* ===== breakpoint_inline_editor ===== */
335 |
336 |
337 | BreakpointInlineLineEdit::BreakpointInlineLineEdit(QWidget *parent) : QLineEdit(parent) {}
338 |
339 | void BreakpointInlineLineEdit::setEmbeddedWidgets(QComboBox *modeCombo, QComboBox *hitModeCombo,
340 | QToolButton *pauseButton)
341 | {
342 | modeCombo_ = modeCombo;
343 | hitModeCombo_ = hitModeCombo;
344 | pauseButton_ = pauseButton;
345 | relayout();
346 | }
347 |
348 | void BreakpointInlineLineEdit::relayout()
349 | {
350 | if (!modeCombo_ || width() <= 0)
351 | {
352 | return;
353 | }
354 |
355 | const int kInnerGap = 4;
356 | /* The frame width Qt's style draws around the line edit's
357 | * content rect. We push our embedded widgets just inside the
358 | * frame so they don't overlap the native border. */
359 | QStyleOptionFrame opt;
360 | initStyleOption(&opt);
361 | const int frameW = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt, this);
362 |
363 | /* Vertically center every embedded widget on the line edit's
364 | * own visual mid-line, using each widget's natural sizeHint
365 | * height. This is the same alignment QLineEdit's built-in
366 | * trailing/leading actions use, and it's what makes the row
367 | * read as one coherent control on every platform — combos
368 | * with a different intrinsic height than the line edit's text
369 | * area sit pixel-aligned with the caret rather than stretched
370 | * top-to-bottom. */
371 | const auto centeredRect = [this](const QSize &hint, int x)
372 | {
373 | int h = hint.height();
374 | if (h > height())
375 | {
376 | h = height();
377 | }
378 | const int y = (height() - h) / 2;
379 | return QRect(x, y, hint.width(), h);
380 | };
381 |
382 | /* QMacStyle paints the @c QComboBox's native popup arrow with
383 | * one pixel of optical padding above the label, which makes
384 | * the combo's text baseline read 1 px higher than the
385 | * @c QLineEdit's caret baseline when both are vertically
386 | * centered in the same row. Other platforms render the combo
387 | * flush with the line edit's text, so the nudge is macOS-only.
388 | * Both combos (mode on the left, hit-count comparison on the
389 | * right) need the same nudge so they land on a shared
390 | * baseline. */
391 | #ifdef Q_OS_MACOS
392 | constexpr int comboBaselineNudge = 1;
393 | #else
394 | constexpr int comboBaselineNudge = 0;
395 | #endif
396 |
397 | int leftEdge = frameW + kInnerGap;
398 | int rightEdge = width() - frameW - kInnerGap;
399 |
400 | const QSize modeHint = modeCombo_->sizeHint();
401 | QRect modeRect = centeredRect(modeHint, leftEdge);
402 | modeRect.translate(0, comboBaselineNudge);
403 | modeCombo_->setGeometry(modeRect);
404 | leftEdge += modeHint.width() + kInnerGap;
405 |
406 | if (pauseButton_ && !pauseButton_->isHidden())
407 | {
408 | /* Force the toggle's height to @c editor.height() - 6 so
409 | * its Highlight-color chip clears the line edit's frame
410 | * by 3 px on top and 3 px on bottom regardless of the
411 | * @c QToolButton's natural sizeHint.
412 | *
413 | * Two things conspire against a "shrink to a smaller
414 | * height" attempt that goes through sizeHint or
415 | * @c centeredRect:
416 | * - @c centeredRect clamps @c h to @c editor.height()
417 | * when sizeHint is taller, undoing any pre-shrink.
418 | * - @c QToolButton's @c sizeHint() can be smaller than
419 | * the editor on some platforms, so a @c qMin with
420 | * sizeHint silently keeps the natural (larger
421 | * relative to the chosen inset) height.
422 | *
423 | * @c setMaximumHeight is the belt-and-braces lock —
424 | * @c setGeometry alone is enough today, but a future
425 | * re-layout triggered by Qt's polish / size-policy
426 | * machinery would otherwise bring back the natural
427 | * height. The chip stylesheet renders at the button's
428 | * geometry, so capping the geometry caps the chip. */
429 | const QSize hint = pauseButton_->sizeHint();
430 | const int h = qMax(0, height() - 6);
431 | rightEdge -= hint.width();
432 | pauseButton_->setMaximumHeight(h);
433 | const int y = (height() - h) / 2;
434 | pauseButton_->setGeometry(rightEdge, y, hint.width(), h);
435 | rightEdge -= kInnerGap;
436 | }
437 | if (hitModeCombo_ && !hitModeCombo_->isHidden())
438 | {
439 | const QSize hint = hitModeCombo_->sizeHint();
440 | rightEdge -= hint.width();
441 | QRect hitRect = centeredRect(hint, rightEdge);
442 | hitRect.translate(0, comboBaselineNudge);
443 | hitModeCombo_->setGeometry(hitRect);
444 | rightEdge -= kInnerGap;
445 | }
446 |
447 | /* setTextMargins reserves space inside the line edit's content
448 | * rect for our embedded widgets — the typing area and the
449 | * placeholder text never collide with the combo / checkbox. */
450 | const int leftMargin = leftEdge - frameW;
451 | const int rightMargin = (width() - frameW) - rightEdge;
452 | setTextMargins(leftMargin, 0, rightMargin, 0);
453 | }
454 |
455 | void BreakpointInlineLineEdit::resizeEvent(QResizeEvent *e)
456 | {
457 | QLineEdit::resizeEvent(e);
458 | relayout();
459 | }
460 |
461 | void BreakpointInlineLineEdit::showEvent(QShowEvent *e)
462 | {
463 | QLineEdit::showEvent(e);
464 | /* The editor was created and configured (mode, visibility of
465 | * the auxiliary widgets) before the view called show() on us.
466 | * Any earlier @c relayout() bailed out on width()==0; this
467 | * is the first time we're guaranteed to have a real size and
468 | * a settled visibility for every child. */
469 | relayout();
470 | }
471 |
472 | void BreakpointInlineLineEdit::paintEvent(QPaintEvent *e)
473 | {
474 | QLineEdit::paintEvent(e);
475 | /* Draw an explicit 1 px border on top of the native frame.
476 | * QMacStyle's @c QLineEdit frame is intentionally faint
477 | * (especially in dark mode) and disappears against the row's
478 | * highlight; embedding mode / hit-mode combos and the pause
479 | * toggle as children clutters the cell further, so without a
480 | * visible border the user can no longer tell where the
481 | * editable area begins and ends. We draw with @c QPalette::Mid
482 | * so the stroke adapts to light and dark themes automatically.
483 | *
484 | * Antialiasing is left off so the 1 px stroke lands on integer
485 | * pixel boundaries — a crisp line rather than a half-bright
486 | * 2 px smear — and we inset by 1 pixel so the border lives
487 | * inside the widget rect (which @c QLineEdit::paintEvent has
488 | * just painted) instead of outside it where the native focus
489 | * ring lives. */
490 | QPainter p(this);
491 | QPen pen(palette().color(QPalette::Active, QPalette::Mid));
492 | pen.setWidth(1);
493 | pen.setCosmetic(true);
494 | p.setPen(pen);
495 | p.setBrush(Qt::NoBrush);
496 | p.drawRect(rect().adjusted(0, 0, -1, -1));
497 | }
498 |
499 | /* ===== breakpoint_delegate ===== */
500 |
501 | // Inline editor for the Breakpoints list's Location column. A small mode
502 | // picker on the left
503 | // (Expression / Hit Count / Log Message) reconfigures the value line
504 | // edit's validator / placeholder / tooltip to match the chosen mode and
505 | // stashes the previously-typed text under a per-mode draft slot so
506 | // switching back restores it. The Hit Count mode restricts input to
507 | // non-negative integers via a QIntValidator. Each commit updates only
508 | // the selected mode's field; the others are preserved unchanged on the
509 | // model item.
510 | //
511 | // The editor IS the value @c QLineEdit (see @c BreakpointInlineLineEdit
512 | // above); the mode combo, hit-mode combo and pause checkbox are
513 | // children of the line edit, positioned in the line edit's text
514 | // margins. This keeps the inline editor's parent chain identical to
515 | // the Watch tree's bare-@c QLineEdit editor, so the platform's native
516 | // style draws both trees' edit fields at exactly the same height with
517 | // exactly the same frame, focus ring and selection colours.
518 | //
519 | // Adding a fourth mode in the future is a one-row append to
520 | // @c LuaDbgBreakpointModes::kBreakpointEditModes plus an extension of
521 | // @c LuaDbgBreakpointModes::applyEditorMode and the commit/load logic
522 | // in @c setEditorData / @c setModelData below; no other site needs to
523 | // change.
524 |
525 | LuaDbgBreakpointConditionDelegate::LuaDbgBreakpointConditionDelegate(LuaDebuggerDialog *dialog)
526 | : QStyledItemDelegate(dialog)
527 | {
528 | }
529 |
530 | QWidget *LuaDbgBreakpointConditionDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /*option*/,
531 | const QModelIndex & /*index*/) const
532 | {
533 | using namespace LuaDbgBreakpointModes;
534 |
535 | /* The editor IS a @c QLineEdit — same widget class as the Watch
536 | * editor, so the platform style draws an identical inline
537 | * edit. The mode combo, hit-count comparison combo and "also
538 | * pause" checkbox are children of the line edit, positioned
539 | * inside the line edit's text-margin area by
540 | * @ref BreakpointInlineLineEdit::relayout. */
541 | BreakpointInlineLineEdit *editor = new BreakpointInlineLineEdit(parent);
542 | /* Suppress the macOS focus ring around the actively edited
543 | * cell — same rationale as the Watch editor: the cell
544 | * selection plus the explicit border drawn in
545 | * BreakpointInlineLineEdit::paintEvent already make the
546 | * edited row obvious. No-op on Linux / Windows. */
547 | editor->setAttribute(Qt::WA_MacShowFocusRect, false);
548 |
549 | QComboBox *mode = new QComboBox(editor);
550 | /* Force a Qt-managed popup view. macOS otherwise opens the
551 | * combo as a native NSMenu, which is not a Qt widget and is
552 | * outside the editor's parent chain; while that menu is
553 | * active QApplication::focusWidget() returns @c nullptr, our
554 | * focusChanged listener treats that as "click outside",
555 | * commits the pending edit and tears the editor down before
556 | * the user can pick a row from the dropdown. Setting an
557 | * explicit QListView keeps the popup inside the editor's
558 | * widget tree so isAncestorOf() recognises it as part of the
559 | * edit session. */
560 | mode->setView(new QListView(mode));
561 |
562 | for (const ModeSpec &spec : kBreakpointEditModes)
563 | {
564 | mode->addItem(translatedLabel(spec), static_cast<int>(spec.mode));
565 | }
566 |
567 | /* The hit-count comparison-mode combo and the "also pause"
568 | * checkbox are children of the @c BreakpointInlineLineEdit
569 | * just like the mode combo. They are toggled visible by the
570 | * mode-combo currentIndexChanged handler below; the line
571 | * edit's @c relayout() pass reserves text-margin space for
572 | * whichever ones are currently visible. */
573 | QComboBox *hitModeCombo = new QComboBox(editor);
574 | hitModeCombo->setView(new QListView(hitModeCombo));
575 | /* Labels are deliberately short — the integer field next to
576 | * the combo carries the value of N, and the tooltip below
577 | * spells the modes out in full. The longest label drives the
578 | * combo's sizeHint width inside the inline editor; keeping
579 | * them at 1–5 visible characters lets the row stay narrow
580 | * even on tight columns. */
581 | hitModeCombo->addItem(QCoreApplication::translate("BreakpointConditionDelegate", "from"),
582 | static_cast<int>(WSLUA_HIT_COUNT_MODE_FROM));
583 | hitModeCombo->addItem(QCoreApplication::translate("BreakpointConditionDelegate", "every"),
584 | static_cast<int>(WSLUA_HIT_COUNT_MODE_EVERY));
585 | hitModeCombo->addItem(QCoreApplication::translate("BreakpointConditionDelegate", "once"),
586 | static_cast<int>(WSLUA_HIT_COUNT_MODE_ONCE));
587 | hitModeCombo->setToolTip(QCoreApplication::translate("BreakpointConditionDelegate",
588 | "Comparison mode for the hit count:\n"
589 | "from — pause on every hit from N onwards.\n"
590 | "every — pause on hits N, 2N, 3N…\n"
591 | "once — pause once on the N-th hit and deactivate the "
592 | "breakpoint."));
593 | hitModeCombo->setVisible(false);
594 |
595 | /* Icon-only "also pause" toggle. The horizontal space inside
596 | * the inline editor is tight (the QLineEdit must stay
597 | * usable), so we drop the "Pause" word and rely on the
598 | * platform pause glyph plus the tooltip. We use a checkable
599 | * @c QToolButton (auto-raise, icon-only) rather than a
600 | * @c QCheckBox so the cell shows just the pause glyph
601 | * without an empty @c QCheckBox indicator next to it; the
602 | * tool button's depressed-state visual already conveys the
603 | * "checked" semantics. The accessibility name preserves the
604 | * textual label for screen readers. */
605 | QToolButton *pauseChk = new QToolButton(editor);
606 | pauseChk->setCheckable(true);
607 | pauseChk->setFocusPolicy(Qt::TabFocus);
608 | pauseChk->setToolButtonStyle(Qt::ToolButtonIconOnly);
609 | /* Icon is drawn from the editor's own palette so the bars
610 | * automatically read white in dark mode and black in light
611 | * mode — a fixed stock pixmap would be near-invisible in
612 | * one of the two themes. */
613 | pauseChk->setIcon(makePauseIcon(editor->palette()));
614 | pauseChk->setIconSize(QSize(16, 16));
615 | /* Stylesheet drives the on/off background: transparent when
616 | * unchecked (just the bars on the cell background), full
617 | * Highlight-color rounded chip filling the button when
618 | * checked. The chip is the primary on/off signal; the icon
619 | * colors (ButtonText vs HighlightedText) follow it.
620 | *
621 | * Using a stylesheet here also disables @c autoRaise (which
622 | * is no longer needed since we paint our own hover / pressed
623 | * feedback) — both controls would otherwise compete and
624 | * leave the button looking ambiguous. */
625 | pauseChk->setStyleSheet(pauseToggleStyleSheet());
626 | pauseChk->setAccessibleName(QCoreApplication::translate("BreakpointConditionDelegate", "Pause"));
627 | pauseChk->setToolTip(QCoreApplication::translate("BreakpointConditionDelegate",
628 | "Pause: format and emit the log message AND pause "
629 | "execution.\n"
630 | "Off = logpoint only (matches the historical "
631 | "\"logpoints never pause\" convention)."));
632 | pauseChk->setVisible(false);
633 |
634 | editor->setEmbeddedWidgets(mode, hitModeCombo, pauseChk);
635 |
636 | editor->setProperty("luaDbgModeCombo", QVariant::fromValue<QObject *>(mode));
637 | editor->setProperty("luaDbgHitModeCombo", QVariant::fromValue<QObject *>(hitModeCombo));
638 | editor->setProperty("luaDbgPauseCheckBox", QVariant::fromValue<QObject *>(pauseChk));
639 |
640 | /* Per-mode draft text caches. The editor is a single line edit
641 | * shared across all three modes, so when the user switches mode
642 | * we have to remember what they typed under the previous mode
643 | * and restore what they had typed (or the persisted value, see
644 | * setEditorData) under the new mode. */
645 | editor->setProperty(draftPropertyName(Mode::Expression), QString());
646 | editor->setProperty(draftPropertyName(Mode::HitCount), QString());
647 | editor->setProperty(draftPropertyName(Mode::LogMessage), QString());
648 | /* -1 means "not initialised yet" so the very first
649 | * applyEditorMode does not write the empty current text into a
650 | * draft slot before it has loaded the actual draft. */
651 | editor->setProperty("luaDbgCurrentMode", -1);
652 |
653 | QObject::connect(mode, QOverload<int>::of(&QComboBox::currentIndexChanged), editor,
654 | [editor](int idx) { applyEditorMode(editor, idx); });
655 |
656 | /* Install the event filter only on widgets whose lifetime
657 | * we explicitly manage:
658 | * - the editor itself, which IS the QLineEdit (focus /
659 | * Escape / generic safety net),
660 | * - the popup view of every QComboBox in the editor
661 | * (Show/Hide tracking; lets the focus-out commit logic
662 | * keep the editor alive while any combo dropdown is
663 | * open, including the inner hit-count-mode combo).
664 | *
665 | * Restricting the filter to widgets we own keeps @c watched
666 | * pointers stable: events emitted from partially-destroyed
667 | * children during editor teardown (e.g. ~QComboBox calling
668 | * close()/setVisible(false) and emitting Hide) never reach
669 | * the filter, so qobject_cast on the watched pointer cannot
670 | * dereference a freed vtable. */
671 | LuaDbgBreakpointConditionDelegate *self = const_cast<LuaDbgBreakpointConditionDelegate *>(this);
672 | editor->installEventFilter(self);
673 | const auto installPopupFilter = [self, editor](QComboBox *combo)
674 | {
675 | if (!combo || !combo->view())
676 | {
677 | return;
678 | }
679 | /* Tag the view with its owning editor so the eventFilter
680 | * Show/Hide branch can update the popup-open counter
681 | * without walking the parent chain (which during a
682 | * shown-popup state goes through Qt's internal
683 | * QComboBoxPrivateContainer top-level, not the editor). */
684 | combo->view()->setProperty("luaDbgEditorOwner", QVariant::fromValue<QObject *>(editor));
685 | combo->view()->installEventFilter(self);
686 | };
687 | installPopupFilter(mode);
688 | for (QComboBox *c : editor->findChildren<QComboBox *>())
689 | {
690 | if (c != mode)
691 | {
692 | installPopupFilter(c);
693 | }
694 | }
695 |
696 | /* Commit-on-Enter inside the value editors.
697 | *
698 | * Wired via @c QLineEdit::returnPressed on every QLineEdit
699 | * inside the stack pages. We also walk page descendants so
700 | * a future page that hosts multiple QLineEdit children is
701 | * covered without changes here.
702 | *
703 | * The closeEditorOnAccept lambda is one-shot per editor —
704 | * the @c luaDbgClosing guard ensures commitData/closeEditor
705 | * are emitted at most once. Enter, focus loss and the
706 | * delegate's own event filter can race to commit, and
707 | * re-emitting on an already-tearing-down editor crashes the
708 | * view. */
709 | const auto closeEditorOnAccept = [self](QWidget *editorWidget)
710 | {
711 | if (!editorWidget)
712 | {
713 | return;
714 | }
715 | if (editorWidget->property("luaDbgClosing").toBool())
716 | {
717 | return;
718 | }
719 | editorWidget->setProperty("luaDbgClosing", true);
720 | emit self->commitData(editorWidget);
721 | emit self->closeEditor(editorWidget, QAbstractItemDelegate::SubmitModelCache);
722 | };
723 | QObject::connect(editor, &QLineEdit::returnPressed, editor,
724 | [closeEditorOnAccept, editor]() { closeEditorOnAccept(editor); });
725 |
726 | /* The editor IS the value line edit, so it receives keyboard
727 | * focus by default when QAbstractItemView shows it. The mode
728 | * combo, hit-mode combo and pause checkbox are reachable with
729 | * Tab as ordinary children of the line edit. */
730 |
731 | /* Click-outside-to-commit. QStyledItemDelegate's built-in
732 | * "FocusOut closes the editor" hook only watches the editor
733 | * widget itself; if the user opens the mode combo's popup and
734 | * then clicks somewhere outside the row, focus moves to a
735 | * widget that is neither the editor nor a descendant, so the
736 | * built-in handler doesn't fire — we have to do this in
737 | * @c QApplication::focusChanged instead.
738 | *
739 | * Listen to QApplication::focusChanged instead, deferring the
740 | * decision via a zero-delay timer so the new focus has settled
741 | * (covers both ordinary clicks elsewhere and clicks that land
742 | * on a widget with no focus policy, where focusWidget() ends
743 | * up @c nullptr). The combo's popup and any tooltip we show
744 | * all stay descendants of @a editor and leave the editor
745 | * open. */
746 | QPointer<QWidget> editorGuard(editor);
747 | QPointer<QComboBox> modeGuard(mode);
748 | QPointer<QAbstractItemView> popupGuard(mode->view());
749 | /* Helper: is the user currently inside the mode combo's
750 | * dropdown? Combines the explicit open/close flag we set from
751 | * the eventFilter (most reliable) with `view->isVisible()`
752 | * as a backup; in either case we treat "popup is open" as
753 | * "still inside the editor" so the editor doesn't close
754 | * while the user is picking a mode. */
755 | auto popupOpen = [editorGuard, popupGuard]()
756 | {
757 | if (editorGuard && editorGuard->property("luaDbgPopupOpen").toBool())
758 | {
759 | return true;
760 | }
761 | return popupGuard && popupGuard->isVisible();
762 | };
763 | /* Helper: should the focus shift to @a w be treated as "still
764 | * inside the editor"? True for the editor itself, any
765 | * descendant, the mode combo or its descendants, and the
766 | * combo popup view (which Qt may parent via a top-level
767 | * Qt::Popup window — so isAncestorOf isn't reliable across
768 | * platforms). */
769 | auto stillInside = [editorGuard, modeGuard, popupGuard](QWidget *w)
770 | {
771 | if (!w)
772 | {
773 | return false;
774 | }
775 | if (editorGuard && (w == editorGuard.data() || editorGuard->isAncestorOf(w)))
776 | {
777 | return true;
778 | }
779 | if (modeGuard && (w == modeGuard.data() || modeGuard->isAncestorOf(w)))
780 | {
781 | return true;
782 | }
783 | if (popupGuard && (w == popupGuard.data() || popupGuard->isAncestorOf(w)))
784 | {
785 | return true;
786 | }
787 | return false;
788 | };
789 | QObject::connect(qApp(static_cast<QApplication *>(QCoreApplication::instance ())), &QApplication::focusChanged, editor,
790 | [self, editorGuard, popupOpen, stillInside](QWidget *old, QWidget *now)
791 | {
792 | if (!editorGuard)
793 | {
794 | return;
795 | }
796 | /* Already torn down or in the process of being torn
797 | * down by another commit path (Enter via
798 | * returnPressed, or a previous focus-loss tick).
799 | * Re-emitting commitData / closeEditor on a
800 | * deleteLater'd editor crashes the view. */
801 | if (editorGuard->property("luaDbgClosing").toBool())
802 | {
803 | return;
804 | }
805 | if (popupOpen())
806 | {
807 | return;
808 | }
809 | if (stillInside(now))
810 | {
811 | return;
812 | }
813 | /* Transient null-focus state (e.g. native menu/popup
814 | * just took focus, app deactivation, or focus moving
815 | * through a non-Qt widget): keep the editor open. The
816 | * deferred timer below re-checks once focus settles. */
817 | if (!now)
818 | {
819 | if (stillInside(old))
820 | {
821 | QTimer::singleShot(0, editorGuard.data(),
822 | [editorGuard, popupOpen, stillInside, self]()
823 | {
824 | if (!editorGuard)
825 | {
826 | return;
827 | }
828 | if (editorGuard->property("luaDbgClosing").toBool())
829 | {
830 | return;
831 | }
832 | if (popupOpen())
833 | {
834 | return;
835 | }
836 | QWidget *fw = QApplication::focusWidget();
837 | if (!fw || stillInside(fw))
838 | {
839 | return;
840 | }
841 | editorGuard->setProperty("luaDbgClosing", true);
842 | emit self->commitData(editorGuard.data());
843 | emit self->closeEditor(
844 | editorGuard.data(),
845 | QAbstractItemDelegate::SubmitModelCache);
846 | });
847 | }
848 | return;
849 | }
850 | editorGuard->setProperty("luaDbgClosing", true);
851 | emit self->commitData(editorGuard.data());
852 | emit self->closeEditor(editorGuard.data(), QAbstractItemDelegate::SubmitModelCache);
853 | });
854 |
855 | return editor;
856 | }
857 |
858 | void LuaDbgBreakpointConditionDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
859 | {
860 | using namespace LuaDbgBreakpointModes;
861 |
862 | QLineEdit *valueEdit = qobject_cast<QLineEdit *>(editor);
863 | QComboBox *mode = qobject_cast<QComboBox *>(editor->property("luaDbgModeCombo").value<QObject *>());
864 | if (!valueEdit || !mode)
865 | {
866 | return;
867 | }
868 |
869 | const QAbstractItemModel *model = index.model();
870 | const QModelIndex activeIndex = model->index(index.row(), BreakpointColumn::Active, index.parent());
871 |
872 | const QString condition = model->data(activeIndex, BreakpointConditionRole).toString();
873 | const qint64 target = model->data(activeIndex, BreakpointHitTargetRole).toLongLong();
874 | const int hitMode = model->data(activeIndex, BreakpointHitModeRole).toInt();
875 | const QString logMessage = model->data(activeIndex, BreakpointLogMessageRole).toString();
876 |
877 | /* Seed the per-mode draft caches with the persisted values
878 | * before applyEditorMode() runs — applyEditorMode loads the
879 | * draft for the active mode into the line edit. The Hit Count
880 | * cache is the integer rendered as a string (empty for
881 | * target == 0 so the field reads as unconfigured rather than
882 | * literal "0"). */
883 | editor->setProperty(draftPropertyName(Mode::Expression), condition);
884 | editor->setProperty(draftPropertyName(Mode::HitCount), target > 0 ? QString::number(target) : QString());
885 | editor->setProperty(draftPropertyName(Mode::LogMessage), logMessage);
886 |
887 | if (QComboBox *hitModeCombo = editorHitModeCombo(editor))
888 | {
889 | const int comboIdx = hitModeCombo->findData(hitMode);
890 | hitModeCombo->setCurrentIndex(comboIdx >= 0 ? comboIdx : 0);
891 | }
892 | if (QToolButton *logPauseChk = editorPauseToggle(editor))
893 | {
894 | logPauseChk->setChecked(model->data(activeIndex, BreakpointLogAlsoPauseRole).toBool());
895 | }
896 |
897 | Mode initial = Mode::Expression;
898 | if (!logMessage.isEmpty())
899 | {
900 | initial = Mode::LogMessage;
901 | }
902 | else if (!condition.isEmpty())
903 | {
904 | initial = Mode::Expression;
905 | }
906 | else if (target > 0)
907 | {
908 | initial = Mode::HitCount;
909 | }
910 |
911 | const int idx = mode->findData(static_cast<int>(initial));
912 | if (idx >= 0)
913 | {
914 | /* setCurrentIndex fires currentIndexChanged when the index
915 | * actually changes, which the connected handler routes to
916 | * applyEditorMode. The very first edit opens with the combo
917 | * at its default index 0 (Expression); if @c initial is
918 | * also Expression, no change → no signal → the line edit
919 | * would never get seeded. Always invoke applyEditorMode
920 | * explicitly here so the editor is fully configured
921 | * regardless of whether the index changed. */
922 | QSignalBlocker blocker(mode);
923 | mode->setCurrentIndex(idx);
924 | blocker.unblock();
925 | applyEditorMode(editor, idx);
926 | }
927 | }
928 |
929 | void LuaDbgBreakpointConditionDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
930 | const QModelIndex &index) const
931 | {
932 | using namespace LuaDbgBreakpointModes;
933 |
934 | QLineEdit *valueEdit = qobject_cast<QLineEdit *>(editor);
935 | QComboBox *mode = qobject_cast<QComboBox *>(editor->property("luaDbgModeCombo").value<QObject *>());
936 | if (!valueEdit || !mode)
937 | {
938 | return;
939 | }
940 |
941 | const Mode chosen = static_cast<Mode>(mode->currentData().toInt());
942 | const QModelIndex activeIndex = model->index(index.row(), BreakpointColumn::Active, index.parent());
943 | const QString currentText = valueEdit->text();
944 |
945 | switch (chosen)
946 | {
947 | case Mode::Expression:
948 | {
949 | /* Accept whatever the user typed unconditionally — empty
950 | * (clears the condition) or syntactically invalid (the
951 | * dispatch in LuaDebuggerBreakpointsController::onModelDataChanged
952 | * runs the parse checker after writing the condition and stamps
953 | * the row with the @c condition_error warning icon + error
954 | * string tooltip immediately, so a typo is visible at commit
955 | * time rather than only after the line has been hit). */
956 | model->setData(activeIndex, currentText.trimmed(), BreakpointConditionRole);
957 | return;
958 | }
959 | case Mode::HitCount:
960 | {
961 | /* Empty / non-numeric / negative input maps to 0 ("no hit
962 | * count"). The QIntValidator on the editor already rejects
963 | * negatives and non-digits during typing, but we still
964 | * tolerate empty text here so an explicit clear commits
965 | * cleanly. */
966 | const QString text = currentText.trimmed();
967 | bool ok = false;
968 | const qlonglong v = text.toLongLong(&ok);
969 | const qlonglong target = (ok && v > 0) ? v : 0;
970 | model->setData(activeIndex, target, BreakpointHitTargetRole);
971 | /* Persist the comparison-mode pick alongside the integer so
972 | * the dispatch in LuaDebuggerBreakpointsController::onModelDataChanged
973 | * can forward both to the core in one tick. The mode is
974 | * meaningful only when target > 0; we still write it for
975 | * target == 0 so toggling the value back on later remembers
976 | * the previous mode. */
977 | if (QComboBox *hitModeCombo = editorHitModeCombo(editor))
978 | {
979 | model->setData(activeIndex, hitModeCombo->currentData().toInt(), BreakpointHitModeRole);
980 | }
981 | return;
982 | }
983 | case Mode::LogMessage:
984 | {
985 | /* Do NOT trim — leading / trailing whitespace can be
986 | * intentional in a log line. */
987 | model->setData(activeIndex, currentText, BreakpointLogMessageRole);
988 | if (QToolButton *logPauseChk = editorPauseToggle(editor))
989 | {
990 | model->setData(activeIndex, logPauseChk->isChecked(), BreakpointLogAlsoPauseRole);
991 | }
992 | return;
993 | }
994 | }
995 | }
996 |
997 | void LuaDbgBreakpointConditionDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option,
998 | const QModelIndex & /*index*/) const
999 | {
1000 | /* Use the row rect, but ensure the editor is at least as tall
1001 | * as a QLineEdit's natural sizeHint so the inline inputs read
1002 | * at the same comfortable height as the Watch inline editor.
1003 | * The accompanying @ref sizeHint override keeps the row itself
1004 | * tall enough to host this geometry without overlapping the
1005 | * row below. */
1006 | QRect rect = option.rect;
1007 | const int preferred = preferredEditorHeight();
1008 | if (rect.height() < preferred)
1009 | {
1010 | rect.setHeight(preferred);
1011 | }
1012 | editor->setGeometry(rect);
1013 | }
1014 |
1015 | QSize LuaDbgBreakpointConditionDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
1016 | {
1017 | /* The Watch tree's inline QLineEdit reads taller than the
1018 | * default text-only Breakpoints row because the row height
1019 | * matches QLineEdit::sizeHint(); mirror that on this column so
1020 | * the two inline editors visually agree. The row itself
1021 | * inherits this height through QTreeView's per-row sizing. */
1022 | QSize base = QStyledItemDelegate::sizeHint(option, index);
1023 | const int preferred = preferredEditorHeight();
1024 | if (base.height() < preferred)
1025 | {
1026 | base.setHeight(preferred);
1027 | }
1028 | return base;
1029 | }
1030 |
1031 | bool LuaDbgBreakpointConditionDelegate::eventFilter(QObject *watched, QEvent *event)
1032 | {
1033 | /* Track the open state of every QComboBox popup inside the
1034 | * editor via Show/Hide events on its view. We can't rely on
1035 | * `view->isVisible()` racing with focusChanged, and Qt has
1036 | * no aboutToShow/aboutToHide signal on QComboBox we can use
1037 | * here. We store a refcount on the editor (luaDbgPopupOpenCount)
1038 | * so that ANY open dropdown — outer mode selector or the
1039 | * inner hit-count-mode combo — keeps the editor alive
1040 | * during focus shifts to its popup. The boolean
1041 | * luaDbgPopupOpen is also kept in sync as a convenience for
1042 | * existing readers.
1043 | *
1044 | * @c watched is guaranteed to be a popup view we explicitly
1045 | * installed on in createEditor(), and its
1046 | * @c luaDbgEditorOwner property points to the owning editor
1047 | * that we set at install time. We avoid walking the runtime
1048 | * parent chain because Qt reparents popup views into a
1049 | * private top-level container while the popup is shown. */
1050 | if (event->type() == QEvent::Show || event->type() == QEvent::Hide)
1051 | {
1052 | QWidget *view = qobject_cast<QWidget *>(watched);
1053 | if (view)
1054 | {
1055 | QWidget *owner = qobject_cast<QWidget *>(view->property("luaDbgEditorOwner").value<QObject *>());
1056 | if (owner)
1057 | {
1058 | int n = owner->property("luaDbgPopupOpenCount").toInt();
1059 | if (event->type() == QEvent::Show)
1060 | {
1061 | ++n;
1062 | }
1063 | else if (n > 0)
1064 | {
1065 | --n;
1066 | }
1067 | owner->setProperty("luaDbgPopupOpenCount", n);
1068 | owner->setProperty("luaDbgPopupOpen", n > 0);
1069 | }
1070 | }
1071 | }
1072 | /* Enter is intentionally NOT handled here. The dialog installs
1073 | * its own descendant-shortcut filter and the platform input
1074 | * method can both reorder/swallow key events before our
1075 | * delegate filter sees them, which made an event-filter-based
1076 | * Enter handler unreliable in practice. We instead wire the
1077 | * QLineEdit's canonical "user accepted the input" signal
1078 | * (returnPressed) in createEditor(); that is emitted by Qt
1079 | * only after the widget has actually processed the key, and
1080 | * it fires even when an outside filter swallowed the
1081 | * QKeyEvent.
1082 | *
1083 | * We still handle Escape here because there is no Qt signal
1084 | * for "user pressed Escape" on a QLineEdit. */
1085 | if (event->type() == QEvent::KeyPress)
1086 | {
1087 | QKeyEvent *ke = static_cast<QKeyEvent *>(event);
1088 | const int key = ke->key();
1089 | if (key != Qt::Key_Escape)
1090 | {
1091 | return QStyledItemDelegate::eventFilter(watched, event);
1092 | }
1093 |
1094 | QWidget *editor = qobject_cast<QWidget *>(watched);
1095 | if (!editor || !editor->isAncestorOf(QApplication::focusWidget()))
1096 | {
1097 | if (QWidget *w = qobject_cast<QWidget *>(watched))
1098 | {
1099 | editor = w;
1100 | while (editor->parentWidget())
1101 | {
1102 | if (editor->property("luaDbgModeCombo").isValid())
1103 | {
1104 | break;
1105 | }
1106 | editor = editor->parentWidget();
1107 | }
1108 | }
1109 | }
1110 | if (!editor)
1111 | {
1112 | return QStyledItemDelegate::eventFilter(watched, event);
1113 | }
1114 |
1115 | /* Don't hijack Escape inside the mode combo or its popup;
1116 | * the combo uses Escape to dismiss its dropdown, and we
1117 | * want that to keep the editor open. */
1118 | QComboBox *modeCombo = qobject_cast<QComboBox *>(editor->property("luaDbgModeCombo").value<QObject *>());
1119 | QWidget *watchedWidget = qobject_cast<QWidget *>(watched);
1120 | const bool inModeCombo =
1121 | modeCombo && watchedWidget &&
1122 | (watchedWidget == modeCombo || modeCombo->isAncestorOf(watchedWidget) ||
1123 | (modeCombo->view() &&
1124 | (watchedWidget == modeCombo->view() || modeCombo->view()->isAncestorOf(watchedWidget))));
1125 | if (inModeCombo)
1126 | {
1127 | return QStyledItemDelegate::eventFilter(watched, event);
1128 | }
1129 |
1130 | editor->setProperty("luaDbgClosing", true);
1131 | emit closeEditor(editor, RevertModelCache);
1132 | return true;
1133 | }
1134 | return QStyledItemDelegate::eventFilter(watched, event);
1135 | }
1136 |
1137 | int LuaDbgBreakpointConditionDelegate::preferredEditorHeight() const
1138 | {
1139 | if (cachedPreferredHeight_ <= 0)
1140 | {
1141 | QLineEdit probe;
1142 | cachedPreferredHeight_ = probe.sizeHint().height();
1143 | }
1144 | return cachedPreferredHeight_;
1145 | }
1146 |
1147 | /* ===== breakpoints_controller ===== */
1148 |
1149 | LuaDebuggerBreakpointsController::LuaDebuggerBreakpointsController(LuaDebuggerDialog *host) : QObject(host), host_(host)
1150 | {
1151 | }
1152 |
1153 | void LuaDebuggerBreakpointsController::attach(QTreeView *tree, QStandardItemModel *model)
1154 | {
1155 | tree_ = tree;
1156 | model_ = model;
1157 | if (!tree_ || !model_)
1158 | {
1159 | return;
1160 | }
1161 |
1162 | connect(model_, &QStandardItemModel::itemChanged, this, &LuaDebuggerBreakpointsController::onItemChanged);
1163 | connect(model_, &QStandardItemModel::dataChanged, this, &LuaDebuggerBreakpointsController::onModelDataChanged);
1164 | connect(tree_, &QTreeView::doubleClicked, this, &LuaDebuggerBreakpointsController::onItemDoubleClicked);
1165 | connect(tree_, &QTreeView::customContextMenuRequested, this, &LuaDebuggerBreakpointsController::showContextMenu);
1166 | connect(model_, &QAbstractItemModel::rowsInserted, this, [this]() { updateHeaderButtonState(); });
1167 | connect(model_, &QAbstractItemModel::rowsRemoved, this, [this]() { updateHeaderButtonState(); });
1168 | connect(model_, &QAbstractItemModel::modelReset, this, [this]() { updateHeaderButtonState(); });
1169 | if (QItemSelectionModel *sel = tree_->selectionModel())
1170 | {
1171 | connect(sel, &QItemSelectionModel::selectionChanged, this, [this]() { updateHeaderButtonState(); });
1172 | }
1173 | updateHeaderButtonState();
1174 | }
1175 |
1176 | void LuaDebuggerBreakpointsController::attachHeaderButtons(QToolButton *toggleAll, QToolButton *remove,
1177 | QToolButton *removeAll, QToolButton *edit,
1178 | QAction *removeAllAction)
1179 | {
1180 | toggleAllButton_ = toggleAll;
1181 | removeButton_ = remove;
1182 | removeAllButton_ = removeAll;
1183 | editButton_ = edit;
1184 | removeAllAction_ = removeAllAction;
1185 |
1186 | if (toggleAllButton_)
1187 | {
1188 | connect(toggleAllButton_, &QToolButton::clicked, this, &LuaDebuggerBreakpointsController::toggleAllActive);
1189 | }
1190 | if (removeButton_)
1191 | {
1192 | connect(removeButton_, &QToolButton::clicked, this, [this]() { removeSelected(); });
1193 | }
1194 | if (removeAllButton_)
1195 | {
1196 | connect(removeAllButton_, &QToolButton::clicked, this, &LuaDebuggerBreakpointsController::clearAll);
1197 | }
1198 | if (editButton_)
1199 | {
1200 | connect(editButton_, &QToolButton::clicked, this,
1201 | [this]()
1202 | {
1203 | if (!tree_)
1204 | {
1205 | return;
1206 | }
1207 | /* Resolve the edit target the same way the context
1208 | * menu does: prefer the focused / current row, fall
1209 | * back to the first selected row when nothing is
1210 | * focused. The button mirrors the Remove button's
1211 | * enable state (any selected row), and
1212 | * startInlineEdit() silently skips stale (file-missing)
1213 | * rows, so an "always single row" launch is enough here. */
1214 | int row = -1;
1215 | const QModelIndex cur = tree_->currentIndex();
1216 | if (cur.isValid())
1217 | {
1218 | row = cur.row();
1219 | }
1220 | else if (QItemSelectionModel *sel = tree_->selectionModel())
1221 | {
1222 | for (const QModelIndex &si : sel->selectedIndexes())
1223 | {
1224 | if (si.isValid())
1225 | {
1226 | row = si.row();
1227 | break;
1228 | }
1229 | }
1230 | }
1231 | startInlineEdit(row);
1232 | });
1233 | }
1234 | if (removeAllAction_)
1235 | {
1236 | connect(removeAllAction_, &QAction::triggered, this, &LuaDebuggerBreakpointsController::clearAll);
1237 | }
1238 | updateHeaderButtonState();
1239 | }
1240 |
1241 | void LuaDebuggerBreakpointsController::configureColumns() const
1242 | {
1243 | if (!tree_ || !tree_->header() || !model_)
1244 | {
1245 | return;
1246 | }
1247 | QHeaderView *breakpointHeader = tree_->header();
1248 | breakpointHeader->setStretchLastSection(true);
1249 | breakpointHeader->setSectionResizeMode(BreakpointColumn::Active, QHeaderView::ResizeToContents);
1250 | breakpointHeader->setSectionResizeMode(BreakpointColumn::Line, QHeaderView::Interactive);
1251 | breakpointHeader->setSectionResizeMode(BreakpointColumn::Location, QHeaderView::Interactive);
1252 | model_->setHeaderData(BreakpointColumn::Location, Qt::Horizontal, host_->tr("Location"));
1253 | tree_->setColumnHidden(BreakpointColumn::Line, true);
1254 | tree_->setColumnWidth(BreakpointColumn::Active, tree_->fontMetrics().height() * 4);
1255 | }
1256 |
1257 | void LuaDebuggerBreakpointsController::startInlineEdit(int row)
1258 | {
1259 | if (!model_ || !tree_)
1260 | {
1261 | return;
1262 | }
1263 | if (row < 0 || row >= model_->rowCount())
1264 | {
1265 | return;
1266 | }
1267 | const QModelIndex editTarget = model_->index(row, BreakpointColumn::Location);
1268 | if (!editTarget.isValid() || !(editTarget.flags() & Qt::ItemIsEditable))
1269 | {
1270 | return;
1271 | }
1272 | tree_->setCurrentIndex(editTarget);
1273 | tree_->scrollTo(editTarget);
1274 | tree_->edit(editTarget);
1275 | }
1276 |
1277 | void LuaDebuggerBreakpointsController::onItemDoubleClicked(const QModelIndex &index)
1278 | {
1279 | if (!index.isValid() || !model_)
1280 | {
1281 | return;
1282 | }
1283 | QStandardItem *activeItem = model_->item(index.row(), BreakpointColumn::Active);
1284 | if (!activeItem)
1285 | {
1286 | return;
1287 | }
1288 | const QString file = activeItem->data(BreakpointFileRole).toString();
1289 | const int64_t lineNumber = activeItem->data(BreakpointLineRole).toLongLong();
1290 | if (file.isEmpty() || lineNumber <= 0)
1291 | {
1292 | return;
1293 | }
1294 | LuaDebuggerCodeView *view = host_->codeTabsController().loadFile(file);
1295 | if (view)
1296 | {
1297 | view->moveCaretToLineStart(static_cast<qint32>(lineNumber));
1298 | }
1299 | }
1300 |
1301 | void LuaDebuggerBreakpointsController::showContextMenu(const QPoint &pos)
1302 | {
1303 | if (!tree_ || !model_)
1304 | {
1305 | return;
1306 | }
1307 |
1308 | const QModelIndex ix = tree_->indexAt(pos);
1309 | if (ix.isValid() && tree_->selectionModel() && !tree_->selectionModel()->isRowSelected(ix.row(), ix.parent()))
1310 | {
1311 | tree_->setCurrentIndex(ix);
1312 | }
1313 |
1314 | QMenu menu(host_);
1315 | QAction *editAct = nullptr;
1316 | QAction *openAct = nullptr;
1317 | QAction *resetHitsAct = nullptr;
1318 | QAction *removeAct = nullptr;
1319 |
1320 | auto rowHasResettableHits = [this](int row) -> bool
1321 | {
1322 | QStandardItem *activeItem = model_->item(row, BreakpointColumn::Active);
1323 | if (!activeItem)
1324 | return false;
1325 | const qlonglong target = activeItem->data(BreakpointHitTargetRole).toLongLong();
1326 | const qlonglong count = activeItem->data(BreakpointHitCountRole).toLongLong();
1327 | return target > 0 || count > 0;
1328 | };
1329 |
1330 | bool anyResettable = false;
1331 | QSet<int> selRowsSet;
1332 | if (tree_->selectionModel())
1333 | {
1334 | for (const QModelIndex &si : tree_->selectionModel()->selectedIndexes())
1335 | {
1336 | if (!si.isValid())
1337 | continue;
1338 | if (selRowsSet.contains(si.row()))
1339 | continue;
1340 | selRowsSet.insert(si.row());
1341 | if (rowHasResettableHits(si.row()))
1342 | {
1343 | anyResettable = true;
1344 | }
1345 | }
1346 | }
1347 | if (selRowsSet.isEmpty() && ix.isValid())
1348 | {
1349 | anyResettable = rowHasResettableHits(ix.row());
1350 | }
1351 |
1352 | bool anyResettableInModel = false;
1353 | {
1354 | const int rc = model_->rowCount();
1355 | for (int r = 0; r < rc; ++r)
1356 | {
1357 | if (rowHasResettableHits(r))
1358 | {
1359 | anyResettableInModel = true;
1360 | break;
1361 | }
1362 | }
1363 | }
1364 |
1365 | if (ix.isValid())
1366 | {
1367 | editAct = menu.addAction(host_->tr("Edit..."));
1368 | editAct->setEnabled(ix.flags() & Qt::ItemIsEditable);
1369 | openAct = menu.addAction(host_->tr("Open Source"));
1370 | menu.addSeparator();
1371 | resetHitsAct = menu.addAction(host_->tr("Reset Hit Count"));
1372 | resetHitsAct->setEnabled(anyResettable);
1373 | menu.addSeparator();
1374 | removeAct = menu.addAction(host_->tr("Remove"));
1375 | removeAct->setShortcut(QKeySequence::Delete);
1376 | }
1377 | QAction *resetAllHitsAct = nullptr;
1378 | QAction *removeAllAct = nullptr;
1379 | if (model_->rowCount() > 0)
1380 | {
1381 | resetAllHitsAct = menu.addAction(host_->tr("Reset All Hit Counts"));
1382 | resetAllHitsAct->setEnabled(anyResettableInModel);
1383 | removeAllAct = menu.addAction(host_->tr("Remove All Breakpoints"));
1384 | removeAllAct->setShortcut(kLuaDbgCtxRemoveAllBreakpoints);
1385 | }
1386 | if (menu.isEmpty())
1387 | {
1388 | return;
1389 | }
1390 |
1391 | QAction *chosen = menu.exec(tree_->viewport()->mapToGlobal(pos));
1392 | if (!chosen)
1393 | {
1394 | return;
1395 | }
1396 | if (chosen == editAct)
1397 | {
1398 | startInlineEdit(ix.row());
1399 | return;
1400 | }
1401 | if (chosen == openAct)
1402 | {
1403 | onItemDoubleClicked(ix);
1404 | return;
1405 | }
1406 | if (chosen == resetHitsAct)
1407 | {
1408 | QSet<int> rows = selRowsSet;
1409 | if (rows.isEmpty() && ix.isValid())
1410 | {
1411 | rows.insert(ix.row());
1412 | }
1413 | for (int row : rows)
1414 | {
1415 | QStandardItem *activeItem = model_->item(row, BreakpointColumn::Active);
1416 | if (!activeItem)
1417 | continue;
1418 | const QString file = activeItem->data(BreakpointFileRole).toString();
1419 | const int64_t line = activeItem->data(BreakpointLineRole).toLongLong();
1420 | if (file.isEmpty() || line <= 0)
1421 | continue;
1422 | wslua_debugger_reset_breakpoint_hit_count(file.toUtf8().constData(), line);
1423 | }
1424 | refreshFromEngine();
1425 | return;
1426 | }
1427 | if (chosen == removeAct)
1428 | {
1429 | removeSelected();
1430 | return;
1431 | }
1432 | if (chosen == resetAllHitsAct)
1433 | {
1434 | wslua_debugger_reset_all_breakpoint_hit_counts();
1435 | refreshFromEngine();
1436 | return;
1437 | }
1438 | if (chosen == removeAllAct)
1439 | {
1440 | clearAll();
1441 | return;
1442 | }
1443 | }
1444 |
1445 | void LuaDebuggerBreakpointsController::clearAll()
1446 | {
1447 | const unsigned count = wslua_debugger_get_breakpoint_count();
1448 | if (count == 0)
1449 | {
1450 | return;
1451 | }
1452 |
1453 | QMessageBox::StandardButton reply =
1454 | QMessageBox::question(host_, host_->tr("Clear All Breakpoints"),
1455 | host_->tr("Are you sure you want to remove %Ln breakpoint(s)?", "", count),
1456 | QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
1457 |
1458 | if (reply != QMessageBox::Yes)
1459 | {
1460 | return;
1461 | }
1462 |
1463 | wslua_debugger_clear_breakpoints();
1464 | refreshFromEngine();
1465 | refreshAllOpenTabMarkers();
1466 | }
1467 |
1468 | void LuaDebuggerBreakpointsController::refreshFromEngine()
1469 | {
1470 | if (!model_)
1471 | {
1472 | return;
1473 | }
1474 | /* Suppress dispatch through onItemChanged while we rebuild the model;
1475 | * the inline-edit slot is fine for user-triggered checkbox /
1476 | * delegate-driven changes but a wholesale rebuild from core would
1477 | * otherwise loop. Restored unconditionally on the function tail so
1478 | * an early return does not leave the flag set. */
1479 | const bool prevSuppress = suppressItemChanged_;
1480 | suppressItemChanged_ = true;
1481 | model_->removeRows(0, model_->rowCount());
1482 | model_->setHeaderData(BreakpointColumn::Location, Qt::Horizontal, host_->tr("Location"));
1483 | unsigned count = wslua_debugger_get_breakpoint_count();
1484 | bool hasActiveBreakpoint = false;
1485 | const bool collectInitialFiles = !tabsPrimed_;
1486 | QVector<QString> initialBreakpointFiles;
1487 | QSet<QString> seenInitialFiles;
1488 | for (unsigned i = 0; i < count; i++)
1489 | {
1490 | const char *file_path = nullptr;
1491 | int64_t line = 0;
1492 | bool active = false;
1493 | const char *condition_c = nullptr;
1494 | int64_t hit_count_target = 0;
1495 | int64_t hit_count = 0;
1496 | bool condition_error = false;
1497 | const char *log_message_c = nullptr;
1498 | wslua_hit_count_mode_t hit_count_mode = WSLUA_HIT_COUNT_MODE_FROM;
1499 | bool log_also_pause = false;
1500 | if (!wslua_debugger_get_breakpoint_extended(i, &file_path, &line, &active, &condition_c, &hit_count_target,
1501 | &hit_count, &condition_error, &log_message_c, &hit_count_mode,
1502 | &log_also_pause))
1503 | {
1504 | continue;
1505 | }
1506 |
1507 | QString normalizedPath = host_->normalizedFilePath(QString::fromUtf8(file_path));
1508 | const QString condition = condition_c ? QString::fromUtf8(condition_c) : QString();
1509 | const QString logMessage = log_message_c ? QString::fromUtf8(log_message_c) : QString();
1510 | const bool hasCondition = !condition.isEmpty();
1511 | const bool hasLog = !logMessage.isEmpty();
1512 | const bool hasHitTarget = hit_count_target > 0;
1513 |
1514 | QFileInfo fileInfo(normalizedPath);
1515 | bool fileExists = fileInfo.exists() && fileInfo.isFile();
1516 |
1517 | QStandardItem *const activeItem = new QStandardItem();
1518 | QStandardItem *const lineItem = new QStandardItem();
1519 | QStandardItem *const locationItem = new QStandardItem();
1520 | /* QStandardItem ships with Qt::ItemIsEditable on by default; the
1521 | * Active checkbox cell and the (hidden) Line cell must not host
1522 | * an editor — the inline condition / hit-count / log-message
1523 | * editor lives on the Location column only. Without this,
1524 | * double-clicking the checkbox column opens a stray QLineEdit
1525 | * over the row. */
1526 | activeItem->setFlags(activeItem->flags() & ~Qt::ItemIsEditable);
1527 | lineItem->setFlags(lineItem->flags() & ~Qt::ItemIsEditable);
1528 | activeItem->setCheckable(true);
1529 | activeItem->setCheckState(active ? Qt::Checked : Qt::Unchecked);
1530 | activeItem->setData(normalizedPath, BreakpointFileRole);
1531 | activeItem->setData(static_cast<qlonglong>(line), BreakpointLineRole);
1532 | activeItem->setData(condition, BreakpointConditionRole);
1533 | activeItem->setData(static_cast<qlonglong>(hit_count_target), BreakpointHitTargetRole);
1534 | activeItem->setData(static_cast<qlonglong>(hit_count), BreakpointHitCountRole);
1535 | activeItem->setData(condition_error, BreakpointConditionErrRole);
1536 | activeItem->setData(logMessage, BreakpointLogMessageRole);
1537 | activeItem->setData(static_cast<int>(hit_count_mode), BreakpointHitModeRole);
1538 | activeItem->setData(log_also_pause, BreakpointLogAlsoPauseRole);
1539 | lineItem->setText(QString::number(line));
1540 | const QString fileDisplayName = fileInfo.fileName();
1541 | QString locationText =
1542 | QStringLiteral("%1:%2")(QString(QtPrivate::qMakeStringPrivate(u"" "%1:%2"))).arg(fileDisplayName.isEmpty() ? normalizedPath : fileDisplayName).arg(line);
1543 | locationItem->setText(locationText);
1544 | locationItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
1545 | /* The location cell is the inline-edit target for condition /
1546 | * hit count / log message. Make it editable on existing files;
1547 | * stale rows below clear the flag. */
1548 | locationItem->setFlags((locationItem->flags() | Qt::ItemIsEditable) & ~Qt::ItemIsUserCheckable);
1549 |
1550 | /* Compose a multi-line tooltip applied to all three cells, so
1551 | * hovering anywhere on the row reveals the full condition / hit
1552 | * count / log details that no longer have a dedicated column. */
1553 | QStringList tooltipLines;
1554 | tooltipLines.append(host_->tr("Location: %1:%2").arg(normalizedPath).arg(line));
1555 | if (hasCondition)
1556 | {
1557 | tooltipLines.append(host_->tr("Condition: %1").arg(condition));
1558 | }
1559 | if (hasHitTarget)
1560 | {
1561 | QString modeDesc;
1562 | switch (hit_count_mode)
1563 | {
1564 | case WSLUA_HIT_COUNT_MODE_EVERY:
1565 | modeDesc = host_->tr("pauses on hits %1, 2\xc3\x97%1, "
1566 | "3\xc3\x97%1, \xe2\x80\xa6")
1567 | .arg(hit_count_target);
1568 | break;
1569 | case WSLUA_HIT_COUNT_MODE_ONCE:
1570 | modeDesc = host_->tr("pauses once on hit %1, then deactivates the "
1571 | "breakpoint")
1572 | .arg(hit_count_target);
1573 | break;
1574 | case WSLUA_HIT_COUNT_MODE_FROM:
1575 | default:
1576 | modeDesc = host_->tr("pauses on every hit from %1 onwards").arg(hit_count_target);
1577 | break;
1578 | }
1579 | tooltipLines.append(
1580 | host_->tr("Hit Count: %1 / %2 (%3)").arg(hit_count).arg(hit_count_target).arg(modeDesc));
1581 | }
1582 | else if (hit_count > 0)
1583 | {
1584 | tooltipLines.append(host_->tr("Hits: %1").arg(hit_count));
1585 | }
1586 | if (hasLog)
1587 | {
1588 | tooltipLines.append(host_->tr("Log: %1").arg(logMessage));
1589 | tooltipLines.append(log_also_pause ? host_->tr("(logpoint — also pauses)")
1590 | : host_->tr("(logpoint — does not pause)"));
1591 | }
1592 | if (condition_error)
1593 | {
1594 | tooltipLines.append(host_->tr("Condition error on last evaluation — treated as "
1595 | "false (silent). Edit or reset the breakpoint to "
1596 | "clear."));
1597 | /* Surface the actual Lua error string so users don't have
1598 | * to guess which identifier was nil. The C-side getter
1599 | * returns a freshly allocated copy under the breakpoints
1600 | * mutex, so reading it here is safe even when the line
1601 | * hook is racing to overwrite the field. */
1602 | char *err_msg = wslua_debugger_get_breakpoint_condition_error_message(i);
1603 | if (err_msg && err_msg[0])
1604 | {
1605 | tooltipLines.append(host_->tr("Condition error: %1").arg(QString::fromUtf8(err_msg)));
1606 | }
1607 | g_free(err_msg);
1608 | }
1609 |
1610 | /* Cell icons render with @c QIcon::Selected mode when the row
1611 | * is selected; theme icons (QIcon::fromTheme) usually don't
1612 | * ship that mode, so a dark glyph against the dark blue
1613 | * selection background reads as an invisible blob in dark
1614 | * mode. luaDbgMakeSelectionAwareIcon synthesises the Selected
1615 | * pixmap from the tree's palette (HighlightedText) so every
1616 | * row indicator stays legible while the row is highlighted. */
1617 | const QPalette bpPalette = tree_->palette();
1618 |
1619 | if (!fileExists)
1620 | {
1621 | /* Mark stale breakpoints with warning icon and gray text.
1622 | * The "file not found" indicator stays on the Location cell
1623 | * because it describes the *file*, not the breakpoint's
1624 | * extras (condition / hit count / log message). */
1625 | locationItem->setIcon(luaDbgMakeSelectionAwareIcon(QIcon::fromTheme("dialog-warning"), bpPalette));
1626 | tooltipLines.prepend(host_->tr("File not found: %1").arg(normalizedPath));
1627 | activeItem->setForeground(QBrush(Qt::gray));
1628 | lineItem->setForeground(QBrush(Qt::gray));
1629 | locationItem->setForeground(QBrush(Qt::gray));
1630 | /* Disable the checkbox + inline editor for stale breakpoints */
1631 | activeItem->setFlags(activeItem->flags() & ~Qt::ItemIsUserCheckable);
1632 | activeItem->setCheckState(Qt::Unchecked);
1633 | locationItem->setFlags(locationItem->flags() & ~Qt::ItemIsEditable);
1634 | }
1635 | else
1636 | {
1637 | /* Extras indicator on the Active column, drawn after the
1638 | * checkbox (Qt's standard cell layout: check indicator,
1639 | * decoration, then text). Mirrors the gutter dot's white
1640 | * core so users get a consistent at-a-glance cue both in
1641 | * the editor margin and in the Breakpoints list.
1642 | *
1643 | * Indicator priority: condition error > logpoint >
1644 | * conditional / hit count > plain. */
1645 | if (condition_error)
1646 | {
1647 | activeItem->setIcon(luaDbgMakeSelectionAwareIcon(QIcon::fromTheme("dialog-warning"), bpPalette));
1648 | }
1649 | else if (hasLog || hasCondition || hasHitTarget)
1650 | {
1651 | /* Painted glyph from kLuaDbgRowLog or kLuaDbgRowExtras,
1652 | * drawn in QPalette::Text and routed through
1653 | * luaDbgMakeSelectionAwareIcon so the glyph stays legible
1654 | * on the highlighted-row background. */
1655 | const QSize iconSz = tree_->iconSize();
1656 | const int side = (iconSz.isValid() && iconSz.height() > 0) ? iconSz.height() : 16;
1657 | const QColor pen = bpPalette.color(QPalette::Text);
1658 | const QString &glyph = hasLog ? kLuaDbgRowLog : kLuaDbgRowExtras;
1659 | QIcon icon = luaDbgPaintedGlyphIcon(glyph, side, host_->devicePixelRatioF(), host_->font(), pen,
1660 | /*margin=*/2);
1661 | activeItem->setIcon(luaDbgMakeSelectionAwareIcon(icon, bpPalette));
1662 | }
1663 | }
1664 |
1665 | const QString tooltipText = tooltipLines.join(QChar('\n'));
1666 | activeItem->setToolTip(tooltipText);
1667 | lineItem->setToolTip(tooltipText);
1668 | locationItem->setToolTip(tooltipText);
1669 |
1670 | if (active && fileExists)
1671 | {
1672 | hasActiveBreakpoint = true;
1673 | }
1674 |
1675 | model_->appendRow({activeItem, lineItem, locationItem});
1676 |
1677 | /* Highlight the breakpoint row that matches the current pause
1678 | * location with the same bold-accent (and one-shot background
1679 | * flash on pause entry) treatment the Watch / Variables trees
1680 | * use, so the row that "fired" stands out at a glance. The
1681 | * matching gate is the file + line pair captured in
1682 | * handlePause(); both are cleared in clearPausedStateUi(), so
1683 | * this branch is dormant whenever the debugger is not paused.
1684 | *
1685 | * applyChangedVisuals must run after appendRow so the cells
1686 | * have a concrete model index — scheduleFlashClear() captures
1687 | * a QPersistentModelIndex on each cell to drive its timed
1688 | * clear, and that index is only valid once the row is in the
1689 | * model. */
1690 | if (host_->isDebuggerPaused() && fileExists && !host_->pausedFile().isEmpty() &&
1691 | normalizedPath == host_->pausedFile() && line == host_->pausedLine())
1692 | {
1693 | host_->applyChangedVisuals(activeItem, /*changed=*/true);
1694 | }
1695 |
1696 | if (fileExists)
1697 | {
1698 | host_->filesController().ensureEntry(normalizedPath);
1699 | }
1700 |
1701 | if (collectInitialFiles && fileExists && !seenInitialFiles.contains(normalizedPath))
1702 | {
1703 | initialBreakpointFiles.append(normalizedPath);
1704 | seenInitialFiles.insert(normalizedPath);
1705 | }
1706 | }
1707 |
1708 | if (hasActiveBreakpoint)
1709 | {
1710 | host_->ensureDebuggerEnabledForActiveBreakpoints();
1711 | }
1712 | host_->refreshDebuggerStateUi();
1713 |
1714 | if (collectInitialFiles)
1715 | {
1716 | tabsPrimed_ = true;
1717 | host_->codeTabsController().openInitialBreakpointFiles(initialBreakpointFiles);
1718 | }
1719 |
1720 | updateHeaderButtonState();
1721 | suppressItemChanged_ = prevSuppress;
1722 | }
1723 |
1724 | void LuaDebuggerBreakpointsController::onItemChanged(QStandardItem *item)
1725 | {
1726 | if (!item)
1727 | {
1728 | return;
1729 | }
1730 | /* Re-entrancy guard: refreshFromEngine() rebuilds the model and writes
1731 | * many roles via setData; without this gate every set during the
1732 | * rebuild would loop back through wslua_debugger_set_breakpoint_*. */
1733 | if (suppressItemChanged_)
1734 | {
1735 | return;
1736 | }
1737 | if (item->column() != BreakpointColumn::Active)
1738 | {
1739 | return;
1740 | }
1741 | const QString file = item->data(BreakpointFileRole).toString();
1742 | const int64_t lineNumber = item->data(BreakpointLineRole).toLongLong();
1743 | const bool active = item->checkState() == Qt::Checked;
1744 | wslua_debugger_set_breakpoint_active(file.toUtf8().constData(), lineNumber, active);
1745 | /* Activating or deactivating a breakpoint must never change the
1746 | * debugger's enabled state. This is especially important during a live
1747 | * capture, where debugging is suppressed and any flip (direct or
1748 | * deferred via LuaDebuggerCaptureSuppression's prev-enabled snapshot)
1749 | * would silently re-enable the debugger when the capture ends. Just
1750 | * refresh the UI to mirror the (unchanged) core state. */
1751 | host_->refreshDebuggerStateUi();
1752 |
1753 | refreshOpenTabMarkers({file});
1754 |
1755 | /* The Breakpoints table is the only mutation path that does not flow
1756 | * through refreshFromEngine(); refresh the section-header dot here so
1757 | * its color mirrors the new aggregate active state. */
1758 | updateHeaderButtonState();
1759 | }
1760 |
1761 | void LuaDebuggerBreakpointsController::onModelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight,
1762 | const QVector<int> &roles)
1763 | {
1764 | if (suppressItemChanged_ || !model_)
1765 | {
1766 | return;
1767 | }
1768 | /* The delegate writes BreakpointConditionRole / BreakpointHitTargetRole
1769 | * / BreakpointLogMessageRole on column 0 of the touched row. Translate
1770 | * those changes into the matching wslua_debugger_set_breakpoint_*
1771 | * calls and refresh the row visuals. We dispatch on `roles` so this
1772 | * slot ignores the ordinary display / decoration churn that
1773 | * refreshFromEngine itself emits. */
1774 | const bool wantsCondition = roles.isEmpty() || roles.contains(BreakpointConditionRole);
1775 | const bool wantsTarget = roles.isEmpty() || roles.contains(BreakpointHitTargetRole);
1776 | const bool wantsLog = roles.isEmpty() || roles.contains(BreakpointLogMessageRole);
1777 | const bool wantsHitMode = roles.isEmpty() || roles.contains(BreakpointHitModeRole);
1778 | const bool wantsLogAlsoPause = roles.isEmpty() || roles.contains(BreakpointLogAlsoPauseRole);
1779 | if (!wantsCondition && !wantsTarget && !wantsLog && !wantsHitMode && !wantsLogAlsoPause)
1780 | {
1781 | return;
1782 | }
1783 |
1784 | bool touched = false;
1785 | for (int row = topLeft.row(); row <= bottomRight.row(); ++row)
1786 | {
1787 | QStandardItem *activeItem = model_->item(row, BreakpointColumn::Active);
1788 | if (!activeItem)
1789 | continue;
1790 | const QString file = activeItem->data(BreakpointFileRole).toString();
1791 | const int64_t line = activeItem->data(BreakpointLineRole).toLongLong();
1792 | if (file.isEmpty() || line <= 0)
1793 | continue;
1794 | const QByteArray fileUtf8 = file.toUtf8();
1795 |
1796 | if (wantsCondition)
1797 | {
1798 | const QString cond = activeItem->data(BreakpointConditionRole).toString();
1799 | const QByteArray condUtf8 = cond.toUtf8();
1800 | wslua_debugger_set_breakpoint_condition(fileUtf8.constData(), line,
1801 | cond.isEmpty() ? NULL__null : condUtf8.constData());
1802 | /* Parse-time validation. The runtime evaluator treats
1803 | * any error in the condition as silent-false, so without
1804 | * this check a typo (e.g. unbalanced parens, or a
1805 | * missing @c return inside a statement) would only
1806 | * surface as a row icon after the line is hit. Running
1807 | * the parse-only checker at commit time stamps the row
1808 | * with the condition_error flag/message immediately; on
1809 | * a successful parse the flag we just cleared via
1810 | * set_breakpoint_condition stays cleared. */
1811 | if (!cond.isEmpty())
1812 | {
1813 | char *parse_err = NULL__null;
1814 | const bool parses_ok = wslua_debugger_check_condition_syntax(condUtf8.constData(), &parse_err);
1815 | if (!parses_ok)
1816 | {
1817 | wslua_debugger_set_breakpoint_condition_error(fileUtf8.constData(), line,
1818 | parse_err ? parse_err : "Parse error");
1819 | }
1820 | g_free(parse_err);
1821 | }
1822 | touched = true;
1823 | }
1824 | if (wantsTarget)
1825 | {
1826 | const qlonglong target = activeItem->data(BreakpointHitTargetRole).toLongLong();
1827 | wslua_debugger_set_breakpoint_hit_count_target(fileUtf8.constData(), line, static_cast<int64_t>(target));
1828 | touched = true;
1829 | }
1830 | if (wantsHitMode)
1831 | {
1832 | /* The mode role is meaningful only when target > 0, but
1833 | * we forward it regardless so toggling the integer back
1834 | * on later remembers the last mode the user picked. The
1835 | * core ignores the mode when target == 0. */
1836 | const int hitMode = activeItem->data(BreakpointHitModeRole).toInt();
1837 | wslua_debugger_set_breakpoint_hit_count_mode(fileUtf8.constData(), line,
1838 | static_cast<wslua_hit_count_mode_t>(hitMode));
1839 | touched = true;
1840 | }
1841 | if (wantsLog)
1842 | {
1843 | const QString msg = activeItem->data(BreakpointLogMessageRole).toString();
1844 | wslua_debugger_set_breakpoint_log_message(fileUtf8.constData(), line,
1845 | msg.isEmpty() ? NULL__null : msg.toUtf8().constData());
1846 | touched = true;
1847 | }
1848 | if (wantsLogAlsoPause)
1849 | {
1850 | const bool alsoPause = activeItem->data(BreakpointLogAlsoPauseRole).toBool();
1851 | wslua_debugger_set_breakpoint_log_also_pause(fileUtf8.constData(), line, alsoPause);
1852 | touched = true;
1853 | }
1854 | }
1855 |
1856 | if (touched)
1857 | {
1858 | /* Rebuild rows so the tooltip and Location-cell indicator reflect
1859 | * the updated condition / hit target / log message. Deferred to
1860 | * the next event-loop tick on purpose: we are still inside the
1861 | * model's dataChanged emit, immediately followed by an
1862 | * itemChanged emit on the same item; tearing down every row
1863 | * synchronously here would dangle the QStandardItem pointer
1864 | * delivered to onItemChanged and would also leave the inline
1865 | * editor pointing at a destroyed model index, which can
1866 | * silently swallow the just-committed edit (the source of the
1867 | * "condition / hit count are sticky" symptom). The
1868 | * suppressItemChanged_ guard inside refreshFromEngine still
1869 | * prevents this path from looping back into either slot. */
1870 | QPointer<LuaDebuggerBreakpointsController> self(this);
1871 | QTimer::singleShot(0, this,
1872 | [self]()
1873 | {
1874 | if (self)
1875 | {
1876 | self->refreshFromEngine();
1877 | }
1878 | });
1879 | }
1880 | }
1881 |
1882 | void LuaDebuggerBreakpointsController::showGutterMenu(const QString &filename, qint32 line, const QPoint &globalPos)
1883 | {
1884 | /* Re-check the breakpoint state at popup time rather than trusting
1885 | * what the gutter saw on the click. The model is the source of
1886 | * truth and the C-side state may have changed between the click
1887 | * and the queued slot dispatch (e.g. a hit-count target just got
1888 | * met from another script line, or another reload-driven refresh
1889 | * landed in the queue first). If the breakpoint has gone away,
1890 | * silently skip — there's nothing meaningful to offer. */
1891 | const QByteArray filePathUtf8 = filename.toUtf8();
1892 | const int32_t state = wslua_debugger_get_breakpoint_state(filePathUtf8.constData(), line);
1893 | if (state == -1)
1894 | {
1895 | return;
1896 | }
1897 | const bool currentlyActive = (state == 1);
1898 |
1899 | QMenu menu(host_);
1900 | QAction *editAct = menu.addAction(host_->tr("&Edit..."));
1901 | QAction *toggleAct = menu.addAction(currentlyActive ? host_->tr("&Disable") : host_->tr("&Enable"));
1902 | menu.addSeparator();
1903 | QAction *removeAct = menu.addAction(host_->tr("&Remove"));
1904 |
1905 | /* exec() returns the chosen action, or nullptr if the user
1906 | * dismissed the menu (Escape, click outside, focus loss). The
1907 | * dismiss path is a no-op by design — the user-typed condition /
1908 | * hit-count target / log message stays exactly as it was. */
1909 | QAction *chosen = menu.exec(globalPos);
1910 | if (!chosen)
1911 | {
1912 | return;
1913 | }
1914 |
1915 | if (chosen == editAct)
1916 | {
1917 | /* Find the row that matches this (file, line) pair so the
1918 | * Location-cell delegate can open in place. Compare against
1919 | * the *normalized* path stored under BreakpointFileRole — the
1920 | * gutter may have handed us a non-canonical filename. */
1921 | if (!model_)
1922 | {
1923 | return;
1924 | }
1925 | const QString normalized = host_->normalizedFilePath(filename);
1926 | int targetRow = -1;
1927 | for (int row = 0; row < model_->rowCount(); ++row)
1928 | {
1929 | QStandardItem *activeItem = model_->item(row, BreakpointColumn::Active);
1930 | if (!activeItem)
1931 | continue;
1932 | const int64_t rowLine = activeItem->data(BreakpointLineRole).toLongLong();
1933 | if (rowLine != line)
1934 | continue;
1935 | const QString rowFile = activeItem->data(BreakpointFileRole).toString();
1936 | if (rowFile == normalized)
1937 | {
1938 | targetRow = row;
1939 | break;
1940 | }
1941 | }
1942 | if (targetRow >= 0)
1943 | {
1944 | startInlineEdit(targetRow);
1945 | }
1946 | return;
1947 | }
1948 |
1949 | if (chosen == toggleAct)
1950 | {
1951 | setActiveFromUser(filename, line, !currentlyActive);
1952 | }
1953 | else if (chosen == removeAct)
1954 | {
1955 | removeAtLine(filename, line);
1956 | }
1957 | }
1958 |
1959 | bool LuaDebuggerBreakpointsController::removeRows(const QList<int> &rows)
1960 | {
1961 | if (!model_ || rows.isEmpty())
1962 | {
1963 | return false;
1964 | }
1965 |
1966 | /* Collect (file, line) pairs for the requested rows before touching the
1967 | * model: rebuilding the model in refreshFromEngine() would invalidate
1968 | * any QStandardItem pointers we held. De-duplicate row indices so callers
1969 | * can pass selectionModel()->selectedIndexes() directly. */
1970 | QVector<QPair<QString, int64_t>> toRemove;
1971 | QSet<int> seenRows;
1972 | for (int row : rows)
1973 | {
1974 | if (row < 0 || seenRows.contains(row))
1975 | {
1976 | continue;
1977 | }
1978 | seenRows.insert(row);
1979 | QStandardItem *const activeItem = model_->item(row, BreakpointColumn::Active);
1980 | if (!activeItem)
1981 | {
1982 | continue;
1983 | }
1984 | toRemove.append(
1985 | {activeItem->data(BreakpointFileRole).toString(), activeItem->data(BreakpointLineRole).toLongLong()});
1986 | }
1987 | if (toRemove.isEmpty())
1988 | {
1989 | return false;
1990 | }
1991 |
1992 | QSet<QString> touchedFiles;
1993 | for (const auto &bp : toRemove)
1994 | {
1995 | wslua_debugger_remove_breakpoint(bp.first.toUtf8().constData(), bp.second);
1996 | touchedFiles.insert(bp.first);
1997 | }
1998 | refreshFromEngine();
1999 | refreshOpenTabMarkers(touchedFiles);
2000 | return true;
2001 | }
2002 |
2003 | bool LuaDebuggerBreakpointsController::removeSelected()
2004 | {
2005 | if (!tree_)
2006 | {
2007 | return false;
2008 | }
2009 | QItemSelectionModel *const sm = tree_->selectionModel();
2010 | if (!sm)
2011 | {
2012 | return false;
2013 | }
2014 | QList<int> rows;
2015 | for (const QModelIndex &ix : sm->selectedIndexes())
2016 | {
2017 | if (ix.isValid())
2018 | {
2019 | rows.append(ix.row());
2020 | }
2021 | }
2022 | return removeRows(rows);
2023 | }
2024 |
2025 | void LuaDebuggerBreakpointsController::toggleAllActive()
2026 | {
2027 | const unsigned n = wslua_debugger_get_breakpoint_count();
2028 | if (n == 0U)
2029 | {
2030 | return;
2031 | }
2032 | /* Activate all only when every BP is off; if any is on (all on or mix),
2033 | * this control shows "deactivate" and turns all off. */
2034 | bool allInactive = true;
2035 | for (unsigned i = 0; i < n; ++i)
2036 | {
2037 | const char *file_path;
2038 | int64_t line;
2039 | bool active;
2040 | if (wslua_debugger_get_breakpoint(i, &file_path, &line, &active) && active)
2041 | {
2042 | allInactive = false;
2043 | break;
2044 | }
2045 | }
2046 | const bool makeActive = allInactive;
2047 | for (unsigned i = 0; i < n; ++i)
2048 | {
2049 | const char *file_path;
2050 | int64_t line;
2051 | bool active;
2052 | if (wslua_debugger_get_breakpoint(i, &file_path, &line, &active))
2053 | {
2054 | wslua_debugger_set_breakpoint_active(file_path, line, makeActive);
2055 | }
2056 | }
2057 | refreshFromEngine();
2058 | refreshAllOpenTabMarkers();
2059 | }
2060 |
2061 | void LuaDebuggerBreakpointsController::updateHeaderButtonState()
2062 | {
2063 | if (toggleAllButton_)
2064 | {
2065 | const int side = std::max(toggleAllButton_->height(), toggleAllButton_->width());
2066 | const qreal dpr = toggleAllButton_->devicePixelRatioF();
2067 | LuaDebuggerCodeView *const cv = host_->codeTabsController().currentCodeView();
2068 | const QFont *const editorFont = (cv && !cv->getFilename().isEmpty()) ? &cv->font() : nullptr;
2069 | const unsigned n = wslua_debugger_get_breakpoint_count();
2070 | bool allInactive = n > 0U;
2071 | for (unsigned i = 0; allInactive && i < n; ++i)
2072 | {
2073 | const char *file_path;
2074 | int64_t line;
2075 | bool active;
2076 | if (wslua_debugger_get_breakpoint(i, &file_path, &line, &active))
2077 | {
2078 | if (active)
2079 | {
2080 | allInactive = false;
2081 | }
2082 | }
2083 | }
2084 | LuaDbgBpHeaderIconMode mode;
2085 | const QString tglLineKeys = kLuaDbgCtxToggleBreakpoint.toString(QKeySequence::NativeText);
2086 | if (n == 0U)
2087 | {
2088 | mode = LuaDbgBpHeaderIconMode::NoBreakpoints;
2089 | toggleAllButton_->setEnabled(false);
2090 | toggleAllButton_->setToolTip(host_->tr("No breakpoints\n%1: add or remove breakpoint on the current "
2091 | "line in the editor")
2092 | .arg(tglLineKeys));
2093 | }
2094 | else if (allInactive)
2095 | {
2096 | /* All BPs off: dot is gray (mirrors gutter); click activates all. */
2097 | mode = LuaDbgBpHeaderIconMode::ActivateAll;
2098 | toggleAllButton_->setEnabled(true);
2099 | toggleAllButton_->setToolTip(host_->tr("All breakpoints are inactive — click to activate all\n"
2100 | "%1: add or remove on the current line in the editor")
2101 | .arg(tglLineKeys));
2102 | }
2103 | else
2104 | {
2105 | /* Any BP on (all-on or mix): dot is red (mirrors gutter); click
2106 | * deactivates all. */
2107 | mode = LuaDbgBpHeaderIconMode::DeactivateAll;
2108 | toggleAllButton_->setEnabled(true);
2109 | toggleAllButton_->setToolTip(host_->tr("Click to deactivate all breakpoints\n"
2110 | "%1: add or remove on the current line in the editor")
2111 | .arg(tglLineKeys));
2112 | }
2113 | /* Cache the three icons keyed by (font, side, dpr); cursor moves
2114 | * fire updateHeaderButtonState() frequently and only the mode
2115 | * actually varies on hot paths. */
2116 | const QString cacheKey = QStringLiteral("%1/%2/%3")(QString(QtPrivate::qMakeStringPrivate(u"" "%1/%2/%3")))
2117 | .arg(editorFont != nullptr ? editorFont->key() : QGuiApplication::font().key())
2118 | .arg(side)
2119 | .arg(dpr);
2120 | if (cacheKey != headerIconCacheKey_)
2121 | {
2122 | headerIconCacheKey_ = cacheKey;
2123 | for (QIcon &cached : headerIconCache_)
2124 | {
2125 | cached = QIcon();
2126 | }
2127 | }
2128 | const int modeIdx = static_cast<int>(mode);
2129 | if (headerIconCache_[modeIdx].isNull())
2130 | {
2131 | headerIconCache_[modeIdx] = luaDbgBreakpointHeaderIconForMode(editorFont, mode, side, dpr);
2132 | }
2133 | toggleAllButton_->setIcon(headerIconCache_[modeIdx]);
2134 | }
2135 | /* The Edit and Remove header buttons share enable state: both act on
2136 | * the breakpoint row(s) the user has selected, so a selection-only
2137 | * gate keeps them visually and behaviourally in lockstep. Edit only
2138 | * ever opens one editor (the current/first-selected row); the click
2139 | * handler resolves a single row internally, and startInlineEdit() is
2140 | * a no-op on stale (file-missing) rows, so we don't need to inspect
2141 | * editability here. */
2142 | QItemSelectionModel *const bpSelectionModel = tree_ ? tree_->selectionModel() : nullptr;
2143 | const bool hasBreakpointSelection = bpSelectionModel && !bpSelectionModel->selectedRows().isEmpty();
2144 | if (removeButton_)
2145 | {
2146 | removeButton_->setEnabled(hasBreakpointSelection);
2147 | }
2148 | if (editButton_)
2149 | {
2150 | editButton_->setEnabled(hasBreakpointSelection);
2151 | }
2152 | if (removeAllButton_)
2153 | {
2154 | const bool hasBreakpoints = model_ && model_->rowCount() > 0;
2155 | removeAllButton_->setEnabled(hasBreakpoints);
2156 | if (removeAllAction_)
2157 | {
2158 | removeAllAction_->setEnabled(hasBreakpoints);
2159 | }
2160 | }
2161 | }
2162 |
2163 | void LuaDebuggerBreakpointsController::toggleAtLine(const QString &file, qint32 line)
2164 | {
2165 | if (file.isEmpty() || line < 1)
2166 | {
2167 | return;
2168 | }
2169 | const QByteArray fileUtf8 = file.toUtf8();
2170 | const int32_t state = wslua_debugger_get_breakpoint_state(fileUtf8.constData(), line);
2171 | if (state == -1)
2172 | {
2173 | wslua_debugger_add_breakpoint(fileUtf8.constData(), line);
2174 | host_->ensureDebuggerEnabledForActiveBreakpoints();
2175 | }
2176 | else
2177 | {
2178 | wslua_debugger_remove_breakpoint(fileUtf8.constData(), line);
2179 | host_->refreshDebuggerStateUi();
2180 | }
2181 | refreshFromEngine();
2182 | refreshAllOpenTabMarkers();
2183 | }
2184 |
2185 | void LuaDebuggerBreakpointsController::toggleOnCodeViewLine(LuaDebuggerCodeView *codeView, qint32 line)
2186 | {
2187 | if (!codeView)
2188 | {
2189 | return;
2190 | }
2191 | toggleAtLine(codeView->getFilename(), line);
2192 | }
2193 |
2194 | void LuaDebuggerBreakpointsController::shiftToggleAtLine(const QString &file, qint32 line)
2195 | {
2196 | if (file.isEmpty() || line < 1)
2197 | {
2198 | return;
2199 | }
2200 | const QByteArray fileUtf8 = file.toUtf8();
2201 | const int32_t state = wslua_debugger_get_breakpoint_state(fileUtf8.constData(), line);
2202 | if (state == -1)
2203 | {
2204 | /* Create the row pre-armed: keep the line-hook cost off the
2205 | * caller's fast path until the user explicitly activates it. The
2206 | * core's add+set-active sequence is the same one the JSON restore
2207 | * path uses for a row that was saved inactive. */
2208 | wslua_debugger_add_breakpoint(fileUtf8.constData(), line);
2209 | wslua_debugger_set_breakpoint_active(fileUtf8.constData(), line, false);
2210 | }
2211 | else
2212 | {
2213 | /* Existing row: flip active. Do NOT call ensureDebuggerEnabledForActiveBreakpoints —
2214 | * @c Shift+click is the "no surprise" companion to F9; it should
2215 | * never silently turn the debugger back on. The matching gutter
2216 | * Enable/Disable menu (see @ref setActiveFromUser) does enable
2217 | * because that gesture is "I want this active right now". */
2218 | wslua_debugger_set_breakpoint_active(fileUtf8.constData(), line, state == 0);
2219 | }
2220 | refreshFromEngine();
2221 | refreshAllOpenTabMarkers();
2222 | }
2223 |
2224 | void LuaDebuggerBreakpointsController::setActiveFromUser(const QString &file, qint32 line, bool active)
2225 | {
2226 | if (file.isEmpty() || line < 1)
2227 | {
2228 | return;
2229 | }
2230 | const QByteArray fileUtf8 = file.toUtf8();
2231 | wslua_debugger_set_breakpoint_active(fileUtf8.constData(), line, active);
2232 | if (active)
2233 | {
2234 | host_->ensureDebuggerEnabledForActiveBreakpoints();
2235 | }
2236 | refreshFromEngine();
2237 | refreshAllOpenTabMarkers();
2238 | }
2239 |
2240 | void LuaDebuggerBreakpointsController::removeAtLine(const QString &file, qint32 line)
2241 | {
2242 | if (file.isEmpty() || line < 1)
2243 | {
2244 | return;
2245 | }
2246 | const QByteArray fileUtf8 = file.toUtf8();
2247 | wslua_debugger_remove_breakpoint(fileUtf8.constData(), line);
2248 | host_->refreshDebuggerStateUi();
2249 | refreshFromEngine();
2250 | refreshAllOpenTabMarkers();
2251 | }
2252 |
2253 | void LuaDebuggerBreakpointsController::refreshAllOpenTabMarkers() const
2254 | {
2255 | if (!host_)
2256 | {
2257 | return;
2258 | }
2259 | QTabWidget *const tabs = host_->codeTabsController().tabs();
2260 | if (!tabs)
2261 | {
2262 | return;
2263 | }
2264 | const qint32 tabCount = static_cast<qint32>(tabs->count());
2265 | for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
2266 | {
2267 | LuaDebuggerCodeView *tabView = qobject_cast<LuaDebuggerCodeView *>(tabs->widget(static_cast<int>(tabIndex)));
2268 | if (tabView)
2269 | {
2270 | tabView->updateBreakpointMarkers();
2271 | }
2272 | }
2273 | }
2274 |
2275 | void LuaDebuggerBreakpointsController::refreshOpenTabMarkers(const QSet<QString> &files) const
2276 | {
2277 | if (files.isEmpty() || !host_)
2278 | {
2279 | return;
2280 | }
2281 | QTabWidget *const tabs = host_->codeTabsController().tabs();
2282 | if (!tabs)
2283 | {
2284 | return;
2285 | }
2286 | const qint32 tabCount = static_cast<qint32>(tabs->count());
2287 | for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
2288 | {
2289 | LuaDebuggerCodeView *tabView = qobject_cast<LuaDebuggerCodeView *>(tabs->widget(static_cast<int>(tabIndex)));
2290 | if (tabView && files.contains(tabView->getFilename()))
2291 | {
2292 | tabView->updateBreakpointMarkers();
2293 | }
2294 | }
2295 | }
2296 |
2297 | void LuaDebuggerBreakpointsController::serializeTo(QVariantMap &settingsMap) const
2298 | {
2299 | QVariantList list;
2300 | const unsigned count = wslua_debugger_get_breakpoint_count();
2301 | for (unsigned i = 0; i < count; i++)
2302 | {
2303 | const char *file = nullptr;
2304 | int64_t line = 0;
2305 | bool active = false;
2306 | const char *condition = nullptr;
2307 | int64_t hit_target = 0;
2308 | int64_t hit_count = 0; /* runtime-only; not persisted */
2309 | bool cond_err = false; /* runtime-only; not persisted */
2310 | const char *log_message = nullptr;
2311 | wslua_hit_count_mode_t hit_mode = WSLUA_HIT_COUNT_MODE_FROM;
2312 | bool log_also_pause = false;
2313 | if (!wslua_debugger_get_breakpoint_extended(i, &file, &line, &active, &condition, &hit_target, &hit_count,
2314 | &cond_err, &log_message, &hit_mode, &log_also_pause))
2315 | {
2316 | continue;
2317 | }
2318 | QJsonObject bp;
2319 | bp[QStringLiteral("file")(QString(QtPrivate::qMakeStringPrivate(u"" "file")))] = QString::fromUtf8(file);
2320 | bp[QStringLiteral("line")(QString(QtPrivate::qMakeStringPrivate(u"" "line")))] = static_cast<qint64>(line);
2321 | bp[QStringLiteral("active")(QString(QtPrivate::qMakeStringPrivate(u"" "active")))] = active;
2322 | bp[QStringLiteral("condition")(QString(QtPrivate::qMakeStringPrivate(u"" "condition")))] = QString::fromUtf8(condition ? condition : "");
2323 | bp[QStringLiteral("hitCountTarget")(QString(QtPrivate::qMakeStringPrivate(u"" "hitCountTarget")) )] = static_cast<qint64>(hit_target);
2324 | /* @c hitCountMode is persisted as a string ("from" / "every" /
2325 | * "once") so the JSON file is self-describing and matches the
2326 | * UI dropdown verbatim. */
2327 | const char *modeStr = "from";
| Value stored to 'modeStr' during its initialization is never read
2328 | switch (hit_mode)
2329 | {
2330 | case WSLUA_HIT_COUNT_MODE_EVERY:
2331 | modeStr = "every";
2332 | break;
2333 | case WSLUA_HIT_COUNT_MODE_ONCE:
2334 | modeStr = "once";
2335 | break;
2336 | case WSLUA_HIT_COUNT_MODE_FROM:
2337 | default:
2338 | modeStr = "from";
2339 | break;
2340 | }
2341 | bp[QStringLiteral("hitCountMode")(QString(QtPrivate::qMakeStringPrivate(u"" "hitCountMode")))] = QString::fromLatin1(modeStr);
2342 | bp[QStringLiteral("logMessage")(QString(QtPrivate::qMakeStringPrivate(u"" "logMessage")))] = QString::fromUtf8(log_message ? log_message : "");
2343 | bp[QStringLiteral("logAlsoPause")(QString(QtPrivate::qMakeStringPrivate(u"" "logAlsoPause")))] = log_also_pause;
2344 | list.append(bp.toVariantMap());
2345 | }
2346 | settingsMap[LuaDebuggerSettingsKeys::Breakpoints] = list;
2347 | }
2348 |
2349 | void LuaDebuggerBreakpointsController::restoreFrom(const QVariantMap &settingsMap)
2350 | {
2351 | QJsonArray breakpointsArray = LuaDebuggerSettingsStore::jsonArrayAt(settingsMap, LuaDebuggerSettingsKeys::Breakpoints);
2352 | for (const QJsonValue &val : breakpointsArray)
2353 | {
2354 | QJsonObject bp = val.toObject();
2355 | const QString file = bp.value("file").toString();
2356 | const int64_t line = bp.value("line").toVariant().toLongLong();
2357 | if (file.isEmpty() || line <= 0)
2358 | {
2359 | continue;
2360 | }
2361 | const bool active = bp.value("active").toBool(true);
2362 | const QString condition = bp.value("condition").toString();
2363 | const int64_t hitCountTarget = bp.value("hitCountTarget").toVariant().toLongLong();
2364 | const QString modeStr = bp.value("hitCountMode").toString().toLower();
2365 | wslua_hit_count_mode_t hitCountMode = WSLUA_HIT_COUNT_MODE_FROM;
2366 | if (modeStr == QStringLiteral("every")(QString(QtPrivate::qMakeStringPrivate(u"" "every"))))
2367 | {
2368 | hitCountMode = WSLUA_HIT_COUNT_MODE_EVERY;
2369 | }
2370 | else if (modeStr == QStringLiteral("once")(QString(QtPrivate::qMakeStringPrivate(u"" "once"))))
2371 | {
2372 | hitCountMode = WSLUA_HIT_COUNT_MODE_ONCE;
2373 | }
2374 | const QString logMessage = bp.value("logMessage").toString();
2375 | const bool logAlsoPause = bp.value("logAlsoPause").toBool(false);
2376 |
2377 | const QByteArray fb = file.toUtf8();
2378 | if (wslua_debugger_get_breakpoint_state(fb.constData(), line) < 0)
2379 | {
2380 | wslua_debugger_add_breakpoint(fb.constData(), line);
2381 | }
2382 | wslua_debugger_set_breakpoint_active(fb.constData(), line, active);
2383 | const QByteArray cb = condition.toUtf8();
2384 | wslua_debugger_set_breakpoint_condition(fb.constData(), line, condition.isEmpty() ? NULL__null : cb.constData());
2385 | wslua_debugger_set_breakpoint_hit_count_target(fb.constData(), line, hitCountTarget);
2386 | wslua_debugger_set_breakpoint_hit_count_mode(fb.constData(), line, hitCountMode);
2387 | const QByteArray mb = logMessage.toUtf8();
2388 | wslua_debugger_set_breakpoint_log_message(fb.constData(), line, logMessage.isEmpty() ? NULL__null : mb.constData());
2389 | wslua_debugger_set_breakpoint_log_also_pause(fb.constData(), line, logAlsoPause);
2390 | }
2391 | }
2392 |
2393 | /* ===== dialog_breakpoints (LuaDebuggerDialog members) ===== */
2394 |
2395 | CollapsibleSection *LuaDebuggerDialog::createBreakpointsSection(QWidget *parent)
2396 | {
2397 | breakpointsSection = new CollapsibleSection(tr("Breakpoints"), parent);
2398 | breakpointsSection->setToolTip(tr("<p><b>Expression</b><br/>"
2399 | "Pause only when this Lua expression is truthy in the "
2400 | "current frame. Runtime errors count as false and surface a "
2401 | "warning icon on the row.</p>"
2402 | "<p><b>Hit Count</b><br/>"
2403 | "Gate the pause on a hit counter. "
2404 | "The dropdown next to <i>N</i> picks the "
2405 | "comparison mode: <code>from</code> pauses on every hit "
2406 | "from <i>N</i> onwards (default); <code>every</code> "
2407 | "pauses on hits <i>N</i>, 2&times;<i>N</i>, "
2408 | "3&times;<i>N</i>, &hellip;; <code>once</code> pauses on "
2409 | "the <i>N</i>-th hit and deactivates the breakpoint. The "
2410 | "counter is preserved "
2411 | "across edits; right-click the row to reset it.</p>"
2412 | "<p><b>Log Message</b><br/>"
2413 | "Write a line to the <i>Evaluate</i> output (and "
2414 | "Wireshark's debug log) each time the breakpoint fires &mdash; "
2415 | "after the <i>Hit Count</i> gate and any <i>Expression</i> "
2416 | "allow it. By default execution continues; click the pause "
2417 | "toggle on the editor row to also pause after emitting. "
2418 | "Tags: <code>{expr}</code> (any Lua value); "
2419 | "<code>{filename}</code>, <code>{basename}</code>, "
2420 | "<code>{line}</code>, <code>{function}</code>, "
2421 | "<code>{what}</code>; <code>{hits}</code>, "
2422 | "<code>{depth}</code>, <code>{thread}</code>; "
2423 | "<code>{timestamp}</code>, <code>{datetime}</code>, "
2424 | "<code>{epoch}</code>, <code>{epoch_ms}</code>, "
2425 | "<code>{elapsed}</code>, <code>{delta}</code>; "
2426 | "<code>{{</code> / <code>}}</code> for literal braces.</p>"
2427 | "<p>Edit the <i>Location</i> cell (double-click, F2, or "
2428 | "right-click &rarr; Edit) to attach one of these. A white "
2429 | "core inside the breakpoint dot &mdash; in this list and in "
2430 | "the gutter &mdash; marks rows that carry extras.</p>"));
2431 | breakpointsModel = new QStandardItemModel(this);
2432 | breakpointsModel->setColumnCount(BreakpointColumn::Count);
2433 | breakpointsModel->setHorizontalHeaderLabels({tr("Active"), tr("Line"), tr("File")});
2434 | breakpointsTree = new QTreeView();
2435 | breakpointsTree->setModel(breakpointsModel);
2436 | /* Inline edit on the Location column (delegate-driven mode picker for
2437 | * Condition / Hit Count / Log Message). DoubleClicked is the default
2438 | * trigger; the slot in onBreakpointItemDoubleClicked redirects double-
2439 | * click on any row cell to the editable column so the editor opens
2440 | * even when the user clicked the Active checkbox or the hidden Line
2441 | * column. EditKeyPressed enables F2 to open the editor with keyboard. */
2442 | breakpointsTree->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed |
2443 | QAbstractItemView::SelectedClicked);
2444 | breakpointsTree->setItemDelegateForColumn(BreakpointColumn::Location,
2445 | new LuaDbgBreakpointConditionDelegate(this));
2446 | breakpointsTree->setRootIsDecorated(false);
2447 | breakpointsTree->setSelectionBehavior(QAbstractItemView::SelectRows);
2448 | breakpointsTree->setSelectionMode(QAbstractItemView::ExtendedSelection);
2449 | breakpointsTree->setAllColumnsShowFocus(true);
2450 | breakpointsTree->setContextMenuPolicy(Qt::CustomContextMenu);
2451 | breakpointsSection->setContentWidget(breakpointsTree);
2452 | {
2453 | const int hdrH = breakpointsSection->titleButtonHeight();
2454 | const QFont hdrTitleFont = breakpointsSection->titleButtonFont();
2455 | auto *const bpHeaderBtnRow = new QWidget(breakpointsSection);
2456 | auto *const bpHeaderBtnLayout = new QHBoxLayout(bpHeaderBtnRow);
2457 | bpHeaderBtnLayout->setContentsMargins(0, 0, 0, 0);
2458 | bpHeaderBtnLayout->setSpacing(4);
2459 | bpHeaderBtnLayout->setAlignment(Qt::AlignVCenter);
2460 | QToolButton *const bpTglBtn = new QToolButton(bpHeaderBtnRow);
2461 | breakpointHeaderToggleButton_ = bpTglBtn;
2462 | styleLuaDebuggerHeaderBreakpointToggleButton(bpTglBtn, hdrH);
2463 | bpTglBtn->setIcon(luaDbgBreakpointHeaderIconForMode(nullptr, LuaDbgBpHeaderIconMode::NoBreakpoints, hdrH,
2464 | bpTglBtn->devicePixelRatioF()));
2465 | bpTglBtn->setAutoRaise(true);
2466 | bpTglBtn->setStyleSheet(kLuaDbgHeaderToolButtonStyle);
2467 | bpTglBtn->setEnabled(false);
2468 | bpTglBtn->setToolTip(tr("No breakpoints"));
2469 | QToolButton *const bpEditBtn = new QToolButton(bpHeaderBtnRow);
2470 | breakpointHeaderEditButton_ = bpEditBtn;
2471 | /* Painted via luaDbgPaintedGlyphButtonIcon so the disabled
2472 | * pixmap is baked from the palette's disabled-text gray instead
2473 | * of QStyle::generatedIconPixmap()'s synthesised filter, keeping
2474 | * the disabled tone in step with the neighbouring +/- buttons. */
2475 | {
2476 | QIcon gear = luaDbgPaintedGlyphButtonIcon(kLuaDbgHeaderEdit, hdrH, devicePixelRatioF(),
2477 | hdrTitleFont, palette(), /*margin=*/2);
2478 | bpEditBtn->setIcon(gear);
2479 | }
2480 | styleLuaDebuggerHeaderIconOnlyButton(bpEditBtn, hdrH);
2481 | bpEditBtn->setAutoRaise(true);
2482 | bpEditBtn->setStyleSheet(kLuaDbgHeaderToolButtonStyle);
2483 | bpEditBtn->setEnabled(false);
2484 | bpEditBtn->setToolTip(tr("Edit Breakpoint"));
2485 | QToolButton *const bpRemBtn = new QToolButton(bpHeaderBtnRow);
2486 | breakpointHeaderRemoveButton_ = bpRemBtn;
2487 | styleLuaDebuggerHeaderPlusMinusButton(bpRemBtn, hdrH, hdrTitleFont);
2488 | bpRemBtn->setText(kLuaDbgHeaderMinus);
2489 | bpRemBtn->setAutoRaise(true);
2490 | bpRemBtn->setStyleSheet(kLuaDbgHeaderToolButtonStyle);
2491 | bpRemBtn->setEnabled(false);
2492 | bpRemBtn->setToolTip(
2493 | tr("Remove Breakpoint (%1)").arg(QKeySequence(QKeySequence::Delete).toString(QKeySequence::NativeText)));
2494 | QToolButton *const bpRemAllBtn = new QToolButton(bpHeaderBtnRow);
2495 | breakpointHeaderRemoveAllButton_ = bpRemAllBtn;
2496 | {
2497 | QIcon icon = luaDbgPaintedGlyphButtonIcon(kLuaDbgHeaderRemoveAll, hdrH, devicePixelRatioF(),
2498 | hdrTitleFont, palette(), /*margin=*/2);
2499 | bpRemAllBtn->setIcon(icon);
2500 | }
2501 | styleLuaDebuggerHeaderIconOnlyButton(bpRemAllBtn, hdrH);
2502 | bpRemAllBtn->setAutoRaise(true);
2503 | bpRemAllBtn->setStyleSheet(kLuaDbgHeaderToolButtonStyle);
2504 | bpRemAllBtn->setEnabled(false);
2505 | bpRemAllBtn->setToolTip(
2506 | tr("Remove All Breakpoints (%1)").arg(kLuaDbgCtxRemoveAllBreakpoints.toString(QKeySequence::NativeText)));
2507 | bpHeaderBtnLayout->addWidget(bpTglBtn);
2508 | bpHeaderBtnLayout->addWidget(bpRemBtn);
2509 | bpHeaderBtnLayout->addWidget(bpEditBtn);
2510 | bpHeaderBtnLayout->addWidget(bpRemAllBtn);
2511 | breakpointsSection->setHeaderTrailingWidget(bpHeaderBtnRow);
2512 | }
2513 | breakpointsSection->setExpanded(true);
2514 | return breakpointsSection;
2515 | }
2516 |
2517 | void LuaDebuggerDialog::wireBreakpointsPanel()
2518 | {
2519 | breakpointsController_.attach(breakpointsTree, breakpointsModel);
2520 |
2521 | /* "Remove All Breakpoints" needs a real, dialog-wide shortcut so
2522 | * Ctrl+Shift+F9 fires regardless of focus. Setting the keys only
2523 | * on the right-click menu action (built on demand) made the
2524 | * shortcut a label without a binding. */
2525 | actionRemoveAllBreakpoints_ = new QAction(tr("Remove All Breakpoints"), this);
2526 | actionRemoveAllBreakpoints_->setShortcut(kLuaDbgCtxRemoveAllBreakpoints);
2527 | actionRemoveAllBreakpoints_->setShortcutContext(Qt::WidgetWithChildrenShortcut);
2528 | actionRemoveAllBreakpoints_->setEnabled(false);
2529 | addAction(actionRemoveAllBreakpoints_);
2530 |
2531 | breakpointsController_.attachHeaderButtons(breakpointHeaderToggleButton_, breakpointHeaderRemoveButton_,
2532 | breakpointHeaderRemoveAllButton_, breakpointHeaderEditButton_,
2533 | actionRemoveAllBreakpoints_);
2534 | }