### Bug Summary

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

### Annotated Source Code

Press '?' to see keyboard shortcuts

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

Keyboard shortcuts:

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

Close
Show only relevant lines Show control flow arrows

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