Bug Summary

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

Annotated Source Code

Press '?' to see keyboard shortcuts

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