Bug Summary

File:builds/wireshark/wireshark/ui/qt/lua_debugger/lua_debugger_code_editor.cpp
Warning:line 432, column 38
Called C++ object pointer is null

Annotated Source Code

Press '?' to see keyboard shortcuts

clang -cc1 -cc1 -triple x86_64-pc-linux-gnu -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name lua_debugger_code_editor.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-22/lib/clang/22 -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/QtConcurrent -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/QtNetwork -isystem /usr/include/x86_64-linux-gnu/qt6/QtPrintSupport -isystem /usr/include/x86_64-linux-gnu/qt6/QtGui -isystem /usr/include/x86_64-linux-gnu/qt6/QtWidgets -isystem /usr/include/x86_64-linux-gnu/qt6/QtSvg -isystem /usr/include/x86_64-linux-gnu/qt6/QtMultimedia -isystem /usr/include/x86_64-linux-gnu/qt6/QtDBus -D CARES_NO_DEPRECATED -D G_DISABLE_DEPRECATED -D G_DISABLE_SINGLE_INCLUDES -D QT_CONCURRENT_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_SVG_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/16/../../../../include/c++/16 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/x86_64-linux-gnu/c++/16 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/c++/16/backward -internal-isystem /usr/lib/llvm-22/lib/clang/22/include -internal-isystem /usr/local/include -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/16/../../../../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 -fdwarf2-cfi-asm -o /builds/wireshark/wireshark/sbout/2026-11-05-100337-3661-1 -x c++ /builds/wireshark/wireshark/ui/qt/lua_debugger/lua_debugger_code_editor.cpp
1/* lua_debugger_code_editor.cpp
2 *
3 * Wireshark - Network traffic analyzer
4 * By Gerald Combs <[email protected]>
5 * Copyright 1998 Gerald Combs
6 *
7 * SPDX-License-Identifier: GPL-2.0-or-later
8 */
9
10/**
11 * @file
12 * Code editor stack: editor view, gutter, theme palette, font policy
13 * and the script-tabs controller.
14 */
15
16#include <lua_debugger_code_editor.h>
17#include <ui_lua_debugger_dialog.h>
18
19#include <QAction>
20#include <QApplication>
21#include <QColor>
22#include <QContextMenuEvent>
23#include <QDir>
24#include <QEvent>
25#include <QFile>
26#include <QFileInfo>
27#include <QFont>
28#include <QFontDatabase>
29#include <QGuiApplication>
30#include <QHeaderView>
31#include <QKeyEvent>
32#include <QList>
33#include <QMessageBox>
34#include <QMetaObject>
35#include <QModelIndex>
36#include <QMouseEvent>
37#include <QPainter>
38#include <QPalette>
39#include <QPlainTextEdit>
40#include <QStandardItem>
41#include <QStandardItemModel>
42#include <QStandardPaths>
43#include <QStringList>
44#include <QTabWidget>
45#include <QTextBlock>
46#include <QTextDocument>
47#include <QTextStream>
48#include <QTreeView>
49#include <QWidget>
50#include <QtGlobal>
51
52#include <algorithm>
53#include <climits>
54
55#include "epan/wslua/wslua_debugger.h"
56#include "lua_debugger_breakpoints.h"
57#include "lua_debugger_dialog.h"
58#include "lua_debugger_files.h"
59#include "main_application.h"
60#include <ui/qt/utils/font_manager.h>
61#include "utils/color_utils.h"
62#include <ui/qt/utils/theme_manager.h>
63#include <ui/qt/utils/themes/contrast_adapt_icon.h>
64
65/* ===== code_palette ===== */
66
67LuaDebuggerEditorPalette luaDebuggerEditorPalette()
68{
69 const ThemeManager *theme = ThemeManager::instance();
70 LuaDebuggerEditorPalette p;
71 p.editorBackground = theme->color(ThemeManager::CodeBackground);
72 p.editorText = theme->color(ThemeManager::CodeText);
73 p.selection = theme->color(ThemeManager::CodeSelection);
74 p.selectionText = theme->color(ThemeManager::CodeSelectionText);
75 p.gutterBackground = theme->color(ThemeManager::CodeGutter);
76 p.gutterText = theme->color(ThemeManager::CodeGutterText);
77 p.pausedLine = theme->color(ThemeManager::CodePausedLine);
78 return p;
79}
80
81/* ===== code_view ===== */
82
83LuaDebuggerCodeView::LuaDebuggerCodeView(QWidget *parent)
84 : QPlainTextEdit(parent), lineNumberArea(new LineNumberArea(this)), syntaxHighlighter(nullptr)
85{
86 /* Gutter tooltip surfaces both click affordances; the Shift+click
87 * behavior is otherwise non-discoverable. */
88 lineNumberArea->setToolTip(tr("Click: add or remove breakpoint\n"
89 "Drag existing breakpoint: move to nearest free visible line\n"
90 "Shift+click: on an empty line, add a disabled breakpoint;\n"
91 "on an existing breakpoint, toggle its active state"));
92 syntaxHighlighter = new LuaSyntaxHighlighter(document());
93
94 connect(this, &LuaDebuggerCodeView::blockCountChanged, this, &LuaDebuggerCodeView::updateLineNumberAreaWidth);
95 connect(this, &LuaDebuggerCodeView::updateRequest, this, &LuaDebuggerCodeView::updateLineNumberArea);
96 connect(this, &LuaDebuggerCodeView::cursorPositionChanged, this, &LuaDebuggerCodeView::rebuildLineHighlights);
97
98 /* QAbstractScrollArea delivers key events to the viewport; Esc never
99 * reaches QDialog::keyPressEvent. Forward to LuaDebuggerDialog::handleEscapeKey(). */
100 viewport()->installEventFilter(this);
101
102 setReadOnly(false);
103 setLineWrapMode(QPlainTextEdit::NoWrap);
104
105 QFont initialFont;
106 if (mainApp && mainApp->isInitialized())
107 {
108 initialFont = FontManager::monospaceFont();
109 }
110 setEditorFont(initialFont);
111 applyEditorPalette();
112 rebuildLineHighlights();
113}
114
115QString LuaDebuggerCodeView::luaIdentifierUnderCursor(const QTextCursor &cursor) const
116{
117 const QString block = cursor.block().text();
118 const int posInBlock = cursor.positionInBlock();
119 if (block.isEmpty() || posInBlock < 0 || posInBlock > block.size())
120 {
121 return {};
122 }
123
124 auto isIdentChar = [](QChar c) { return c.isLetterOrNumber() || c == QLatin1Char('_'); };
125 auto isIdentStart = [](QChar c) { return c.isLetter() || c == QLatin1Char('_'); };
126
127 /* Caret may sit one past the end of the identifier; expand left
128 * until we are inside, then sweep both directions. */
129 int start = posInBlock;
130 int end = posInBlock;
131 if (start > 0 && !isIdentChar(block.at(start - 1)) && (start >= block.size() || !isIdentChar(block.at(start))))
132 {
133 return {};
134 }
135 while (start > 0 && isIdentChar(block.at(start - 1)))
136 {
137 --start;
138 }
139 while (end < block.size() && isIdentChar(block.at(end)))
140 {
141 ++end;
142 }
143 if (start >= end)
144 {
145 return {};
146 }
147 if (!isIdentStart(block.at(start)))
148 {
149 return {};
150 }
151 return block.mid(start, end - start);
152}
153
154QString LuaDebuggerCodeView::watchExpressionForContextMenu(const QPoint &viewportPos) const
155{
156 QString s = textCursor().selectedText().trimmed();
157 if (!s.isEmpty())
158 {
159 return s;
160 }
161 return luaIdentifierUnderCursor(cursorForPosition(viewportPos));
162}
163
164qint32 LuaDebuggerCodeView::lineNumberAreaWidth()
165{
166 qint32 digits = 1;
167 qint32 maxBlockCount = qMax(1, blockCount());
168 while (maxBlockCount >= 10)
169 {
170 maxBlockCount /= 10;
171 ++digits;
172 }
173
174 const qint32 space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
175 return space + 20; // Extra space for breakpoint icon
176}
177
178void LuaDebuggerCodeView::updateLineNumberAreaWidth(int /* newBlockCount */)
179{
180 setViewportMargins(static_cast<int>(lineNumberAreaWidth()), 0, 0, 0);
181}
182
183void LuaDebuggerCodeView::updateLineNumberArea(const QRect &rect, int dy)
184{
185 if (dy)
186 lineNumberArea->scroll(0, dy);
187 else
188 lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
189
190 if (rect.contains(viewport()->rect()))
191 updateLineNumberAreaWidth(0);
192}
193
194void LuaDebuggerCodeView::resizeEvent(QResizeEvent *e)
195{
196 QPlainTextEdit::resizeEvent(e);
197
198 QRect cr = contentsRect();
199 lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), static_cast<int>(lineNumberAreaWidth()), cr.height()));
200}
201
202bool LuaDebuggerCodeView::eventFilter(QObject *watched, QEvent *event)
203{
204 if (watched == viewport() && event->type() == QEvent::KeyPress)
205 {
206 auto *ke = static_cast<QKeyEvent *>(event);
207 if (ke->key() == Qt::Key_Escape && ke->modifiers() == Qt::NoModifier)
208 {
209 if (LuaDebuggerDialog *dlg = LuaDebuggerDialog::instance())
210 {
211 QMetaObject::invokeMethod(dlg, "handleEscapeKey", Qt::QueuedConnection);
212 return true;
213 }
214 }
215 }
216 return QPlainTextEdit::eventFilter(watched, event);
217}
218
219void LuaDebuggerCodeView::rebuildLineHighlights()
220{
221 QList<QTextEdit::ExtraSelection> extraSelections;
222
223 /* Debugger paused line — amber bar; theme-specific (independent of caret). */
224 if (pausedExecutionLine_ > 0)
225 {
226 QTextBlock pauseBlock = document()->findBlockByNumber(static_cast<int>(pausedExecutionLine_ - 1));
227 if (pauseBlock.isValid())
228 {
229 QTextCursor pauseCursor(pauseBlock);
230 pauseCursor.movePosition(QTextCursor::StartOfBlock);
231 QTextEdit::ExtraSelection pauseSel;
232 const LuaDebuggerEditorPalette palette = luaDebuggerEditorPalette();
233 pauseSel.format.setBackground(palette.pausedLine);
234 pauseSel.format.setProperty(QTextFormat::FullWidthSelection, true);
235 pauseSel.cursor = pauseCursor;
236 pauseSel.cursor.clearSelection();
237 extraSelections.append(pauseSel);
238 }
239 }
240
241 /* Caret line — subtle; skip if same line as debugger (do not replace pause look). */
242 QTextBlock caretBlock = textCursor().block();
243 if (caretBlock.isValid())
244 {
245 const int caretLine = caretBlock.blockNumber() + 1;
246 const bool sameAsPause = (pausedExecutionLine_ > 0) && (caretLine == pausedExecutionLine_);
247
248 if (!sameAsPause)
249 {
250 /* Match the line-number gutter background (see applyEditorPalette). */
251 const QColor lineColor = lineNumberArea->palette().color(QPalette::Base);
252
253 QTextCursor caretLineCursor(caretBlock);
254 caretLineCursor.movePosition(QTextCursor::StartOfBlock);
255 QTextEdit::ExtraSelection caretSel;
256 caretSel.format.setBackground(lineColor);
257 caretSel.format.setProperty(QTextFormat::FullWidthSelection, true);
258 caretSel.cursor = caretLineCursor;
259 caretSel.cursor.clearSelection();
260 extraSelections.append(caretSel);
261 }
262 }
263
264 setExtraSelections(extraSelections);
265}
266
267void LuaDebuggerCodeView::setCurrentLine(qint32 line)
268{
269 /* The gutter repaints on cursor movement via the updateRequest
270 * signal, but the early-return branches below do not move the
271 * cursor, and we need the paused-line arrow (and the
272 * accompanying dimmed breakpoint circle, if any) to appear /
273 * disappear whenever pausedExecutionLine_ changes. Request a
274 * line-number-area repaint explicitly in every branch. */
275 if (line <= 0)
276 {
277 pausedExecutionLine_ = -1;
278 rebuildLineHighlights();
279 lineNumberArea->update();
280 return;
281 }
282
283 QTextBlock block = document()->findBlockByNumber(static_cast<int>(line - 1));
284 if (!block.isValid())
285 {
286 pausedExecutionLine_ = -1;
287 rebuildLineHighlights();
288 lineNumberArea->update();
289 return;
290 }
291
292 pausedExecutionLine_ = line;
293 QTextCursor cursor(block);
294 cursor.movePosition(QTextCursor::StartOfBlock);
295 setTextCursor(cursor);
296 rebuildLineHighlights();
297 lineNumberArea->update();
298}
299
300void LuaDebuggerCodeView::moveCaretToLineStart(qint32 line)
301{
302 if (line <= 0)
303 {
304 return;
305 }
306
307 QTextBlock block = document()->findBlockByNumber(static_cast<int>(line - 1));
308 if (!block.isValid())
309 {
310 return;
311 }
312
313 QTextCursor cursor(block);
314 cursor.movePosition(QTextCursor::StartOfBlock);
315 setTextCursor(cursor);
316}
317
318void LuaDebuggerCodeView::clearCurrentLineHighlight()
319{
320 pausedExecutionLine_ = -1;
321 rebuildLineHighlights();
322 /* Force the gutter to repaint so the paused-line arrow
323 * is cleared and any dimmed breakpoint circle returns to full
324 * brightness on resume, even when no cursor movement triggers
325 * updateRequest. */
326 lineNumberArea->update();
327}
328
329void LuaDebuggerCodeView::setEditorFont(const QFont &font)
330{
331 QFont resolvedFont = font;
332 if (resolvedFont.family().isEmpty())
333 {
334 resolvedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
335 }
336 resolvedFont.setStyleHint(QFont::TypeWriter, QFont::PreferDefault);
337 QPlainTextEdit::setFont(resolvedFont);
338 lineNumberArea->setFont(resolvedFont);
339 updateLineNumberAreaWidth(0);
340 lineNumberArea->update();
341 viewport()->update();
342}
343
344void LuaDebuggerCodeView::updateBreakpointMarkers()
345{
346 lineNumberArea->update();
347}
348
349void LuaDebuggerCodeView::applyTheme()
350{
351 applyEditorPalette();
352 if (syntaxHighlighter)
353 {
354 static_cast<LuaSyntaxHighlighter *>(syntaxHighlighter)->updateColors();
355 }
356 lineNumberArea->update();
357 viewport()->update();
358 rebuildLineHighlights();
359}
360
361void LuaDebuggerCodeView::applyEditorPalette()
362{
363 const LuaDebuggerEditorPalette colors = luaDebuggerEditorPalette();
364 QPalette pal = palette();
365 QPalette gutterPal = lineNumberArea->palette();
366
367 pal.setColor(QPalette::Base, colors.editorBackground);
368 pal.setColor(QPalette::Text, colors.editorText);
369 pal.setColor(QPalette::Highlight, colors.selection);
370 pal.setColor(QPalette::HighlightedText, colors.selectionText);
371 /* Match the editor base on Window so the gap between gutter and
372 * viewport doesn't reveal the host palette. */
373 pal.setColor(QPalette::Window, colors.editorBackground);
374
375 gutterPal.setColor(QPalette::Window, colors.gutterBackground);
376 gutterPal.setColor(QPalette::Base, colors.gutterBackground);
377 gutterPal.setColor(QPalette::WindowText, colors.gutterText);
378
379 setPalette(pal);
380 setAutoFillBackground(true);
381
382 viewport()->setPalette(pal);
383 viewport()->setAutoFillBackground(true);
384
385 lineNumberArea->setPalette(gutterPal);
386 lineNumberArea->setAutoFillBackground(true);
387 lineNumberArea->update();
388}
389
390void LuaDebuggerCodeView::lineNumberAreaPaintEvent(QPaintEvent *event)
391{
392 const LuaDebuggerEditorPalette colors = luaDebuggerEditorPalette();
393 QPainter painter(lineNumberArea);
394 painter.setRenderHint(QPainter::Antialiasing, true);
395
396 painter.fillRect(event->rect(), colors.gutterBackground);
397
398 /* Highlight the drag target line if a breakpoint move is in progress. */
399 LineNumberArea *gutter = static_cast<LineNumberArea *>(lineNumberArea);
400 if (gutter && gutter->isDraggingBreakpoint() && gutter->dragTargetLine() > 0)
1
Assuming 'gutter' is null
401 {
402 const qint32 targetLine = gutter->dragTargetLine();
403 QTextBlock targetBlock = document()->findBlockByNumber(static_cast<int>(targetLine - 1));
404 if (targetBlock.isValid())
405 {
406 qint32 targetTop = static_cast<qint32>(blockBoundingGeometry(targetBlock).translated(contentOffset()).top());
407 qint32 targetHeight = static_cast<qint32>(blockBoundingRect(targetBlock).height());
408 QColor dragHighlight = colors.selection;
409 dragHighlight.setAlpha(80);
410 painter.fillRect(0, targetTop, lineNumberArea->width(), targetHeight, dragHighlight);
411 }
412 }
413
414 /* Canonicalize the filename once for all visible lines. */
415 char *canonical = nullptr;
416 if (!filename.isEmpty())
2
Assuming the condition is false
3
Taking false branch
417 {
418 canonical = wslua_debugger_canonical_path(filename.toUtf8().constData());
419 }
420
421 QTextBlock block = firstVisibleBlock();
422 qint32 blockNumber = block.blockNumber();
423 qint32 top = static_cast<qint32>(blockBoundingGeometry(block).translated(contentOffset()).top());
424 qint32 bottom = top + static_cast<qint32>(blockBoundingRect(block).height());
425
426 while (block.isValid() && top <= event->rect().bottom())
4
Assuming the condition is true
5
Assuming the condition is true
427 {
428 if (block.isVisible() && bottom >= event->rect().top())
6
Assuming the condition is true
7
Assuming the condition is true
8
Taking true branch
429 {
430 QString number = QString::number(blockNumber + 1);
431 painter.setPen(colors.gutterText);
432 painter.drawText(0, top, lineNumberArea->width() - 20, fontMetrics().height(), Qt::AlignRight, number);
9
Called C++ object pointer is null
433
434 const qint32 lineNo = static_cast<qint32>(blockNumber + 1);
435 const bool pausedHere = (pausedExecutionLine_ == lineNo);
436
437 /* Breakpoint circle. On the paused line the continue icon is
438 * drawn on top of it.
439 * During a breakpoint drag, skip drawing the source line's
440 * breakpoint so the preview at the target is visually distinct. */
441 const bool isDragSource = (gutter && gutter->isDraggingBreakpoint() && lineNo == gutter->dragSourceLine());
442 if (canonical && !isDragSource)
443 {
444 bool hasExtras = false;
445 const int32_t state = wslua_debugger_get_breakpoint_state_canonical(canonical, lineNo, &hasExtras);
446 if (state != -1)
447 {
448 const qint32 radius = fontMetrics().height() / 2 - 1;
449 const qint32 cx = lineNumberArea->width() - 16;
450 const qint32 cy = top + 2;
451 luaDbgDrawBreakpointDot(painter, cx, cy, radius, (state == 1), colors.gutterText, hasExtras);
452 }
453 }
454
455 /* The continue icon in the theme's warning colour, outlined in
456 * the gutter text colour, marks the line the debugger is paused
457 * at. Drawn after (and therefore on top of) the breakpoint
458 * circle. */
459 if (pausedHere)
460 {
461 const qint32 radius = fontMetrics().height() / 2 - 2;
462 const qint32 diameter = radius * 2;
463 const qint32 x = lineNumberArea->width() - 15;
464 const qint32 y = top + 2;
465 /* The play shape covers about 18/24 of the icon box. Draw
466 * the box at a whole-pixel size, unscaled, centred on the
467 * diameter-sized square the marker occupies. */
468 const qint32 box = qRound(diameter * 24.0 / 18.0);
469 const QPoint boxTopLeft(x + (diameter - box) / 2, y + (diameter - box) / 2);
470 const qreal dpr = painter.device() ? painter.device()->devicePixelRatioF() : 1.0;
471 static ContrastAdaptIcon icon("lua-debug-paused-line");
472 icon.setTokenColors({
473 {ThemeManager::AccentWarning, ThemeManager::instance()->color(ThemeManager::AccentWarning)},
474 {ThemeManager::PaletteText, colors.gutterText},
475 });
476 painter.drawPixmap(boxTopLeft, icon.pixmap(QSize(box, box), dpr));
477 }
478 }
479
480 block = block.next();
481 top = bottom;
482 bottom = top + static_cast<qint32>(blockBoundingRect(block).height());
483 ++blockNumber;
484 }
485
486 /* Draw semi-transparent preview breakpoint at the drag target line.
487 * Uses the same icon (concentric for extras) and colour as the
488 * source breakpoint, with reduced alpha for visual distinction. */
489 if (gutter && gutter->isDraggingBreakpoint() && gutter->dragTargetLine() > 0)
490 {
491 const qint32 targetLine = gutter->dragTargetLine();
492 const qint32 sourceLine = gutter->dragSourceLine();
493 QTextBlock targetBlock = document()->findBlockByNumber(static_cast<int>(targetLine - 1));
494 if (targetBlock.isValid() && canonical && sourceLine > 0)
495 {
496 /* Query the source breakpoint to get its state and extras info. */
497 bool hasExtras = false;
498 const int32_t state = wslua_debugger_get_breakpoint_state_canonical(canonical, sourceLine, &hasExtras);
499 if (state != -1)
500 {
501 qint32 targetTop = static_cast<qint32>(blockBoundingGeometry(targetBlock).translated(contentOffset()).top());
502 qint32 targetBottom = targetTop + static_cast<qint32>(blockBoundingRect(targetBlock).height());
503
504 if (targetTop <= event->rect().bottom() && targetBottom >= event->rect().top())
505 {
506 const qint32 radius = fontMetrics().height() / 2 - 1;
507 const qint32 cx = lineNumberArea->width() - 16;
508 const qint32 cy = targetTop + 2;
509 luaDbgDrawBreakpointDot(painter, cx, cy, radius, (state == 1), colors.gutterText, hasExtras,
510 120);
511 }
512 }
513 }
514 }
515
516 g_free(canonical)(__builtin_object_size ((canonical), 0) != ((size_t) - 1)) ? g_free_sized
(canonical, __builtin_object_size ((canonical), 0)) : (g_free
) (canonical)
;
517}
518
519qint32 LineNumberArea::lineAtY(qint32 yPx) const
520{
521 QTextBlock block = codeEditor->firstVisibleBlock();
522 qint32 top =
523 static_cast<qint32>(codeEditor->blockBoundingGeometry(block).translated(codeEditor->contentOffset()).top());
524 qint32 bottom = top + static_cast<qint32>(codeEditor->blockBoundingRect(block).height());
525 qint32 blockNumber = block.blockNumber();
526 while (block.isValid())
527 {
528 if (yPx >= top && yPx <= bottom)
529 {
530 return blockNumber + 1;
531 }
532 block = block.next();
533 top = bottom;
534 bottom = top + static_cast<qint32>(codeEditor->blockBoundingRect(block).height());
535 ++blockNumber;
536 }
537 return -1;
538}
539
540bool LineNumberArea::hasBreakpointAtLine(qint32 line) const
541{
542 if (!codeEditor || codeEditor->filename.isEmpty() || line < 1)
543 {
544 return false;
545 }
546
547 char *canonical = wslua_debugger_canonical_path(codeEditor->filename.toUtf8().constData());
548 if (!canonical)
549 {
550 return false;
551 }
552 const int32_t state = wslua_debugger_get_breakpoint_state_canonical(canonical, line, /*has_extras=*/nullptr);
553 g_free(canonical)(__builtin_object_size ((canonical), 0) != ((size_t) - 1)) ? g_free_sized
(canonical, __builtin_object_size ((canonical), 0)) : (g_free
) (canonical)
;
554 return state != -1;
555}
556
557qint32 LineNumberArea::nearestVisibleDropLine(qint32 yPx, qint32 sourceLine) const
558{
559 struct Candidate
560 {
561 qint32 line;
562 qint32 centerY;
563 };
564
565 QVector<Candidate> candidates;
566 QTextBlock block = codeEditor->firstVisibleBlock();
567 qint32 top =
568 static_cast<qint32>(codeEditor->blockBoundingGeometry(block).translated(codeEditor->contentOffset()).top());
569 qint32 bottom = top + static_cast<qint32>(codeEditor->blockBoundingRect(block).height());
570 qint32 blockNumber = block.blockNumber();
571 const qint32 viewportBottom = codeEditor->viewport()->height();
572
573 while (block.isValid() && top <= viewportBottom)
574 {
575 if (block.isVisible() && bottom >= 0)
576 {
577 const qint32 line = blockNumber + 1;
578 const bool occupied = hasBreakpointAtLine(line);
579 if (!occupied || line == sourceLine)
580 {
581 candidates.append({line, top + (bottom - top) / 2});
582 }
583 }
584
585 block = block.next();
586 top = bottom;
587 bottom = top + static_cast<qint32>(codeEditor->blockBoundingRect(block).height());
588 ++blockNumber;
589 }
590
591 if (candidates.isEmpty())
592 {
593 return -1;
594 }
595
596 qint32 bestLine = -1;
597 qint32 bestDist = INT_MAX2147483647;
598 qint32 bestLineDelta = INT_MAX2147483647;
599 for (const Candidate &candidate : candidates)
600 {
601 const qint32 yDist = qAbs(candidate.centerY - yPx);
602 const qint32 lineDelta = qAbs(candidate.line - sourceLine);
603 if (bestLine == -1 || yDist < bestDist || (yDist == bestDist && lineDelta < bestLineDelta))
604 {
605 bestLine = candidate.line;
606 bestDist = yDist;
607 bestLineDelta = lineDelta;
608 }
609 }
610 return bestLine;
611}
612
613void LineNumberArea::mousePressEvent(QMouseEvent *event)
614{
615 leftPressArmed_ = false;
616 draggingBreakpoint_ = false;
617 pressedLine_ = -1;
618 dragTargetLine_ = -1;
619 pressShiftToggle_ = false;
620 pressHadBreakpoint_ = false;
621
622 /* Only the primary button drives add / remove / toggle here. The
623 * secondary button (right-click on Win/Linux, Ctrl-click or
624 * two-finger trackpad tap on macOS) is handled by
625 * @ref contextMenuEvent, which always pops the
626 * Edit / Disable / Remove menu so that gesture is never confused
627 * with the toggle path. */
628 if (event->button() != Qt::LeftButton)
629 {
630 QWidget::mousePressEvent(event);
631 return;
632 }
633
634 const QPoint click_pos = event->pos();
635 if (click_pos.x() <= width() - 20)
636 {
637 QWidget::mousePressEvent(event);
638 return;
639 }
640
641 const qint32 lineNo = lineAtY(click_pos.y());
642 if (lineNo < 1)
643 {
644 QWidget::mousePressEvent(event);
645 return;
646 }
647
648 leftPressArmed_ = true;
649 pressPos_ = click_pos;
650 pressedLine_ = lineNo;
651 dragTargetLine_ = lineNo;
652 pressShiftToggle_ = (event->modifiers() & Qt::ShiftModifier) != 0;
653 pressHadBreakpoint_ = hasBreakpointAtLine(lineNo);
654 event->accept();
655}
656
657void LineNumberArea::mouseMoveEvent(QMouseEvent *event)
658{
659 if (!leftPressArmed_ || !pressHadBreakpoint_)
660 {
661 QWidget::mouseMoveEvent(event);
662 return;
663 }
664
665 if (!draggingBreakpoint_)
666 {
667 const qint32 dragDistance = (event->pos() - pressPos_).manhattanLength();
668 if (dragDistance < QApplication::startDragDistance())
669 {
670 return;
671 }
672 draggingBreakpoint_ = true;
673 setCursor(Qt::ClosedHandCursor);
674 }
675
676 const qint32 prevTarget = dragTargetLine_;
677 dragTargetLine_ = nearestVisibleDropLine(event->pos().y(), pressedLine_);
678
679 /* Repaint gutter to show updated drag target highlight. */
680 if (dragTargetLine_ != prevTarget)
681 {
682 update();
683 }
684 event->accept();
685}
686
687void LineNumberArea::mouseReleaseEvent(QMouseEvent *event)
688{
689 if (event->button() != Qt::LeftButton)
690 {
691 QWidget::mouseReleaseEvent(event);
692 return;
693 }
694
695 const bool armed = leftPressArmed_;
696 const bool wasDragging = draggingBreakpoint_;
697 const bool sourceHasBreakpoint = pressHadBreakpoint_;
698 const bool toggleActive = pressShiftToggle_;
699 const qint32 lineNo = pressedLine_;
700 const qint32 dropLine = (dragTargetLine_ > 0) ? dragTargetLine_ : nearestVisibleDropLine(event->pos().y(), lineNo);
701
702 leftPressArmed_ = false;
703 draggingBreakpoint_ = false;
704 pressedLine_ = -1;
705 dragTargetLine_ = -1;
706 pressShiftToggle_ = false;
707 pressHadBreakpoint_ = false;
708 unsetCursor();
709
710 if (!armed || lineNo < 1)
711 {
712 QWidget::mouseReleaseEvent(event);
713 return;
714 }
715
716 if (wasDragging && sourceHasBreakpoint)
717 {
718 if (dropLine > 0 && dropLine != lineNo)
719 {
720 emit codeEditor->breakpointMoveRequested(codeEditor->filename, lineNo, dropLine);
721 codeEditor->viewport()->update();
722 update();
723 }
724 event->accept();
725 return;
726 }
727
728 /* Plain left-click on a "rich" breakpoint (one carrying a
729 * condition, a hit-count target, or a log message) opens the
730 * Edit / Disable / Remove popup instead of removing it: those
731 * extras are easy to lose to a misclick, so the destructive
732 * action requires an explicit Remove choice from the menu. Plain
733 * breakpoints and clicks on bare lines keep the original
734 * add-or-remove-on-click flow. Shift+click keeps the existing
735 * modifier semantics (toggle active / pre-arm disabled). */
736 bool richBp = false;
737 if (!toggleActive && !codeEditor->filename.isEmpty())
738 {
739 char *canonical = wslua_debugger_canonical_path(codeEditor->filename.toUtf8().constData());
740 if (canonical)
741 {
742 bool hasExtras = false;
743 const int32_t state = wslua_debugger_get_breakpoint_state_canonical(canonical, lineNo, &hasExtras);
744 g_free(canonical)(__builtin_object_size ((canonical), 0) != ((size_t) - 1)) ? g_free_sized
(canonical, __builtin_object_size ((canonical), 0)) : (g_free
) (canonical)
;
745 if (state != -1 && hasExtras)
746 {
747 richBp = true;
748 }
749 }
750 }
751
752 if (richBp)
753 {
754 QPoint globalPos = event->globalPosition().toPoint();
755 emit codeEditor->breakpointGutterMenuRequested(codeEditor->filename, lineNo, globalPos);
756 }
757 else
758 {
759 emit codeEditor->breakpointToggled(codeEditor->filename, lineNo, toggleActive);
760 codeEditor->viewport()->update();
761 update();
762 }
763
764 event->accept();
765}
766
767void LineNumberArea::contextMenuEvent(QContextMenuEvent *event)
768{
769 /* Secondary-click contract on the gutter: always pop the
770 * Edit / Disable / Remove menu when the gesture lands on an
771 * existing breakpoint, regardless of whether it carries extras.
772 * This is the canonical "second click" affordance — works on
773 * Windows / Linux right-click, macOS Ctrl-click and the macOS
774 * two-finger trackpad tap, all of which Qt funnels through
775 * QContextMenuEvent. Bare lines have nothing to act on; we let
776 * the event through so the parent editor (or its viewport) can
777 * decide what to do (currently nothing in that area). */
778 const QPoint pos = event->pos();
779 if (pos.x() <= width() - 20 || codeEditor->filename.isEmpty())
780 {
781 QWidget::contextMenuEvent(event);
782 return;
783 }
784
785 const qint32 lineNo = lineAtY(pos.y());
786 if (lineNo < 1)
787 {
788 QWidget::contextMenuEvent(event);
789 return;
790 }
791
792 char *canonical = wslua_debugger_canonical_path(codeEditor->filename.toUtf8().constData());
793 if (!canonical)
794 {
795 QWidget::contextMenuEvent(event);
796 return;
797 }
798 const int32_t state = wslua_debugger_get_breakpoint_state_canonical(canonical, lineNo, /*has_extras=*/nullptr);
799 g_free(canonical)(__builtin_object_size ((canonical), 0) != ((size_t) - 1)) ? g_free_sized
(canonical, __builtin_object_size ((canonical), 0)) : (g_free
) (canonical)
;
800 if (state == -1)
801 {
802 QWidget::contextMenuEvent(event);
803 return;
804 }
805
806 emit codeEditor->breakpointGutterMenuRequested(codeEditor->filename, lineNo, event->globalPos());
807 event->accept();
808}
809
810/* ===== font_policy ===== */
811
812void LuaDebuggerFontPolicy::attach(QTabWidget *codeTabs, QTreeView *variablesTree, QTreeView *watchTree,
813 QStandardItemModel *watchModel, QTreeView *stackTree, QTreeView *fileTree,
814 QTreeView *breakpointsTree, QPlainTextEdit *evalInputEdit,
815 QPlainTextEdit *evalOutputEdit)
816{
817 codeTabs_ = codeTabs;
818 variablesTree_ = variablesTree;
819 watchTree_ = watchTree;
820 watchModel_ = watchModel;
821 stackTree_ = stackTree;
822 fileTree_ = fileTree;
823 breakpointsTree_ = breakpointsTree;
824 evalInputEdit_ = evalInputEdit;
825 evalOutputEdit_ = evalOutputEdit;
826}
827
828void LuaDebuggerFontPolicy::applyAll()
829{
830 applyToCodeEditors(monospaceFont(true));
831 applyToPanels();
832}
833
834void LuaDebuggerFontPolicy::applyToCodeEditors(const QFont &font)
835{
836 QFont effective = font;
837 if (effective.family().isEmpty())
838 {
839 effective = monospaceFont(true);
840 }
841
842 if (!codeTabs_)
843 {
844 return;
845 }
846 const qint32 tabCount = static_cast<qint32>(codeTabs_->count());
847 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
848 {
849 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(codeTabs_->widget(static_cast<int>(tabIndex)));
850 if (view)
851 {
852 view->setEditorFont(effective);
853 }
854 }
855}
856
857void LuaDebuggerFontPolicy::applyToPanels()
858{
859 const QFont panelMono = monospaceFont(false);
860 const QFont headerFont = regularFont();
861
862 const QList<QWidget *> widgets = {variablesTree_, watchTree_, stackTree_,
863 breakpointsTree_, evalInputEdit_, evalOutputEdit_};
864 for (QWidget *widget : widgets)
865 {
866 if (widget)
867 {
868 widget->setFont(panelMono);
869 }
870 }
871
872 const QList<QTreeView *> treesWithStandardHeaders = {variablesTree_, watchTree_, stackTree_, fileTree_,
873 breakpointsTree_};
874 for (QTreeView *tree : treesWithStandardHeaders)
875 {
876 if (tree && tree->header())
877 {
878 tree->header()->setFont(headerFont);
879 }
880 }
881 reapplyToWatchItemModel();
882}
883
884namespace
885{
886
887/* Walk the watch QStandardItemModel tree: set each item's QFont to the
888 * panel monospace while preserving the existing bold bit (change-
889 * highlight) so it wins over the tree widget font after row moves. */
890// NOLINTNEXTLINE(misc-no-recursion)
891void reapplyMonospaceToWatchItemModelRecursive(const QFont &base, QStandardItemModel *m, const QModelIndex &parent)
892{
893 if (!m)
894 {
895 return;
896 }
897 const int rows = m->rowCount(parent);
898 const int cols = m->columnCount(parent);
899 for (int r = 0; r < rows; ++r)
900 {
901 for (int c = 0; c < cols; ++c)
902 {
903 const QModelIndex idx = m->index(r, c, parent);
904 if (QStandardItem *it = m->itemFromIndex(idx))
905 {
906 QFont f = base;
907 f.setBold(it->font().bold());
908 it->setFont(f);
909 }
910 }
911 const QModelIndex specIndex = m->index(r, WatchColumn::Spec, parent);
912 if (specIndex.isValid() && m->rowCount(specIndex) > 0)
913 {
914 reapplyMonospaceToWatchItemModelRecursive(base, m, specIndex);
915 }
916 }
917}
918
919} // namespace
920
921void LuaDebuggerFontPolicy::reapplyToWatchItemModel()
922{
923 if (!watchModel_)
924 {
925 return;
926 }
927 reapplyMonospaceToWatchItemModelRecursive(monospaceFont(false), watchModel_, QModelIndex());
928 if (watchTree_)
929 {
930 watchTree_->update();
931 }
932}
933
934// TODO: LuaDebuggerFontPolicy duplicates FontManager (font resolution + the
935// app-ready fallback). Drop it and have callers use the FontManager directly.
936QFont LuaDebuggerFontPolicy::monospaceFont(bool zoomed) const
937{
938 /* Monospace font for panels and the script editor. */
939 if (mainApp && mainApp->isInitialized())
940 {
941 return (zoomed ? FontManager::zoomedMonospaceFont() : FontManager::monospaceFont());
942 }
943
944 /* Fall back to system fixed font */
945 return QFontDatabase::systemFont(QFontDatabase::FixedFont);
946}
947
948QFont LuaDebuggerFontPolicy::regularFont() const
949{
950 if (mainApp && mainApp->isInitialized())
951 {
952 return mainApp->font();
953 }
954 return QGuiApplication::font();
955}
956
957/* ===== code_tabs_controller ===== */
958
959LuaDebuggerCodeTabsController::LuaDebuggerCodeTabsController(LuaDebuggerDialog *host) : QObject(host), host_(host) {}
960
961void LuaDebuggerCodeTabsController::attach(QTabWidget *tabs)
962{
963 tabs_ = tabs;
964}
965
966void LuaDebuggerDialog::wireCodeTabs()
967{
968 codeTabsController_.attach(ui->codeTabWidget);
969
970 connect(ui->actionSaveFile, &QAction::triggered, &codeTabsController_, &LuaDebuggerCodeTabsController::onSaveFile);
971
972 connect(ui->codeTabWidget, &QTabWidget::tabCloseRequested, &codeTabsController_,
973 &LuaDebuggerCodeTabsController::onTabCloseRequested);
974 connect(ui->codeTabWidget, &QTabWidget::currentChanged, &codeTabsController_,
975 &LuaDebuggerCodeTabsController::onCurrentTabChanged);
976}
977
978QString LuaDebuggerCodeTabsController::lastOpenDirectory()
979{
980 if (lastOpenDirectory_.isEmpty())
981 {
982 lastOpenDirectory_ = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
983 if (lastOpenDirectory_.isEmpty())
984 {
985 lastOpenDirectory_ = QDir::homePath();
986 }
987 }
988 return lastOpenDirectory_;
989}
990
991void LuaDebuggerCodeTabsController::setLastOpenDirectory(const QString &dir)
992{
993 lastOpenDirectory_ = dir;
994}
995
996LuaDebuggerCodeView *LuaDebuggerCodeTabsController::loadFile(const QString &file_path)
997{
998 if (!host_ || !tabs_)
999 {
1000 return nullptr;
1001 }
1002
1003 QString normalizedPath = host_->normalizedFilePath(file_path);
1004 if (normalizedPath.isEmpty())
1005 {
1006 normalizedPath = file_path;
1007 }
1008
1009 /* Check if file exists before creating a tab. */
1010 QFileInfo fileInfo(normalizedPath);
1011 if (!fileInfo.exists() || !fileInfo.isFile())
1012 {
1013 return nullptr;
1014 }
1015
1016 const qint32 existingTabCount = static_cast<qint32>(tabs_->count());
1017 for (qint32 tabIndex = 0; tabIndex < existingTabCount; ++tabIndex)
1018 {
1019 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1020 if (view && view->getFilename() == normalizedPath)
1021 {
1022 tabs_->setCurrentIndex(static_cast<int>(tabIndex));
1023 return view;
1024 }
1025 }
1026
1027 LuaDebuggerCodeView *codeView = new LuaDebuggerCodeView(tabs_);
1028 codeView->setEditorFont(host_->fontPolicy().monospaceFont(true));
1029 codeView->setFilename(normalizedPath);
1030
1031 QFile file(normalizedPath);
1032 if (file.open(QIODevice::ReadOnly | QIODevice::Text))
1033 {
1034 codeView->setPlainText(file.readAll());
1035 }
1036 else
1037 {
1038 /* This should not happen since we checked exists() above,
1039 * but handle it gracefully just in case. */
1040 delete codeView;
1041 return nullptr;
1042 }
1043
1044 host_->filesController().ensureEntry(normalizedPath);
1045
1046 codeView->setContextMenuPolicy(Qt::CustomContextMenu);
1047 connect(codeView, &QWidget::customContextMenuRequested, host_, &LuaDebuggerDialog::onCodeViewContextMenu);
1048
1049 /* Queued connection: the gutter emits this from inside its
1050 * mousePressEvent, so dispatching the popup via a queued slot
1051 * lets the press event fully unwind before QMenu::exec() spins
1052 * its own nested loop. Avoids the "press grab still attached"
1053 * artefacts that bite when a modal popup is opened directly out
1054 * of a press handler. */
1055 connect(codeView, &LuaDebuggerCodeView::breakpointGutterMenuRequested, &host_->breakpointsController(),
1056 &LuaDebuggerBreakpointsController::showGutterMenu, Qt::QueuedConnection);
1057
1058 connect(codeView, &LuaDebuggerCodeView::breakpointToggled, this,
1059 &LuaDebuggerCodeTabsController::onCodeViewBreakpointToggled);
1060
1061 connect(codeView, &LuaDebuggerCodeView::breakpointMoveRequested, &host_->breakpointsController(),
1062 &LuaDebuggerBreakpointsController::moveAtLine, Qt::QueuedConnection);
1063
1064 connect(codeView->document(), &QTextDocument::modificationChanged, this,
1065 [this, codeView]()
1066 {
1067 updateTabTextForCodeView(codeView);
1068 updateWindowModifiedState();
1069 if (tabs_ && tabs_->currentWidget() == codeView)
1070 {
1071 updateSaveActionState();
1072 }
1073 });
1074 connect(codeView, &QPlainTextEdit::cursorPositionChanged, &host_->breakpointsController(),
1075 &LuaDebuggerBreakpointsController::updateHeaderButtonState);
1076
1077 tabs_->addTab(codeView, QFileInfo(normalizedPath).fileName());
1078 updateTabTextForCodeView(codeView);
1079 tabs_->setCurrentWidget(codeView);
1080 tabs_->show();
1081 updateSaveActionState();
1082 updateWindowModifiedState();
1083 host_->updateLuaEditorAuxFrames();
1084 return codeView;
1085}
1086
1087LuaDebuggerCodeView *LuaDebuggerCodeTabsController::currentCodeView() const
1088{
1089 if (!tabs_)
1090 {
1091 return nullptr;
1092 }
1093 return qobject_cast<LuaDebuggerCodeView *>(tabs_->currentWidget());
1094}
1095
1096qint32 LuaDebuggerCodeTabsController::unsavedOpenScriptTabCount() const
1097{
1098 if (!tabs_)
1099 {
1100 return 0;
1101 }
1102 qint32 count = 0;
1103 const qint32 tabCount = static_cast<qint32>(tabs_->count());
1104 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1105 {
1106 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1107 if (view && view->document()->isModified())
1108 {
1109 ++count;
1110 }
1111 }
1112 return count;
1113}
1114
1115bool LuaDebuggerCodeTabsController::hasUnsavedChanges() const
1116{
1117 return unsavedOpenScriptTabCount() > 0;
1118}
1119
1120bool LuaDebuggerCodeTabsController::ensureUnsavedChangesHandled(const QString &title)
1121{
1122 if (!hasUnsavedChanges())
1123 {
1124 return true;
1125 }
1126
1127 const qint32 unsavedCount = unsavedOpenScriptTabCount();
1128 const QMessageBox::StandardButton reply =
1129 QMessageBox::question(host_, title, tr("There are unsaved changes in %Ln open file(s).", "", unsavedCount),
1130 QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save);
1131
1132 if (reply == QMessageBox::Cancel)
1133 {
1134 return false;
1135 }
1136 if (reply == QMessageBox::Save)
1137 {
1138 return saveAllModified();
1139 }
1140 clearAllDocumentModified();
1141 return true;
1142}
1143
1144void LuaDebuggerCodeTabsController::clearAllDocumentModified()
1145{
1146 if (!tabs_)
1147 {
1148 return;
1149 }
1150 const qint32 tabCount = static_cast<qint32>(tabs_->count());
1151 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1152 {
1153 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1154 if (view)
1155 {
1156 view->document()->setModified(false);
1157 }
1158 }
1159}
1160
1161bool LuaDebuggerCodeTabsController::saveCodeView(LuaDebuggerCodeView *view)
1162{
1163 if (!view)
1164 {
1165 return false;
1166 }
1167 const QString path = view->getFilename();
1168 if (path.isEmpty())
1169 {
1170 return false;
1171 }
1172
1173 QFile file(path);
1174 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
1175 {
1176 QMessageBox::warning(host_, tr("Save Lua Script"),
1177 tr("Could not write to %1:\n%2").arg(path, file.errorString()));
1178 return false;
1179 }
1180 QTextStream out(&file);
1181 out << view->toPlainText();
1182 file.close();
1183 view->document()->setModified(false);
1184 return true;
1185}
1186
1187bool LuaDebuggerCodeTabsController::saveAllModified()
1188{
1189 if (!tabs_)
1190 {
1191 return true;
1192 }
1193 const qint32 tabCount = static_cast<qint32>(tabs_->count());
1194 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1195 {
1196 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1197 if (view && view->document()->isModified())
1198 {
1199 if (!saveCodeView(view))
1200 {
1201 return false;
1202 }
1203 }
1204 }
1205 return true;
1206}
1207
1208void LuaDebuggerCodeTabsController::updateTabTextForCodeView(LuaDebuggerCodeView *view)
1209{
1210 if (!view || !tabs_)
1211 {
1212 return;
1213 }
1214 const int tabIndex = tabs_->indexOf(view);
1215 if (tabIndex < 0)
1216 {
1217 return;
1218 }
1219 QString label = QFileInfo(view->getFilename()).fileName();
1220 if (view->document()->isModified())
1221 {
1222 label += QStringLiteral(" *")(QString(QtPrivate::qMakeStringPrivate(u"" " *")));
1223 }
1224 tabs_->setTabText(tabIndex, label);
1225}
1226
1227void LuaDebuggerCodeTabsController::updateSaveActionState()
1228{
1229 if (!host_)
1230 {
1231 return;
1232 }
1233 LuaDebuggerCodeView *view = currentCodeView();
1234 host_->setSaveActionEnabled(view && view->document()->isModified());
1235}
1236
1237void LuaDebuggerCodeTabsController::updateWindowModifiedState()
1238{
1239 if (!host_)
1240 {
1241 return;
1242 }
1243 host_->setWindowModified(hasUnsavedChanges());
1244}
1245
1246void LuaDebuggerCodeTabsController::openInitialBreakpointFiles(const QVector<QString> &files)
1247{
1248 for (const QString &path : files)
1249 {
1250 loadFile(path);
1251 }
1252}
1253
1254void LuaDebuggerCodeTabsController::clearAllCodeHighlights()
1255{
1256 if (!tabs_)
1257 {
1258 return;
1259 }
1260 const qint32 tabCount = static_cast<qint32>(tabs_->count());
1261 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1262 {
1263 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1264 if (view)
1265 {
1266 view->clearCurrentLineHighlight();
1267 }
1268 }
1269}
1270
1271void LuaDebuggerCodeTabsController::applyThemeToAllTabs()
1272{
1273 if (!tabs_)
1274 {
1275 return;
1276 }
1277 const qint32 tabCount = static_cast<qint32>(tabs_->count());
1278 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1279 {
1280 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1281 if (view)
1282 {
1283 view->applyTheme();
1284 }
1285 }
1286}
1287
1288void LuaDebuggerCodeTabsController::onSaveFile()
1289{
1290 LuaDebuggerCodeView *view = currentCodeView();
1291 if (!view || !view->document()->isModified())
1292 {
1293 return;
1294 }
1295 saveCodeView(view);
1296 updateSaveActionState();
1297}
1298
1299void LuaDebuggerCodeTabsController::onCurrentTabChanged(int index)
1300{
1301 Q_UNUSED(index)(void)index;;
1302 if (!host_)
1303 {
1304 return;
1305 }
1306 updateSaveActionState();
1307 host_->updateLuaEditorAuxFrames();
1308 host_->breakpointsController().updateHeaderButtonState();
1309 host_->updateContinueActionState();
1310}
1311
1312void LuaDebuggerCodeTabsController::onTabCloseRequested(int index)
1313{
1314 if (!host_ || !tabs_)
1315 {
1316 return;
1317 }
1318 QWidget *widget = tabs_->widget(index);
1319 auto *view = qobject_cast<LuaDebuggerCodeView *>(widget);
1320 if (view && view->document()->isModified())
1321 {
1322 const QMessageBox::StandardButton reply = QMessageBox::question(
1323 host_, tr("Lua Debugger"),
1324 tr("Save changes to %1 before closing?").arg(QFileInfo(view->getFilename()).fileName()),
1325 QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save);
1326 if (reply == QMessageBox::Cancel)
1327 {
1328 return;
1329 }
1330 if (reply == QMessageBox::Save)
1331 {
1332 if (!saveCodeView(view))
1333 {
1334 return;
1335 }
1336 }
1337 else
1338 {
1339 view->document()->setModified(false);
1340 }
1341 }
1342
1343 tabs_->removeTab(index);
1344 delete widget;
1345 updateSaveActionState();
1346 updateWindowModifiedState();
1347}
1348
1349void LuaDebuggerCodeTabsController::onCodeViewBreakpointToggled(const QString &file_path, qint32 line, bool toggleActive)
1350{
1351 if (!host_)
1352 {
1353 return;
1354 }
1355 /* Two distinct gestures share this code-view signal:
1356 *
1357 * - Plain click (toggleActive=false) — F9 / gutter add+remove. The
1358 * breakpoint is created active and the debugger is auto-enabled
1359 * so the new row is immediately effective.
1360 * - Shift+click (toggleActive=true) — pre-arm / disarm without
1361 * destroying the row, so the user can park breakpoints on lines
1362 * they intend to revisit later. Never auto-enables the debugger;
1363 * that would defeat the "no surprise" intent.
1364 *
1365 * Both gestures dispatch through @ref LuaDebuggerBreakpointsController
1366 * so the chrome refresh (table rebuild, gutter markers, header dot)
1367 * is identical regardless of the entry point. */
1368 if (toggleActive)
1369 {
1370 host_->breakpointsController().shiftToggleAtLine(file_path, line);
1371 }
1372 else
1373 {
1374 host_->breakpointsController().toggleAtLine(file_path, line);
1375 }
1376}