Bug Summary

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

Annotated Source Code

Press '?' to see keyboard shortcuts

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