### Bug Summary

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