ACMX 2.139.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
editor.cpp
Go to the documentation of this file.
1#include "editor.hpp"
2#include "custom_style.hpp"
3#include <QAbstractItemView>
4#include <QAction>
5#include <QApplication>
6#include <QCheckBox>
7#include <QClipboard>
8#include <QColor>
9#include <QCompleter>
10#include <QDoubleSpinBox>
11#include <QFile>
12#include <QFileDialog>
13#include <QFont>
14#include <QHBoxLayout>
15#include <QInputDialog>
16#include <QKeyEvent>
17#include <QLabel>
18#include <QMenuBar>
19#include <QMessageBox>
20#include <QMimeData>
21#include <QMouseEvent>
22#include <QPainter>
23#include <QPalette>
24#include <QPlainTextEdit>
25#include <QPushButton>
26#include <QRegularExpression>
27#include <QSaveFile>
28#include <QScrollArea>
29#include <QScrollBar>
30#include <QSettings>
31#include <QSignalBlocker>
32#include <QSizePolicy>
33#include <QSlider>
34#include <QSplitter>
35#include <QStatusBar>
36#include <QStringListModel>
37#include <QSyntaxHighlighter>
38#include <QTextBlock>
39#include <QTextCharFormat>
40#include <QTextCursor>
41#include <QTextStream>
42#include <QTimer>
43#include <QVBoxLayout>
44#include <algorithm>
45
46namespace {
47 constexpr int LOOP_SAFETY_LIMIT = 10000;
48 const QString LOOP_SAFETY_MARKER = QStringLiteral("ACMX_LOOP_GUARD");
49
50 bool is_identifier_character(QChar character) { return character.isLetterOrNumber() || character == QLatin1Char('_'); }
51
52 int skip_glsl_trivia(const QString &source, int position) {
53 while (position < source.size()) {
54 if (source.at(position).isSpace()) {
55 ++position;
56 } else if (source.mid(position, 2) == QStringLiteral("//")) {
57 position = source.indexOf(QLatin1Char('\n'), position + 2);
58 if (position < 0)
59 return source.size();
60 } else if (source.mid(position, 2) == QStringLiteral("/*")) {
61 position = source.indexOf(QStringLiteral("*/"), position + 2);
62 if (position < 0)
63 return source.size();
64 position += 2;
65 } else {
66 break;
67 }
68 }
69 return position;
70 }
71
72 int find_matching_glsl_delimiter(const QString &source, int opening, QChar open, QChar close) {
73 int depth = 0;
74 for (int position = opening; position < source.size(); ++position) {
75 if (source.mid(position, 2) == QStringLiteral("//")) {
76 position = source.indexOf(QLatin1Char('\n'), position + 2);
77 if (position < 0)
78 return -1;
79 } else if (source.mid(position, 2) == QStringLiteral("/*")) {
80 position = source.indexOf(QStringLiteral("*/"), position + 2);
81 if (position < 0)
82 return -1;
83 ++position;
84 } else if (source.at(position) == QLatin1Char('"')) {
85 ++position;
86 while (position < source.size() && source.at(position) != QLatin1Char('"')) {
87 if (source.at(position) == QLatin1Char('\\'))
88 ++position;
89 ++position;
90 }
91 } else if (source.at(position) == open) {
92 ++depth;
93 } else if (source.at(position) == close && --depth == 0) {
94 return position;
95 }
96 }
97 return -1;
98 }
99
100 struct LoopRange {
101 int keyword = 0;
102 int body_open = 0;
103 int body_close = 0;
104 };
105} // namespace
106
107QString inject_safety_counters(const QString &glsl_source) {
108 QVector<LoopRange> loops;
109 for (int position = 0; position < glsl_source.size(); ++position) {
110 if (glsl_source.mid(position, 2) == QStringLiteral("//")) {
111 position = glsl_source.indexOf(QLatin1Char('\n'), position + 2);
112 if (position < 0)
113 break;
114 continue;
115 }
116 if (glsl_source.mid(position, 2) == QStringLiteral("/*")) {
117 position = glsl_source.indexOf(QStringLiteral("*/"), position + 2);
118 if (position < 0)
119 break;
120 ++position;
121 continue;
122 }
123 if (glsl_source.at(position) == QLatin1Char('"')) {
124 ++position;
125 while (position < glsl_source.size() && glsl_source.at(position) != QLatin1Char('"')) {
126 if (glsl_source.at(position) == QLatin1Char('\\'))
127 ++position;
128 ++position;
129 }
130 continue;
131 }
132 const bool keyword_start = position == 0 || !is_identifier_character(glsl_source.at(position - 1));
133 const bool is_for = glsl_source.mid(position, 3) == QStringLiteral("for") && keyword_start && (position + 3 >= glsl_source.size() || !is_identifier_character(glsl_source.at(position + 3)));
134 const bool is_while = glsl_source.mid(position, 5) == QStringLiteral("while") && keyword_start && (position + 5 >= glsl_source.size() || !is_identifier_character(glsl_source.at(position + 5)));
135 if (!is_for && !is_while)
136 continue;
137
138 const int marker_start = qMax(0, position - 64);
139 if (glsl_source.mid(marker_start, position - marker_start).contains(LOOP_SAFETY_MARKER))
140 continue;
141 const int condition_open = skip_glsl_trivia(glsl_source, position + (is_for ? 3 : 5));
142 if (condition_open >= glsl_source.size() || glsl_source.at(condition_open) != QLatin1Char('('))
143 continue;
144 const int condition_close = find_matching_glsl_delimiter(glsl_source, condition_open, QLatin1Char('('), QLatin1Char(')'));
145 if (condition_close < 0)
146 continue;
147 const int body_open = skip_glsl_trivia(glsl_source, condition_close + 1);
148 if (body_open >= glsl_source.size() || glsl_source.at(body_open) != QLatin1Char('{'))
149 continue;
150 const int body_close = find_matching_glsl_delimiter(glsl_source, body_open, QLatin1Char('{'), QLatin1Char('}'));
151 if (body_close < 0)
152 continue;
153 loops.append({position, body_open, body_close});
154 position = condition_close;
155 }
156
157 QVector<QPair<int, QString>> insertions;
158 for (int index = 0; index < loops.size(); ++index) {
159 const LoopRange &loop = loops.at(index);
160 const QString counter = QStringLiteral("_acmx_loop_guard_%1").arg(index);
161 const int loop_line = glsl_source.left(loop.keyword).count(QLatin1Char('\n')) + 1;
162 const int body_line = glsl_source.left(loop.body_open).count(QLatin1Char('\n')) + 1;
163 insertions.append({loop.body_close + 1, QStringLiteral("}")});
164 insertions.append({loop.body_open + 1, QStringLiteral("\n#line %1\nif (++%2 > %3) { break; }\n#line %1\n").arg(body_line).arg(counter).arg(LOOP_SAFETY_LIMIT)});
165 insertions.append({loop.keyword, QStringLiteral("{ int %1 = 0; // %2\n#line %3\n").arg(counter, LOOP_SAFETY_MARKER).arg(loop_line)});
166 }
167 std::sort(insertions.begin(), insertions.end(), [](const QPair<int, QString> &left, const QPair<int, QString> &right) { return left.first > right.first; });
168 QString modified_source = glsl_source;
169 for (const QPair<int, QString> &insertion : insertions)
170 modified_source.insert(insertion.first, insertion.second);
171 return modified_source;
172}
173
174// --- CustomTextEdit ---
175
176static QColor currentLineBackground(const QPalette &palette) {
177 const QColor base = palette.color(QPalette::Base);
178 const QColor text = palette.color(QPalette::Text);
179 const qreal textMix = base.lightnessF() > 0.5 ? 0.08 : 0.15;
180
181 auto blendChannel = [textMix](int baseChannel, int textChannel) { return qRound(baseChannel + (textChannel - baseChannel) * textMix); };
182
183 return QColor(blendChannel(base.red(), text.red()), blendChannel(base.green(), text.green()), blendChannel(base.blue(), text.blue()));
184}
185
187 switch (severity) {
189 return QColor(240, 178, 50);
191 return QColor(80, 165, 230);
193 return QColor(225, 65, 65);
194 }
195 return QColor(225, 65, 65);
196}
197
198CustomTextEdit::CustomTextEdit(QWidget *parent) : QPlainTextEdit(parent) {
200 m_completionModel = new QStringListModel(this);
201 m_completer = new QCompleter(m_completionModel, this);
202 m_completer->setWidget(this);
203 m_completer->setCompletionMode(QCompleter::PopupCompletion);
204 m_completer->setCaseSensitivity(Qt::CaseInsensitive);
205 m_completer->setFilterMode(Qt::MatchStartsWith);
206 connect(m_completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this, &CustomTextEdit::insertCompletion);
207
208 connect(this, &QPlainTextEdit::blockCountChanged, this, &CustomTextEdit::updateLineNumberAreaWidth);
209 connect(this, &QPlainTextEdit::updateRequest, this, &CustomTextEdit::updateLineNumberArea);
210 connect(this, &QPlainTextEdit::cursorPositionChanged, this, &CustomTextEdit::highlightCurrentLine);
211 connect(this, &QPlainTextEdit::cursorPositionChanged, this, &CustomTextEdit::matchBrackets);
212
215}
216
217void CustomTextEdit::changeEvent(QEvent *event) {
218 QPlainTextEdit::changeEvent(event);
219
220 if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange || event->type() == QEvent::ApplicationPaletteChange) {
222 m_lineNumberArea->update();
223 emit themeChanged();
224 }
225}
226
228 int digits = 1;
229 int max = qMax(1, blockCount());
230 while (max >= 10) {
231 max /= 10;
232 ++digits;
233 }
234 digits = qMax(digits, 3);
235 return 24 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
236}
237
238void CustomTextEdit::setDiagnostics(const QVector<ShaderDiagnostic> &diagnostics) {
239 m_diagnostics = diagnostics;
240 m_lineNumberArea->update();
242}
243
244void CustomTextEdit::setCompletionWords(const QStringList &words) {
245 QStringList uniqueWords = words;
246 uniqueWords.removeDuplicates();
247 uniqueWords.sort(Qt::CaseInsensitive);
248 m_completionModel->setStringList(uniqueWords);
249}
250
252 QTextCursor cursor = textCursor();
253 const int end = cursor.position();
254 int start = end;
255 while (start > 0) {
256 const QChar character = document()->characterAt(start - 1);
257 if (!character.isLetterOrNumber() && character != QLatin1Char('_') && character != QLatin1Char('.')) {
258 break;
259 }
260 --start;
261 }
262 cursor.setPosition(start);
263 cursor.setPosition(end, QTextCursor::KeepAnchor);
264 return cursor.selectedText();
265}
266
267void CustomTextEdit::insertCompletion(const QString &completion) {
268 QTextCursor cursor = textCursor();
269 const QString prefix = completionPrefix();
270 if (!prefix.isEmpty()) {
271 cursor.setPosition(cursor.position() - prefix.size());
272 cursor.setPosition(cursor.position() + prefix.size(), QTextCursor::KeepAnchor);
273 }
274 cursor.insertText(completion);
275 setTextCursor(cursor);
276}
277
279 const QString prefix = completionPrefix();
280 if (!forced && prefix.size() < 3) {
281 m_completer->popup()->hide();
282 return;
283 }
284 m_completer->setCompletionPrefix(prefix);
285 if (m_completer->completionCount() == 0) {
286 m_completer->popup()->hide();
287 return;
288 }
289 m_completer->popup()->setCurrentIndex(m_completer->completionModel()->index(0, 0));
290 QRect popupRect = cursorRect();
291 popupRect.setWidth(qMax(280, m_completer->popup()->sizeHintForColumn(0) + m_completer->popup()->verticalScrollBar()->sizeHint().width()));
292 m_completer->complete(popupRect);
293}
294
295void CustomTextEdit::updateLineNumberAreaWidth(int /*newBlockCount*/) { setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); }
296
297void CustomTextEdit::updateLineNumberArea(const QRect &rect, int dy) {
298 if (dy)
299 m_lineNumberArea->scroll(0, dy);
300 else
301 m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height());
302
303 if (rect.contains(viewport()->rect()))
305}
306
307void CustomTextEdit::resizeEvent(QResizeEvent *event) {
308 QPlainTextEdit::resizeEvent(event);
309 QRect cr = contentsRect();
310 m_lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
311}
312
314 QPainter painter(m_lineNumberArea);
315 painter.fillRect(event->rect(), QColor(30, 30, 30));
316
317 QTextBlock block = firstVisibleBlock();
318 int blockNumber = block.blockNumber();
319 int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
320 int bottom = top + qRound(blockBoundingRect(block).height());
321
322 while (block.isValid() && top <= event->rect().bottom()) {
323 if (block.isVisible() && bottom >= event->rect().top()) {
324 QString number = QString::number(blockNumber + 1);
325 painter.setPen(QColor(120, 120, 120));
326 if (blockNumber == textCursor().blockNumber())
327 painter.setPen(QColor(220, 220, 220));
328 painter.drawText(14, top, m_lineNumberArea->width() - 19, fontMetrics().height(), Qt::AlignRight, number);
329 for (const ShaderDiagnostic &diagnostic : m_diagnostics) {
330 if (diagnostic.line != blockNumber + 1)
331 continue;
332 painter.setBrush(diagnosticColor(diagnostic.severity));
333 painter.setPen(Qt::NoPen);
334 const int diameter = qMin(9, fontMetrics().height() - 2);
335 painter.drawEllipse(2, top + (fontMetrics().height() - diameter) / 2, diameter, diameter);
336 break;
337 }
338 }
339 block = block.next();
340 top = bottom;
341 bottom = top + qRound(blockBoundingRect(block).height());
342 ++blockNumber;
343 }
344}
345
347
348static QChar matchingBracket(QChar ch) {
349 if (ch == '(')
350 return ')';
351 if (ch == ')')
352 return '(';
353 if (ch == '{')
354 return '}';
355 if (ch == '}')
356 return '{';
357 if (ch == '[')
358 return ']';
359 if (ch == ']')
360 return '[';
361 return QChar();
362}
363
364static bool isOpenBracket(QChar ch) { return ch == '(' || ch == '{' || ch == '['; }
365
367 QList<QTextEdit::ExtraSelection> selections;
368
369 // Keep current line highlight
370 QTextEdit::ExtraSelection lineSelection;
371 lineSelection.format.setBackground(currentLineBackground(palette()));
372 lineSelection.format.setProperty(QTextFormat::FullWidthSelection, true);
373 lineSelection.cursor = textCursor();
374 lineSelection.cursor.clearSelection();
375 selections.append(lineSelection);
376
377 QTextCursor cursor = textCursor();
378 QTextDocument *doc = document();
379 int pos = cursor.position();
380
381 auto tryMatch = [&](int checkPos) -> bool {
382 if (checkPos < 0 || checkPos >= doc->characterCount())
383 return false;
384
385 QChar ch = doc->characterAt(checkPos);
386 QChar match = matchingBracket(ch);
387 if (match.isNull())
388 return false;
389
390 bool forward = isOpenBracket(ch);
391 int depth = 1;
392 int i = checkPos + (forward ? 1 : -1);
393
394 while (i >= 0 && i < doc->characterCount() && depth > 0) {
395 QChar c = doc->characterAt(i);
396 if (c == ch)
397 ++depth;
398 else if (c == match)
399 --depth;
400 if (forward)
401 ++i;
402 else
403 --i;
404 }
405
406 if (depth == 0) {
407 int matchPos = forward ? i - 1 : i + 1;
408
409 QTextEdit::ExtraSelection sel1;
410 sel1.format.setBackground(QColor(80, 80, 120));
411 sel1.cursor = QTextCursor(doc);
412 sel1.cursor.setPosition(checkPos);
413 sel1.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
414 selections.append(sel1);
415
416 QTextEdit::ExtraSelection sel2;
417 sel2.format.setBackground(QColor(80, 80, 120));
418 sel2.cursor = QTextCursor(doc);
419 sel2.cursor.setPosition(matchPos);
420 sel2.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
421 selections.append(sel2);
422 return true;
423 }
424 return false;
425 };
426
427 if (!tryMatch(pos))
428 tryMatch(pos - 1);
429
430 for (const ShaderDiagnostic &diagnostic : m_diagnostics) {
431 const QTextBlock block = doc->findBlockByLineNumber(qMax(0, diagnostic.line - 1));
432 if (!block.isValid())
433 continue;
434
435 const int lineLength = qMax(0, block.length() - 1);
436 const int column = qBound(0, diagnostic.column, lineLength);
437 QTextEdit::ExtraSelection diagnosticSelection;
438 diagnosticSelection.cursor = QTextCursor(block);
439 diagnosticSelection.cursor.setPosition(block.position() + column);
440 diagnosticSelection.cursor.setPosition(block.position() + qMin(lineLength, column + qMax(1, lineLength - column)), QTextCursor::KeepAnchor);
441 diagnosticSelection.format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
442 diagnosticSelection.format.setUnderlineColor(diagnosticColor(diagnostic.severity));
443 diagnosticSelection.format.setToolTip(diagnostic.message);
444 selections.append(diagnosticSelection);
445 }
446
447 setExtraSelections(selections);
448}
449
451 QTextCursor cursor = textCursor();
452 if (!cursor.hasSelection())
453 return false;
454 int startBlock = document()->findBlock(cursor.selectionStart()).blockNumber();
455 int endBlock = document()->findBlock(cursor.selectionEnd()).blockNumber();
456 return startBlock != endBlock;
457}
458
460 QTextCursor cursor = textCursor();
461 int start = cursor.selectionStart();
462 int end = cursor.selectionEnd();
463
464 QTextBlock startBlock = document()->findBlock(start);
465 QTextBlock endBlock = document()->findBlock(end);
466 if (end > start && endBlock.position() == end)
467 endBlock = endBlock.previous();
468
469 cursor.beginEditBlock();
470 QTextBlock block = startBlock;
471 while (block.isValid() && block.blockNumber() <= endBlock.blockNumber()) {
472 QTextCursor blockCursor(block);
473 blockCursor.movePosition(QTextCursor::StartOfBlock);
474 blockCursor.insertText(" ");
475 block = block.next();
476 }
477 cursor.endEditBlock();
478}
479
481 QTextCursor cursor = textCursor();
482 int start = cursor.selectionStart();
483 int end = cursor.selectionEnd();
484
485 QTextBlock startBlock = document()->findBlock(start);
486 QTextBlock endBlock = document()->findBlock(end);
487 if (end > start && endBlock.position() == end)
488 endBlock = endBlock.previous();
489
490 cursor.beginEditBlock();
491 QTextBlock block = startBlock;
492 while (block.isValid() && block.blockNumber() <= endBlock.blockNumber()) {
493 QString text = block.text();
494 int spaces = 0;
495 for (auto ch : text) {
496 if (ch == ' ' && spaces < 4)
497 ++spaces;
498 else
499 break;
500 }
501 if (spaces > 0) {
502 QTextCursor blockCursor(block);
503 blockCursor.movePosition(QTextCursor::StartOfBlock);
504 blockCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, spaces);
505 blockCursor.removeSelectedText();
506 }
507 block = block.next();
508 }
509 cursor.endEditBlock();
510}
511
513 QTextCursor cursor = textCursor();
514 QString blockText = cursor.block().text();
515 int col = cursor.positionInBlock();
516
517 QString beforeCursor = blockText.left(col);
518
519 // Compute leading whitespace
520 QString indent;
521 for (auto ch : beforeCursor) {
522 if (ch == ' ' || ch == '\t')
523 indent += ch;
524 else
525 break;
526 }
527
528 QString trimmed = beforeCursor.trimmed();
529 bool afterOpenBrace = trimmed.endsWith('{');
530
531 QString afterCursor = blockText.mid(col);
532 bool beforeCloseBrace = afterCursor.trimmed().startsWith('}');
533
534 cursor.beginEditBlock();
535 cursor.insertText("\n");
536
537 if (afterOpenBrace && beforeCloseBrace) {
538 // Between {} — create indented block
539 cursor.insertText(indent + " ");
540 int cursorPos = cursor.position();
541 cursor.insertText("\n" + indent);
542 cursor.setPosition(cursorPos);
543 setTextCursor(cursor);
544 } else if (afterOpenBrace) {
545 cursor.insertText(indent + " ");
546 } else {
547 cursor.insertText(indent);
548 }
549 cursor.endEditBlock();
550
551 if (!(afterOpenBrace && beforeCloseBrace))
552 setTextCursor(cursor);
553
554 ensureCursorVisible();
555}
556
558 QTextCursor cursor = textCursor();
559 cursor.beginEditBlock();
560 cursor.movePosition(QTextCursor::StartOfBlock);
561 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
562 QString lineText = cursor.selectedText();
563 cursor.movePosition(QTextCursor::EndOfBlock);
564 cursor.insertText("\n" + lineText);
565 cursor.endEditBlock();
566 setTextCursor(cursor);
567}
568
570 QTextCursor cursor = textCursor();
571 if (cursor.blockNumber() == 0)
572 return;
573
574 cursor.beginEditBlock();
575 cursor.movePosition(QTextCursor::StartOfBlock);
576 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
577 QString currentLine = cursor.selectedText();
578
579 // Select the preceding newline + entire current line
580 cursor.movePosition(QTextCursor::StartOfBlock);
581 cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor);
582 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
583 cursor.removeSelectedText();
584
585 // Insert above
586 cursor.movePosition(QTextCursor::StartOfBlock);
587 cursor.insertText(currentLine + "\n");
588 cursor.movePosition(QTextCursor::Up);
589 cursor.endEditBlock();
590 setTextCursor(cursor);
591}
592
594 QTextCursor cursor = textCursor();
595 if (cursor.blockNumber() >= document()->blockCount() - 1)
596 return;
597
598 cursor.beginEditBlock();
599 cursor.movePosition(QTextCursor::StartOfBlock);
600 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
601 QString currentLine = cursor.selectedText();
602
603 // Select current line + trailing newline
604 cursor.movePosition(QTextCursor::StartOfBlock);
605 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
606 cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
607 cursor.removeSelectedText();
608
609 // Insert below current position
610 cursor.movePosition(QTextCursor::EndOfBlock);
611 cursor.insertText("\n" + currentLine);
612 cursor.endEditBlock();
613 setTextCursor(cursor);
614}
615
617 QTextCursor cursor = textCursor();
618 int start = cursor.selectionStart();
619 int end = cursor.selectionEnd();
620
621 QTextBlock startBlock = document()->findBlock(start);
622 QTextBlock endBlock = document()->findBlock(end);
623 if (!cursor.hasSelection())
624 endBlock = startBlock;
625 else if (endBlock.position() == end && end > start)
626 endBlock = endBlock.previous();
627
628 // Check if all selected lines are already commented
629 bool allCommented = true;
630 QTextBlock block = startBlock;
631 while (block.isValid() && block.blockNumber() <= endBlock.blockNumber()) {
632 QString trimmed = block.text().trimmed();
633 if (!trimmed.isEmpty() && !trimmed.startsWith("//")) {
634 allCommented = false;
635 break;
636 }
637 block = block.next();
638 }
639
640 cursor.beginEditBlock();
641 block = startBlock;
642 while (block.isValid() && block.blockNumber() <= endBlock.blockNumber()) {
643 QTextCursor blockCursor(block);
644 if (allCommented) {
645 QString text = block.text();
646 int idx = text.indexOf("//");
647 if (idx >= 0) {
648 blockCursor.movePosition(QTextCursor::StartOfBlock);
649 blockCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, idx);
650 int removeLen = 2;
651 if (idx + 2 < text.length() && text[idx + 2] == ' ')
652 removeLen = 3;
653 blockCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, removeLen);
654 blockCursor.removeSelectedText();
655 }
656 } else {
657 blockCursor.movePosition(QTextCursor::StartOfBlock);
658 blockCursor.insertText("// ");
659 }
660 block = block.next();
661 }
662 cursor.endEditBlock();
663}
664
666 QTextCursor cursor = textCursor();
667 QString text = cursor.block().text();
668
669 int firstNonSpace = 0;
670 for (auto ch : text) {
671 if (ch == ' ' || ch == '\t')
672 ++firstNonSpace;
673 else
674 break;
675 }
676
677 int col = cursor.positionInBlock();
678 auto mode = shift ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor;
679
680 if (col == firstNonSpace || firstNonSpace >= text.length()) {
681 cursor.movePosition(QTextCursor::StartOfBlock, mode);
682 } else {
683 cursor.movePosition(QTextCursor::StartOfBlock, mode);
684 cursor.movePosition(QTextCursor::Right, mode, firstNonSpace);
685 }
686 setTextCursor(cursor);
687}
688
689void CustomTextEdit::keyPressEvent(QKeyEvent *event) {
690 if (m_completer->popup()->isVisible()) {
691 switch (event->key()) {
692 case Qt::Key_Enter:
693 case Qt::Key_Return:
694 case Qt::Key_Escape:
695 case Qt::Key_Tab:
696 case Qt::Key_Backtab:
697 event->ignore();
698 return;
699 default:
700 break;
701 }
702 }
703
704 if (event->key() == Qt::Key_Space && event->modifiers() == Qt::ControlModifier) {
706 return;
707 }
708
709 // Ctrl+D: duplicate line
710 if (event->key() == Qt::Key_D && (event->modifiers() & Qt::ControlModifier)) {
712 return;
713 }
714
715 // Alt+Up: move line up
716 if (event->key() == Qt::Key_Up && (event->modifiers() & Qt::AltModifier)) {
717 moveLineUp();
718 return;
719 }
720
721 // Alt+Down: move line down
722 if (event->key() == Qt::Key_Down && (event->modifiers() & Qt::AltModifier)) {
723 moveLineDown();
724 return;
725 }
726
727 // Ctrl+/: toggle comment
728 if (event->key() == Qt::Key_Slash && (event->modifiers() & Qt::ControlModifier)) {
730 return;
731 }
732
733 // Home: smart home (first non-whitespace or column 0)
734 if (event->key() == Qt::Key_Home && !(event->modifiers() & Qt::ControlModifier)) {
735 smartHome(event->modifiers() & Qt::ShiftModifier);
736 return;
737 }
738
739 // Tab: indent selection or insert spaces
740 if (event->key() == Qt::Key_Tab) {
741 if (hasMultiLineSelection()) {
743 } else {
744 QTextCursor cursor = textCursor();
745 cursor.insertText(" ");
746 }
747 return;
748 }
749
750 // Shift+Tab: unindent
751 if (event->key() == Qt::Key_Backtab) {
752 if (hasMultiLineSelection()) {
754 } else {
755 QTextCursor cursor = textCursor();
756 cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
757 QString blockText = cursor.block().text();
758 int spaces = 0;
759 for (auto ch : blockText) {
760 if (ch == ' ' && spaces < 4)
761 ++spaces;
762 else
763 break;
764 }
765 if (spaces > 0) {
766 cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, spaces);
767 cursor.removeSelectedText();
768 setTextCursor(cursor);
769 }
770 }
771 return;
772 }
773
774 // Enter: auto-indent
775 if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
777 return;
778 }
779
780 // Remove an empty auto-created pair with one Backspace press.
781 if (event->key() == Qt::Key_Backspace && event->modifiers() == Qt::NoModifier) {
782 QTextCursor cursor = textCursor();
783 if (!cursor.hasSelection() && cursor.position() > 0) {
784 const QChar previous = document()->characterAt(cursor.position() - 1);
785 const QChar next = document()->characterAt(cursor.position());
786 const bool isPair = (isOpenBracket(previous) && matchingBracket(previous) == next) || ((previous == '"' || previous == '\'') && previous == next);
787 if (isPair) {
788 cursor.beginEditBlock();
789 cursor.deletePreviousChar();
790 cursor.deleteChar();
791 cursor.endEditBlock();
792 return;
793 }
794 }
795 }
796
797 const auto insertPair = [this](QChar opening, QChar closing) {
798 QTextCursor cursor = textCursor();
799 if (cursor.hasSelection()) {
800 const int selectionStart = cursor.selectionStart();
801 QString selectedText = cursor.selectedText();
802 selectedText.replace(QChar::ParagraphSeparator, '\n');
803 cursor.insertText(QString(opening) + selectedText + QString(closing));
804 cursor.setPosition(selectionStart + 1);
805 cursor.setPosition(selectionStart + 1 + selectedText.size(), QTextCursor::KeepAnchor);
806 } else {
807 cursor.insertText(QString(opening) + QString(closing));
808 cursor.movePosition(QTextCursor::Left);
809 }
810 setTextCursor(cursor);
811 };
812
813 // Auto-close brackets
814 if (event->text() == "{") {
815 insertPair('{', '}');
816 return;
817 }
818 if (event->text() == "(") {
819 insertPair('(', ')');
820 return;
821 }
822 if (event->text() == "[") {
823 insertPair('[', ']');
824 return;
825 }
826
827 // Skip-over closing brackets if the next char matches
828 if (event->text() == "}" || event->text() == ")" || event->text() == "]") {
829 QTextCursor cursor = textCursor();
830 QChar nextChar = document()->characterAt(cursor.position());
831 if (nextChar == event->text().at(0)) {
832 cursor.movePosition(QTextCursor::Right);
833 setTextCursor(cursor);
834 return;
835 }
836 }
837
838 // Auto-close/skip-over quotes
839 if (event->text() == "\"" || event->text() == "'") {
840 QTextCursor cursor = textCursor();
841 QChar quote = event->text().at(0);
842 if (cursor.hasSelection()) {
843 insertPair(quote, quote);
844 return;
845 }
846 QChar nextChar = document()->characterAt(cursor.position());
847 if (nextChar == quote) {
848 cursor.movePosition(QTextCursor::Right);
849 setTextCursor(cursor);
850 return;
851 }
852 cursor.insertText(QString(quote) + QString(quote));
853 cursor.movePosition(QTextCursor::Left);
854 setTextCursor(cursor);
855 return;
856 }
857
858 QPlainTextEdit::keyPressEvent(event);
859 const bool canComplete = event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::ShiftModifier;
860 if (canComplete && (!event->text().isEmpty() || event->key() == Qt::Key_Backspace)) {
861 showCompletionPopup(false);
862 } else {
863 m_completer->popup()->hide();
864 }
865}
866
867void CustomTextEdit::mousePressEvent(QMouseEvent *event) {
868 if (event->button() == Qt::LeftButton && (event->modifiers() & Qt::ControlModifier)) {
869 const QTextCursor cursor = cursorForPosition(event->pos());
870 const QString line = cursor.block().text();
871 const QRegularExpression includeExpression(QStringLiteral("^\\s*#\\s*include\\s*[<\"]([^>\"]+)[>\"]"));
872 const QRegularExpressionMatch match = includeExpression.match(line);
873 if (match.hasMatch()) {
874 const int column = cursor.positionInBlock();
875 const int start = match.capturedStart(1);
876 const int end = match.capturedEnd(1);
877 if (column >= start && column <= end) {
878 emit includeRequested(match.captured(1));
879 event->accept();
880 return;
881 }
882 }
883 }
884 QPlainTextEdit::mousePressEvent(event);
885}
886
887TextEditor::TextEditor(QWidget *parent) : QDialog(parent), m_modified(false), m_textEdit(nullptr), m_highlighter(nullptr), m_statusBar(nullptr), m_lineColLabel(nullptr), m_fontSize(24) { init(); }
888
889void TextEditor::setText(const QString &text) {
890 m_textEdit->setPlainText(text);
891 m_textEdit->document()->setModified(false);
892 m_modified = false;
895}
896
897void TextEditor::setFileName(const QString &filen) {
898 filename = filen;
900}
901
902QString TextEditor::fileName() const { return filename; }
903
904void TextEditor::revealLocation(int lineNumber, int columnNumber, int matchLength) {
905 const QTextBlock block = m_textEdit->document()->findBlockByLineNumber(qMax(0, lineNumber - 1));
906 if (!block.isValid())
907 return;
908
909 const int lineLength = qMax(0, block.length() - 1);
910 const int column = qBound(0, columnNumber, lineLength);
911 const int selectionLength = qBound(0, matchLength, lineLength - column);
912 QTextCursor cursor(block);
913 cursor.setPosition(block.position() + column);
914 if (selectionLength > 0) {
915 cursor.setPosition(block.position() + column + selectionLength, QTextCursor::KeepAnchor);
916 }
917 m_textEdit->setTextCursor(cursor);
918 m_textEdit->centerCursor();
919 m_textEdit->setFocus();
920}
921
923 m_diagnostics.clear();
927 m_compileStatusLabel->setText("Compiling...");
928 m_compileStatusLabel->setStyleSheet("color: #e5b84b;");
929 m_compileStatusLabel->setToolTip(QString());
930}
931
932void TextEditor::setCompileResult(bool success, const QString &compilerOutput) {
933 m_diagnostics = parseDiagnostics(compilerOutput);
937
938 int errors = 0;
939 int warnings = 0;
940 for (const ShaderDiagnostic &diagnostic : m_diagnostics) {
941 errors += diagnostic.severity == ShaderDiagnosticSeverity::Error;
942 warnings += diagnostic.severity == ShaderDiagnosticSeverity::Warning;
943 }
944 if (success) {
945 m_compileStatusLabel->setText(warnings > 0 ? QString("Compiled successfully · %1 warning(s)").arg(warnings) : QStringLiteral("Compiled successfully"));
946 m_compileStatusLabel->setStyleSheet(warnings > 0 ? "color: #e5b84b;" : "color: #55c878;");
947 m_compileStatusLabel->setToolTip(compilerOutput.left(8192));
948 m_statusBar->showMessage(warnings > 0 ? "Shader applied; press F8 to review warnings" : "Shader compiled and applied", 3000);
949 return;
950 }
951 QString status = "Compile failed";
952 if (!m_diagnostics.isEmpty()) {
953 status += QString(" · %1 error(s), %2 warning(s)").arg(errors).arg(warnings);
954 }
955 m_compileStatusLabel->setText(status);
956 m_compileStatusLabel->setStyleSheet("color: #e14b4b;");
957 m_compileStatusLabel->setToolTip(compilerOutput.left(8192));
958 m_statusBar->showMessage(m_diagnostics.isEmpty() ? "Compilation failed; see the ACMX log for compiler output" : "Press F8 to visit the first compiler diagnostic", 5000);
959}
960
961void TextEditor::setShaderContext(bool acmxvk, const QVector<ShaderEditorUniform> &uniforms, const QString &libraryDirectory) {
963 m_uniforms = uniforms;
964 m_libraryDirectory = libraryDirectory;
965 m_snippetsMenu->setEnabled(acmxvk);
966 m_livePreviewCheck->setEnabled(!libraryDirectory.isEmpty());
967 m_loopSafetyCheck->setEnabled(acmxvk && !libraryDirectory.isEmpty());
970}
971
972void TextEditor::setUniformValue(const QString &name, double value) {
973 for (ShaderEditorUniform &uniform : m_uniforms) {
974 if (uniform.name != name)
975 continue;
976 uniform.value = qBound(uniform.minimum, value, uniform.maximum);
977 if (QDoubleSpinBox *spin = m_uniformSpins.value(name)) {
978 const QSignalBlocker blocker(spin);
979 spin->setValue(uniform.value);
980 }
981 if (QSlider *slider = m_uniformSliders.value(name)) {
982 const QSignalBlocker blocker(slider);
983 const double range = uniform.maximum - uniform.minimum;
984 const int position = range > 0.0 ? qRound((uniform.value - uniform.minimum) / range * slider->maximum()) : 0;
985 slider->setValue(position);
986 }
987 break;
988 }
989}
990
991void TextEditor::openInclude(const QString &includeName) {
992 const QFileInfo sourceInfo(filename);
993 const QStringList candidates{sourceInfo.dir().filePath(includeName), QDir(m_libraryDirectory).filePath(includeName)};
994 for (const QString &candidate : candidates) {
995 const QFileInfo includeInfo(candidate);
996 if (includeInfo.exists() && includeInfo.isFile()) {
997 emit openFileRequested(includeInfo.canonicalFilePath(), 1);
998 return;
999 }
1000 }
1001 m_statusBar->showMessage(QStringLiteral("Include file not found: %1").arg(includeName), 4000);
1002}
1003
1005 if (filename.isEmpty() || m_libraryDirectory.isEmpty())
1006 return;
1007 QString source = m_textEdit->toPlainText();
1008 if (m_acmxvkContext && m_loopSafetyCheck->isChecked())
1009 source = inject_safety_counters(source);
1010 emit previewRequested(filename, source);
1011}
1012
1014 if (filename.isEmpty())
1015 return;
1016 QFile file(filename);
1017 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1018 QMessageBox::warning(this, tr("Revert Shader"), file.errorString());
1019 return;
1020 }
1021 setText(QString::fromUtf8(file.readAll()));
1022 m_statusBar->showMessage(tr("Restored the saved shader"), 2500);
1023}
1024
1026 m_uniformSpins.clear();
1027 m_uniformSliders.clear();
1028 while (QLayoutItem *item = m_uniformLayout->takeAt(0)) {
1029 if (QWidget *widget = item->widget())
1030 widget->deleteLater();
1031 delete item;
1032 }
1033
1034 const bool visible = m_acmxvkContext && !m_uniforms.isEmpty();
1035 m_uniformPanel->setVisible(visible);
1036 m_uniformScroll->setVisible(visible);
1037 if (!visible)
1038 return;
1039
1040 auto *heading = new QLabel(tr("Custom Uniforms"), m_uniformPanel);
1041 QFont headingFont = heading->font();
1042 headingFont.setBold(true);
1043 heading->setFont(headingFont);
1044 m_uniformLayout->addWidget(heading);
1045
1046 for (const ShaderEditorUniform &uniform : m_uniforms) {
1047 auto *nameLabel = new QLabel(uniform.name, m_uniformPanel);
1048 if (uniform.slot >= 0) {
1049 nameLabel->setToolTip(QStringLiteral("ext.custom_uniforms[%1].%2").arg(uniform.slot / 4).arg(QStringLiteral("xyzw").at(uniform.slot % 4)));
1050 }
1051 auto *slider = new QSlider(Qt::Horizontal, m_uniformPanel);
1052 slider->setRange(0, 1000);
1053 auto *spin = new QDoubleSpinBox(m_uniformPanel);
1054 spin->setDecimals(8);
1055 spin->setRange(uniform.minimum, uniform.maximum);
1056 spin->setSingleStep(uniform.step);
1057 spin->setValue(uniform.value);
1058 const double range = uniform.maximum - uniform.minimum;
1059 slider->setValue(range > 0.0 ? qRound((uniform.value - uniform.minimum) / range * slider->maximum()) : 0);
1060 m_uniformLayout->addWidget(nameLabel);
1061 m_uniformLayout->addWidget(slider);
1062 m_uniformLayout->addWidget(spin);
1063 m_uniformSpins.insert(uniform.name, spin);
1064 m_uniformSliders.insert(uniform.name, slider);
1065
1066 connect(slider, &QSlider::valueChanged, this, [this, uniform, spin, slider](int position) {
1067 const double ratio = slider->maximum() > 0 ? static_cast<double>(position) / slider->maximum() : 0.0;
1068 const double raw = uniform.minimum + (uniform.maximum - uniform.minimum) * ratio;
1069 const double steps = uniform.step > 0.0 ? qRound((raw - uniform.minimum) / uniform.step) : 0.0;
1070 const double value = qBound(uniform.minimum, uniform.minimum + steps * uniform.step, uniform.maximum);
1071 const QSignalBlocker blocker(spin);
1072 spin->setValue(value);
1073 emit uniformValueChanged(uniform.name, value);
1074 });
1075 connect(spin, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, [this, uniform, slider](double value) {
1076 const double range = uniform.maximum - uniform.minimum;
1077 const int position = range > 0.0 ? qRound((value - uniform.minimum) / range * slider->maximum()) : 0;
1078 const QSignalBlocker blocker(slider);
1079 slider->setValue(position);
1080 emit uniformValueChanged(uniform.name, value);
1081 });
1082 }
1083 m_uniformLayout->addStretch();
1084}
1085
1087 QStringList words{"break", "case", "const", "continue", "default", "discard", "do", "else", "false", "for", "if", "in", "inout", "layout", "out", "precision", "return", "struct", "switch", "true", "uniform", "while", "bool", "int", "uint", "float", "double", "vec2", "vec3", "vec4", "ivec2", "ivec3", "ivec4", "uvec2", "uvec3", "uvec4", "bvec2", "bvec3", "bvec4", "mat2", "mat3", "mat4", "sampler1D", "sampler2D", "sampler2DArray", "samplerCube", "image2D", "abs", "acos", "all", "any", "asin", "atan", "ceil", "clamp", "cos", "cross", "degrees", "distance", "dot", "exp", "exp2", "floor", "fract", "imageLoad", "imageSize", "imageStore", "length", "log", "log2", "max", "min", "mix", "mod", "normalize", "pow", "radians", "reflect", "refract", "round", "sign", "sin", "smoothstep", "sqrt", "step", "tan", "texelFetch", "texture", "textureSize", "transpose"};
1088
1089 if (m_acmxvkContext) {
1090 words << "input_image"
1091 << "output_image"
1092 << "history"
1093 << "spectrum"
1094 << "spectrum_history"
1095 << "ext.mouse"
1096 << "ext.u0"
1097 << "ext.u1"
1098 << "ext.u2"
1099 << "ext.u3"
1100 << "ext.custom_uniforms"
1101 << "ext.audio_bands"
1102 << "ext.audio_history"
1103 << "pc.screen_width"
1104 << "pc.screen_height"
1105 << "pc.effects_on"
1106 << "pc.rotation_degrees"
1107 << "pc.params"
1108 << "iResolution"
1109 << "iTime"
1110 << "iTimeDelta"
1111 << "iFrame"
1112 << "amp_low"
1113 << "amp_mid"
1114 << "amp_high"
1115 << "gl_GlobalInvocationID"
1116 << "gl_LocalInvocationID"
1117 << "gl_LocalInvocationIndex"
1118 << "gl_NumWorkGroups"
1119 << "gl_WorkGroupID"
1120 << "gl_WorkGroupSize"
1121 << "gl_FragCoord";
1122 for (const ShaderEditorUniform &uniform : m_uniforms)
1123 words.append(uniform.name);
1124 }
1125
1126 const QString source = m_textEdit->toPlainText();
1127 const QRegularExpression declarations(QStringLiteral("(?:#\\s*define|\\b(?:void|bool|int|uint|float|double|"
1128 "[biu]?vec[234]|mat[234])\\s+)\\s*"
1129 "([A-Za-z_][A-Za-z0-9_]*)"));
1130 QRegularExpressionMatchIterator matches = declarations.globalMatch(source);
1131 while (matches.hasNext())
1132 words.append(matches.next().captured(1));
1134}
1135
1137 QString defines;
1138 static const QString components = QStringLiteral("xyzw");
1139 for (int index = 0; index < m_uniforms.size(); ++index) {
1140 const ShaderEditorUniform &uniform = m_uniforms[index];
1141 const int slot = uniform.slot >= 0 ? uniform.slot : index;
1142 defines += QString("#define %1 ext.custom_uniforms[%2].%3\n").arg(uniform.name).arg(slot / 4).arg(components.at(slot % 4));
1143 }
1144 return defines;
1145}
1146
1147void TextEditor::insertSnippet(const QString &snippet) {
1148 if (snippet.isEmpty()) {
1149 m_statusBar->showMessage("The active library has no custom uniforms", 3000);
1150 return;
1151 }
1152 QTextCursor cursor = m_textEdit->textCursor();
1153 QString insertion = snippet;
1154 if (cursor.positionInBlock() != 0)
1155 insertion.prepend(QLatin1Char('\n'));
1156 if (!insertion.endsWith(QLatin1Char('\n')))
1157 insertion.append(QLatin1Char('\n'));
1158 cursor.insertText(insertion);
1159 m_textEdit->setTextCursor(cursor);
1160 m_textEdit->setFocus();
1162}
1163
1164QVector<ShaderDiagnostic> TextEditor::parseDiagnostics(const QString &compilerOutput) const {
1165 QVector<ShaderDiagnostic> diagnostics;
1166 const QFileInfo editedFile(filename);
1167 const QString editedCanonical = editedFile.canonicalFilePath();
1168 const QRegularExpression withColumn(QStringLiteral("^(.+):(\\d+):(\\d+):\\s*"
1169 "(?:(error|warning|note)\\s*:\\s*)?(.*)$"),
1170 QRegularExpression::CaseInsensitiveOption);
1171 const QRegularExpression withoutColumn(QStringLiteral("^(.+):(\\d+):\\s*"
1172 "(?:(error|warning|note)\\s*:\\s*)?(.*)$"),
1173 QRegularExpression::CaseInsensitiveOption);
1174
1175 const QStringList lines = compilerOutput.split(QLatin1Char('\n'));
1176 for (const QString &line : lines) {
1177 QRegularExpressionMatch match = withColumn.match(line.trimmed());
1178 const bool hasColumn = match.hasMatch();
1179 if (!hasColumn)
1180 match = withoutColumn.match(line.trimmed());
1181 if (!match.hasMatch())
1182 continue;
1183
1184 QString reportedPath = match.captured(1).trimmed();
1185 if (reportedPath.startsWith(QLatin1Char('"')) && reportedPath.endsWith(QLatin1Char('"'))) {
1186 reportedPath = reportedPath.mid(1, reportedPath.size() - 2);
1187 }
1188 QFileInfo reportedFile(reportedPath);
1189 if (reportedFile.isRelative()) {
1190 reportedFile = QFileInfo(editedFile.absoluteDir(), reportedPath);
1191 }
1192 const QString reportedCanonical = reportedFile.canonicalFilePath();
1193 const bool sameFile = (!editedCanonical.isEmpty() && !reportedCanonical.isEmpty() && editedCanonical == reportedCanonical) || QFileInfo(reportedPath).fileName() == editedFile.fileName();
1194 if (!sameFile)
1195 continue;
1196
1197 ShaderDiagnostic diagnostic;
1198 diagnostic.line = qMax(1, match.captured(2).toInt());
1199 diagnostic.column = hasColumn ? qMax(0, match.captured(3).toInt() - 1) : 0;
1200 const QString severity = match.captured(hasColumn ? 4 : 3).toLower();
1201 if (severity == QStringLiteral("warning"))
1203 else if (severity == QStringLiteral("note"))
1205 const QString detail = match.captured(hasColumn ? 5 : 4).trimmed();
1206 diagnostic.message = detail.isEmpty() ? line.trimmed() : detail;
1207 diagnostics.append(diagnostic);
1208 }
1209 return diagnostics;
1210}
1211
1213 if (m_diagnostics.isEmpty())
1214 return;
1215 if (m_currentDiagnostic < 0)
1216 m_currentDiagnostic = offset < 0 ? m_diagnostics.size() - 1 : 0;
1217 else
1220 revealLocation(diagnostic.line, diagnostic.column, 1);
1221 m_statusBar->showMessage(QString("Diagnostic %1 of %2: %3").arg(m_currentDiagnostic + 1).arg(m_diagnostics.size()).arg(diagnostic.message));
1222}
1223
1225 const bool available = !m_diagnostics.isEmpty();
1226 m_nextDiagnosticAction->setEnabled(available);
1227 m_previousDiagnosticAction->setEnabled(available);
1228}
1229
1231 QString title = "ACMX2";
1232 if (!filename.isEmpty()) {
1233 title += " - " + QFileInfo(filename).fileName();
1234 }
1235 if (m_modified) {
1236 title += " *";
1237 }
1238 setWindowTitle(title);
1239}
1240
1242 m_modified = false;
1243 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1244 QSettings editorSettings("LostSideDead");
1245 m_fontSize = qBound(8, editorSettings.value("editor/fontSize", 24).toInt(), 72);
1247
1248 QVBoxLayout *layout = new QVBoxLayout(this);
1249
1250 QMenuBar *menuBar = new QMenuBar(this);
1251#ifdef Q_OS_MACOS
1252 // Keep editor actions attached to each editor window, matching the main
1253 // interface, instead of moving them into macOS's global menu bar.
1254 menuBar->setNativeMenuBar(false);
1255#endif
1256
1257 QMenu *fileMenu = menuBar->addMenu("&File");
1258
1259 QAction *saveAction = fileMenu->addAction("&Save");
1260 saveAction->setShortcut(QKeySequence::Save);
1261
1262 QAction *saveAsAction = fileMenu->addAction("Save &As...");
1263 saveAsAction->setShortcut(QKeySequence::SaveAs);
1264
1265 fileMenu->addSeparator();
1266
1267 QAction *closeAction = fileMenu->addAction("&Close");
1268 closeAction->setShortcut(QKeySequence::Close);
1269
1270 QMenu *editMenu = menuBar->addMenu("&Edit");
1271
1272 QAction *undoAction = editMenu->addAction("&Undo");
1273 undoAction->setShortcut(QKeySequence::Undo);
1274
1275 QAction *redoAction = editMenu->addAction("&Redo");
1276 redoAction->setShortcut(QKeySequence::Redo);
1277
1278 editMenu->addSeparator();
1279
1280 QAction *cutAction = editMenu->addAction("Cu&t");
1281 cutAction->setShortcut(QKeySequence::Cut);
1282
1283 QAction *copyAction = editMenu->addAction("&Copy");
1284 copyAction->setShortcut(QKeySequence::Copy);
1285
1286 QAction *pasteAction = editMenu->addAction("&Paste");
1287 pasteAction->setShortcut(QKeySequence::Paste);
1288
1289 editMenu->addSeparator();
1290
1291 QAction *selectAllAction = editMenu->addAction("Select &All");
1292 selectAllAction->setShortcut(QKeySequence::SelectAll);
1293
1294 editMenu->addSeparator();
1295
1296 QAction *findAction = editMenu->addAction("&Find...");
1297 findAction->setShortcut(QKeySequence::Find);
1298
1299 QAction *findNextAction = editMenu->addAction("Find &Next");
1300 findNextAction->setShortcut(QKeySequence::FindNext);
1301
1302 QAction *findPrevAction = editMenu->addAction("Find Pre&vious");
1303 findPrevAction->setShortcut(QKeySequence::FindPrevious);
1304
1305 QAction *replaceAction = editMenu->addAction("&Replace...");
1306 replaceAction->setShortcut(QKeySequence::Replace);
1307
1308 editMenu->addSeparator();
1309
1310 QAction *gotoLineAction = editMenu->addAction("&Go to Line...");
1311 gotoLineAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_G));
1312
1313 editMenu->addSeparator();
1314
1315 QAction *duplicateAction = editMenu->addAction("&Duplicate Line");
1316 duplicateAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D));
1317
1318 QAction *toggleCommentAction = editMenu->addAction("Toggle Co&mment");
1319 toggleCommentAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Slash));
1320
1321 QAction *moveUpAction = editMenu->addAction("Move Line &Up");
1322 moveUpAction->setShortcut(QKeySequence(Qt::ALT | Qt::Key_Up));
1323
1324 QAction *moveDownAction = editMenu->addAction("Move Line Dow&n");
1325 moveDownAction->setShortcut(QKeySequence(Qt::ALT | Qt::Key_Down));
1326
1327 editMenu->addSeparator();
1328
1329 QAction *shiftRightAction = editMenu->addAction("Shift &Right");
1330 shiftRightAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_BracketRight));
1331
1332 QAction *shiftLeftAction = editMenu->addAction("Shift &Left");
1333 shiftLeftAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_BracketLeft));
1334
1335 QMenu *diagnosticsMenu = menuBar->addMenu("&Diagnostics");
1336 m_nextDiagnosticAction = diagnosticsMenu->addAction("&Next Diagnostic");
1337 m_nextDiagnosticAction->setShortcut(QKeySequence(Qt::Key_F8));
1338 m_previousDiagnosticAction = diagnosticsMenu->addAction("&Previous Diagnostic");
1339 m_previousDiagnosticAction->setShortcut(QKeySequence(Qt::SHIFT | Qt::Key_F8));
1340
1341 m_snippetsMenu = menuBar->addMenu("&Snippets");
1342 QAction *engineStateSnippet = m_snippetsMenu->addAction("Input and Engine State Bindings");
1343 QAction *pushConstantsSnippet = m_snippetsMenu->addAction("Fragment Push Constants");
1344 QAction *historySnippet = m_snippetsMenu->addAction("Frame History Binding");
1345 QAction *audioSnippet = m_snippetsMenu->addAction("FFT and FFT History Bindings");
1346 QAction *uniformSnippet = m_snippetsMenu->addAction("Custom Uniform Defines");
1347 m_snippetsMenu->addSeparator();
1348 QAction *computeMainSnippet = m_snippetsMenu->addAction("Compute Output and Main Function");
1349 m_snippetsMenu->setEnabled(false);
1350
1351 QMenu *viewMenu = menuBar->addMenu("&View");
1352
1353 QAction *increaseFontAction = viewMenu->addAction("Increase Font Size");
1354 increaseFontAction->setShortcut(QKeySequence::ZoomIn);
1355
1356 QAction *decreaseFontAction = viewMenu->addAction("Decrease Font Size");
1357 decreaseFontAction->setShortcut(QKeySequence::ZoomOut);
1358
1359 QAction *resetFontAction = viewMenu->addAction("Reset Font Size");
1360 resetFontAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_0));
1361
1362 viewMenu->addSeparator();
1363
1364 QAction *toggleWordWrapAction = viewMenu->addAction("Word Wrap");
1365 toggleWordWrapAction->setCheckable(true);
1366 toggleWordWrapAction->setChecked(editorSettings.value("editor/wordWrap", false).toBool());
1367
1368 layout->setMenuBar(menuBar);
1369
1370 auto *previewBar = new QHBoxLayout();
1371 auto *previewButton = new QPushButton(tr("Compile Preview"), this);
1372 auto *saveApplyButton = new QPushButton(tr("Save && Apply"), this);
1373 auto *revertButton = new QPushButton(tr("Revert"), this);
1374 for (QPushButton *button : {previewButton, saveApplyButton, revertButton}) {
1375 button->setAutoDefault(false);
1376 button->setDefault(false);
1377 }
1378 m_livePreviewCheck = new QCheckBox(tr("Live Preview"), this);
1379 m_livePreviewCheck->setChecked(editorSettings.value("editor/livePreview", false).toBool());
1380 m_livePreviewCheck->setEnabled(false);
1381 m_loopSafetyCheck = new QCheckBox(tr("Guard Loops"), this);
1382 m_loopSafetyCheck->setChecked(editorSettings.value("editor/guardLoops", true).toBool());
1383 m_loopSafetyCheck->setToolTip(tr("Add a 10,000-iteration break guard to braced ACMXVK for/while loops in temporary preview sources."));
1384 m_loopSafetyCheck->setEnabled(false);
1385 previewBar->addWidget(previewButton);
1386 previewBar->addWidget(saveApplyButton);
1387 previewBar->addWidget(revertButton);
1388 previewBar->addStretch();
1389 previewBar->addWidget(m_loopSafetyCheck);
1390 previewBar->addWidget(m_livePreviewCheck);
1391 layout->addLayout(previewBar);
1392
1393 auto *splitter = new QSplitter(Qt::Horizontal, this);
1394 splitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1395 m_textEdit = new CustomTextEdit(splitter);
1396 m_textEdit->setLineWrapMode(toggleWordWrapAction->isChecked() ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap);
1397 m_textEdit->setTabStopDistance(4 * m_textEdit->fontMetrics().horizontalAdvance(' '));
1399
1400 splitter->addWidget(m_textEdit);
1401
1402 m_uniformScroll = new QScrollArea(splitter);
1403 m_uniformScroll->setWidgetResizable(true);
1404 m_uniformScroll->setMinimumWidth(220);
1405 m_uniformScroll->setMaximumWidth(360);
1406 m_uniformPanel = new QWidget(m_uniformScroll);
1407 m_uniformLayout = new QVBoxLayout(m_uniformPanel);
1408 m_uniformLayout->setAlignment(Qt::AlignTop);
1409 m_uniformScroll->setWidget(m_uniformPanel);
1410 splitter->addWidget(m_uniformScroll);
1411 splitter->setStretchFactor(0, 1);
1412 splitter->setStretchFactor(1, 0);
1413 m_uniformPanel->hide();
1414 m_uniformScroll->hide();
1415 layout->addWidget(splitter, 1);
1416
1417 m_statusBar = new QStatusBar(this);
1418 m_statusBar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
1419 m_lineColLabel = new QLabel("Line: 1, Col: 1", this);
1420 m_compileStatusLabel = new QLabel("Not compiled", this);
1422 m_statusBar->addPermanentWidget(m_lineColLabel);
1423 layout->addWidget(m_statusBar, 0);
1424
1427 connect(m_textEdit, &CustomTextEdit::themeChanged, this, [this]() { m_highlighter->setEditorPalette(m_textEdit->palette()); });
1428
1429 setLayout(layout);
1430 if (!restoreGeometry(editorSettings.value("editor/geometry").toByteArray()))
1431 setGeometry(300, 300, 1024, 768);
1432
1433 saveAction->setEnabled(false);
1434 undoAction->setEnabled(false);
1435 redoAction->setEnabled(false);
1436 cutAction->setEnabled(false);
1437 copyAction->setEnabled(false);
1438 pasteAction->setEnabled(QApplication::clipboard()->mimeData()->hasText());
1439
1440 connect(saveAction, &QAction::triggered, this, &TextEditor::saveContents);
1441 connect(saveAsAction, &QAction::triggered, this, &TextEditor::saveAs);
1442 connect(closeAction, &QAction::triggered, this, &TextEditor::close);
1443 connect(previewButton, &QPushButton::clicked, this, &TextEditor::requestPreview);
1444 connect(saveApplyButton, &QPushButton::clicked, this, &TextEditor::saveContents);
1445 connect(revertButton, &QPushButton::clicked, this, &TextEditor::revertContents);
1446 connect(m_livePreviewCheck, &QCheckBox::toggled, this, [this](bool checked) {
1447 QSettings("LostSideDead").setValue("editor/livePreview", checked);
1448 if (checked)
1449 m_previewTimer->start();
1450 });
1451 connect(m_loopSafetyCheck, &QCheckBox::toggled, this, [](bool checked) { QSettings("LostSideDead").setValue("editor/guardLoops", checked); });
1452
1453 connect(undoAction, &QAction::triggered, m_textEdit, &QPlainTextEdit::undo);
1454 connect(redoAction, &QAction::triggered, m_textEdit, &QPlainTextEdit::redo);
1455 connect(cutAction, &QAction::triggered, m_textEdit, &QPlainTextEdit::cut);
1456 connect(copyAction, &QAction::triggered, m_textEdit, &QPlainTextEdit::copy);
1457 connect(pasteAction, &QAction::triggered, m_textEdit, &QPlainTextEdit::paste);
1458 connect(selectAllAction, &QAction::triggered, m_textEdit, &QPlainTextEdit::selectAll);
1459 connect(duplicateAction, &QAction::triggered, m_textEdit, &CustomTextEdit::duplicateLine);
1460 connect(toggleCommentAction, &QAction::triggered, m_textEdit, &CustomTextEdit::toggleComment);
1461 connect(moveUpAction, &QAction::triggered, m_textEdit, &CustomTextEdit::moveLineUp);
1462 connect(moveDownAction, &QAction::triggered, m_textEdit, &CustomTextEdit::moveLineDown);
1463
1464 connect(findAction, &QAction::triggered, this, &TextEditor::findText);
1465 connect(findNextAction, &QAction::triggered, this, &TextEditor::findNext);
1466 connect(findPrevAction, &QAction::triggered, this, &TextEditor::findPrevious);
1467 connect(replaceAction, &QAction::triggered, this, &TextEditor::replaceText);
1468 connect(gotoLineAction, &QAction::triggered, this, &TextEditor::gotoLine);
1469
1470 connect(increaseFontAction, &QAction::triggered, this, &TextEditor::increaseFontSize);
1471 connect(decreaseFontAction, &QAction::triggered, this, &TextEditor::decreaseFontSize);
1472 connect(resetFontAction, &QAction::triggered, this, &TextEditor::resetFontSize);
1473
1474 connect(toggleWordWrapAction, &QAction::triggered, this, [this](bool checked) {
1475 m_textEdit->setLineWrapMode(checked ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap);
1476 QSettings("LostSideDead").setValue("editor/wordWrap", checked);
1477 });
1478
1479 connect(shiftRightAction, &QAction::triggered, this, [this]() { m_textEdit->indentSelection(); });
1480 connect(shiftLeftAction, &QAction::triggered, this, [this]() { m_textEdit->unindentSelection(); });
1481 connect(m_nextDiagnosticAction, &QAction::triggered, this, [this]() { navigateDiagnostic(1); });
1482 connect(m_previousDiagnosticAction, &QAction::triggered, this, [this]() { navigateDiagnostic(-1); });
1484 connect(engineStateSnippet, &QAction::triggered, this, [this]() {
1485 insertSnippet(QStringLiteral(R"glsl(layout(set = 0, binding = 0) uniform sampler2D input_image;
1486
1487layout(set = 0, binding = 1, std140) uniform SpriteExtended {
1488 vec4 mouse;
1489 vec4 u0;
1490 vec4 u1;
1491 vec4 u2;
1492 vec4 u3;
1493 vec4 custom_uniforms[16];
1494 vec4 audio_bands;
1495 vec4 audio_history;
1496} ext;
1497
1498#define iResolution max(ext.u0.zw, vec2(1.0))
1499#define iTime ext.u2.y
1500#define iTimeDelta ext.u1.x
1501#define iFrame ext.u2.x
1502#define amp_low ext.audio_bands.x
1503#define amp_mid ext.audio_bands.y
1504#define amp_high ext.audio_bands.z)glsl"));
1505 });
1506 connect(pushConstantsSnippet, &QAction::triggered, this, [this]() {
1507 insertSnippet(QStringLiteral(R"glsl(layout(push_constant) uniform SpritePushConstants {
1508 float screen_width;
1509 float screen_height;
1510 float sprite_pos_x;
1511 float sprite_pos_y;
1512 float sprite_size_w;
1513 float sprite_size_h;
1514 float effects_on;
1515 float rotation_degrees;
1516 vec4 params;
1517} pc;)glsl"));
1518 });
1519 connect(historySnippet, &QAction::triggered, this, [this]() { insertSnippet(QStringLiteral("layout(set = 0, binding = 2) uniform sampler2DArray history;")); });
1520 connect(audioSnippet, &QAction::triggered, this, [this]() {
1521 insertSnippet(QStringLiteral(R"glsl(layout(set = 0, binding = 3) uniform sampler1D spectrum;
1522layout(set = 0, binding = 4) uniform sampler1DArray spectrum_history;)glsl"));
1523 });
1524 connect(uniformSnippet, &QAction::triggered, this, [this]() { insertSnippet(customUniformDefines()); });
1525 connect(computeMainSnippet, &QAction::triggered, this, [this]() {
1526 insertSnippet(QStringLiteral(R"glsl(layout(set = 0, binding = 5, rgba8) writeonly uniform image2D output_image;
1527
1528void main() {
1529 ivec2 pixel = ivec2(gl_GlobalInvocationID.xy);
1530 ivec2 size = imageSize(output_image);
1531 if (any(greaterThanEqual(pixel, size))) {
1532 return;
1533 }
1535 vec2 uv = (vec2(pixel) + vec2(0.5)) / vec2(size);
1536 vec4 source_color = texture(input_image, uv);
1537 imageStore(output_image, pixel, source_color);
1538})glsl"));
1539 });
1540
1541 connect(m_textEdit->document(), &QTextDocument::modificationChanged, this, [this, saveAction](bool modified) {
1542 m_modified = modified;
1543 saveAction->setEnabled(modified);
1544 updateWindowTitle();
1545 });
1546 m_previewTimer = new QTimer(this);
1547 m_previewTimer->setSingleShot(true);
1548 m_previewTimer->setInterval(650);
1549 connect(m_previewTimer, &QTimer::timeout, this, &TextEditor::requestPreview);
1550 connect(m_textEdit->document(), &QTextDocument::contentsChanged, this, [this]() {
1551 updateCompletionWords();
1552 if (m_livePreviewCheck->isChecked())
1553 m_previewTimer->start();
1554 });
1556
1557 connect(m_textEdit, &QPlainTextEdit::cursorPositionChanged, this, &TextEditor::updateCursorPosition);
1558 connect(m_textEdit, &QPlainTextEdit::copyAvailable, cutAction, &QAction::setEnabled);
1559 connect(m_textEdit, &QPlainTextEdit::copyAvailable, copyAction, &QAction::setEnabled);
1560 connect(m_textEdit, &QPlainTextEdit::undoAvailable, undoAction, &QAction::setEnabled);
1561 connect(m_textEdit, &QPlainTextEdit::redoAvailable, redoAction, &QAction::setEnabled);
1562 connect(m_textEdit->document(), &QTextDocument::blockCountChanged, this, [this](int) { updateCursorPosition(); });
1563 connect(QApplication::clipboard(), &QClipboard::dataChanged, this, [pasteAction]() { pasteAction->setEnabled(QApplication::clipboard()->mimeData()->hasText()); });
1564 setAttribute(Qt::WA_DeleteOnClose);
1565}
1566
1568 if (filename.isEmpty()) {
1569 saveAs();
1570 return;
1571 }
1572
1574}
1576bool TextEditor::writeFile(const QString &filePath) {
1577 QSaveFile file(filePath);
1578 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
1579 QMessageBox::warning(this, "Error", "Could not save file: " + filePath + "\n\n" + file.errorString());
1580 return false;
1581 }
1582
1583 QTextStream out(&file);
1584 out << m_textEdit->toPlainText();
1585 out.flush();
1586 if (out.status() != QTextStream::Ok) {
1587 file.cancelWriting();
1588 QMessageBox::warning(this, "Error", "Could not write file: " + filePath);
1589 return false;
1590 }
1591 if (!file.commit()) {
1592 QMessageBox::warning(this, "Error", "Could not finish saving file: " + filePath + "\n\n" + file.errorString());
1593 return false;
1594 }
1595
1596 filename = filePath;
1597 m_textEdit->document()->setModified(false);
1598 m_modified = false;
1600 m_statusBar->showMessage("Saved " + QFileInfo(filename).fileName(), 2000);
1601 emit fileSaved(filename);
1602 return true;
1603}
1604
1605void TextEditor::saveAs() {
1606 QSettings appSettings("LostSideDead");
1607 QString lastDir = appSettings.value("lastEditorSaveDir", QFileInfo(filename).absolutePath()).toString();
1608 QString newFileName = QFileDialog::getSaveFileName(this, "Save File As", lastDir + "/" + QFileInfo(filename).fileName(), "GLSL Files (*.glsl *.frag *.vert *.comp);;All Files (*)");
1609
1610 if (!newFileName.isEmpty()) {
1611 if (writeFile(newFileName)) {
1612 appSettings.setValue("lastEditorSaveDir", QFileInfo(newFileName).absolutePath());
1613 }
1615}
1616
1617void TextEditor::findText() {
1618 bool ok;
1619 QString initialText = m_lastSearchText;
1620 const QString selectedText = m_textEdit->textCursor().selectedText();
1621 if (!selectedText.isEmpty() && !selectedText.contains(QChar::ParagraphSeparator))
1622 initialText = selectedText;
1623 QString searchText = QInputDialog::getText(this, "Find", "Enter text to find:", QLineEdit::Normal, initialText, &ok);
1624 if (ok && !searchText.isEmpty()) {
1625 m_lastSearchText = searchText;
1626 findNext();
1627 }
1628}
1629
1630void TextEditor::findNext() {
1631 if (m_lastSearchText.isEmpty()) {
1632 findText();
1633 return;
1634 }
1635
1636 QTextCursor cursor = m_textEdit->textCursor();
1637 QTextDocument::FindFlags flags;
1638
1639 QTextCursor found = m_textEdit->document()->find(m_lastSearchText, cursor, flags);
1640
1641 if (found.isNull()) {
1642
1643 cursor.movePosition(QTextCursor::Start);
1644 found = m_textEdit->document()->find(m_lastSearchText, cursor, flags);
1645
1646 if (found.isNull()) {
1647 m_statusBar->showMessage("Text not found: " + m_lastSearchText, 3000);
1648 return;
1649 }
1650 }
1651
1652 m_textEdit->setTextCursor(found);
1653 m_statusBar->showMessage("Found: " + m_lastSearchText, 2000);
1654}
1655
1657 if (m_lastSearchText.isEmpty()) {
1658 findText();
1659 return;
1660 }
1661
1662 QTextCursor cursor = m_textEdit->textCursor();
1663 QTextDocument::FindFlags flags = QTextDocument::FindBackward;
1664
1665 QTextCursor found = m_textEdit->document()->find(m_lastSearchText, cursor, flags);
1666
1667 if (found.isNull()) {
1668 cursor.movePosition(QTextCursor::End);
1669 found = m_textEdit->document()->find(m_lastSearchText, cursor, flags);
1670
1671 if (found.isNull()) {
1672 m_statusBar->showMessage("Text not found: " + m_lastSearchText, 3000);
1673 return;
1674 }
1675 }
1676
1677 m_textEdit->setTextCursor(found);
1678 m_statusBar->showMessage("Found: " + m_lastSearchText, 2000);
1679}
1680
1682 bool ok;
1683 QString searchText = QInputDialog::getText(this, "Replace", "Find:", QLineEdit::Normal, m_lastSearchText, &ok);
1684 if (!ok || searchText.isEmpty())
1685 return;
1686
1687 QString replaceWith = QInputDialog::getText(this, "Replace", "Replace with:", QLineEdit::Normal, "", &ok);
1688 if (!ok)
1689 return;
1690
1691 QMessageBox::StandardButton reply = QMessageBox::question(this, "Replace All", "Replace all occurrences of '" + searchText + "' with '" + replaceWith + "'?", QMessageBox::Yes | QMessageBox::No);
1692
1693 if (reply == QMessageBox::Yes) {
1694 QString text = m_textEdit->toPlainText();
1695 int count = text.count(searchText);
1696 if (count > 0) {
1697 text.replace(searchText, replaceWith);
1698 QTextCursor cursor(m_textEdit->document());
1699 cursor.beginEditBlock();
1700 cursor.select(QTextCursor::Document);
1701 cursor.insertText(text);
1702 cursor.endEditBlock();
1703 }
1704 m_lastSearchText = searchText;
1705 m_statusBar->showMessage("Replaced " + QString::number(count) + " occurrence(s)", 3000);
1706 }
1707}
1708
1709void TextEditor::gotoLine() {
1710 bool ok;
1711 int lineNumber = QInputDialog::getInt(this, "Go to Line", "Line number:", 1, 1, m_textEdit->document()->blockCount(), 1, &ok);
1712 if (ok) {
1713 QTextCursor cursor(m_textEdit->document()->findBlockByLineNumber(lineNumber - 1));
1714 m_textEdit->setTextCursor(cursor);
1715 m_textEdit->centerCursor();
1716 }
1717}
1718
1720 m_fontSize += 2;
1721 if (m_fontSize > 72)
1724 QSettings("LostSideDead").setValue("editor/fontSize", m_fontSize);
1725}
1726
1728 m_fontSize -= 2;
1729 if (m_fontSize < 8)
1730 m_fontSize = 8;
1732 QSettings("LostSideDead").setValue("editor/fontSize", m_fontSize);
1733}
1734
1736 m_fontSize = 24;
1738 QSettings("LostSideDead").setValue("editor/fontSize", m_fontSize);
1739}
1740
1742 QString styleSheet;
1744 styleSheet = QString("QPlainTextEdit { "
1745 "font-size: %1px; "
1746 "font-family: 'Courier New', Courier, monospace; "
1747 "}")
1748 .arg(m_fontSize);
1749 } else {
1750 styleSheet = QString("QPlainTextEdit { "
1751 "color: white; "
1752 "font-size: %1px; "
1753 "font-family: 'Courier New', Courier, monospace; "
1754 "background-color: black; "
1755 "}")
1756 .arg(m_fontSize);
1757 }
1758
1759 m_textEdit->setStyleSheet(styleSheet);
1760 m_textEdit->setTabStopDistance(4 * m_textEdit->fontMetrics().horizontalAdvance(' '));
1762}
1763
1765 QTextCursor cursor = m_textEdit->textCursor();
1766 int line = cursor.blockNumber() + 1;
1767 int col = cursor.columnNumber() + 1;
1768 QString status = QString("Line: %1, Col: %2 | Lines: %3").arg(line).arg(col).arg(m_textEdit->document()->blockCount());
1769 if (cursor.hasSelection())
1770 status += QString(" | Selected: %1").arg(cursor.selectionEnd() - cursor.selectionStart());
1771 m_lineColLabel->setText(status);
1772}
1773
1774void TextEditor::closeEvent(QCloseEvent *event) {
1775 if (maybePromptSave()) {
1776 QSettings("LostSideDead").setValue("editor/geometry", saveGeometry());
1777 event->accept();
1778 } else {
1779 event->ignore();
1780 }
1781}
1782
1783void TextEditor::keyPressEvent(QKeyEvent *event) {
1784 if (event->key() == Qt::Key_Escape && event->modifiers() == Qt::NoModifier) {
1785 if (maybePromptSave()) {
1786 QSettings("LostSideDead").setValue("editor/geometry", saveGeometry());
1787 accept();
1788 }
1789 event->accept();
1790 return;
1791 }
1792 QDialog::keyPressEvent(event);
1793}
1794
1796 if (!m_modified)
1797 return true;
1798 QMessageBox::StandardButton reply = QMessageBox::question(this, "Unsaved Changes", "The document has been modified. Do you want to save your changes?", QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
1799
1800 if (reply == QMessageBox::Save) {
1801 saveContents();
1802 // saveContents clears m_modified on success; if the user cancelled
1803 // a Save As dialog the flag stays true, so honour that as Cancel.
1804 return !m_modified;
1805 }
1806 if (reply == QMessageBox::Discard)
1807 return true;
1808 return false;
1809}
void matchBrackets()
Definition editor.cpp:366
void indentSelection()
Definition editor.cpp:459
LineNumberArea * m_lineNumberArea
Definition editor.hpp:109
CustomTextEdit(QWidget *parent=nullptr)
Definition editor.cpp:198
void moveLineDown()
Definition editor.cpp:593
void highlightCurrentLine()
Definition editor.cpp:346
bool hasMultiLineSelection()
Definition editor.cpp:450
int lineNumberAreaWidth()
Compute width required for current line-number digit count.
Definition editor.cpp:227
QVector< ShaderDiagnostic > m_diagnostics
Definition editor.hpp:110
void autoIndentNewLine()
Definition editor.cpp:512
void setCompletionWords(const QStringList &words)
Replace the keywords offered by code completion.
Definition editor.cpp:244
void setDiagnostics(const QVector< ShaderDiagnostic > &diagnostics)
Replace the line markers and underlines shown for compiler messages.
Definition editor.cpp:238
void insertCompletion(const QString &completion)
Definition editor.cpp:267
void lineNumberAreaPaintEvent(QPaintEvent *event)
Paint callback used by the line-number side widget.
Definition editor.cpp:313
void showCompletionPopup(bool forced)
Definition editor.cpp:278
void mousePressEvent(QMouseEvent *event) override
Definition editor.cpp:867
QString completionPrefix() const
Definition editor.cpp:251
void updateLineNumberAreaWidth(int newBlockCount)
Definition editor.cpp:295
void resizeEvent(QResizeEvent *event) override
Definition editor.cpp:307
void smartHome(bool shift)
Definition editor.cpp:665
QCompleter * m_completer
Definition editor.hpp:111
void unindentSelection()
Definition editor.cpp:480
void keyPressEvent(QKeyEvent *event) override
Definition editor.cpp:689
void themeChanged()
void duplicateLine()
Definition editor.cpp:557
QStringListModel * m_completionModel
Definition editor.hpp:112
void toggleComment()
Definition editor.cpp:616
void changeEvent(QEvent *event) override
Definition editor.cpp:217
void moveLineUp()
Definition editor.cpp:569
void includeRequested(const QString &includeName)
void updateLineNumberArea(const QRect &rect, int dy)
Definition editor.cpp:297
void setEditorPalette(const QPalette &palette)
Select syntax colors that contrast with the editor palette.
Definition syntax.cpp:5
GlslSyntaxHighlighter(QTextDocument *parent=nullptr)
Definition syntax.cpp:3
LineNumberArea(CustomTextEdit *editor)
Definition editor.hpp:117
TextEditor(QWidget *parent=nullptr)
Definition editor.cpp:887
QString m_lastSearchText
Definition editor.hpp:218
void updateFontSize()
Definition editor.cpp:1699
void updateCursorPosition()
Definition editor.cpp:1722
CustomTextEdit * m_textEdit
Definition editor.hpp:196
QHash< QString, QDoubleSpinBox * > m_uniformSpins
Definition editor.hpp:210
int m_currentDiagnostic
Definition editor.hpp:213
QHash< QString, QSlider * > m_uniformSliders
Definition editor.hpp:211
bool m_acmxvkContext
Definition editor.hpp:214
bool m_modified
Definition editor.hpp:195
QVector< ShaderEditorUniform > m_uniforms
Definition editor.hpp:215
void decreaseFontSize()
Definition editor.cpp:1685
QLabel * m_lineColLabel
Definition editor.hpp:199
void saveContents()
Definition editor.cpp:1525
QVector< ShaderDiagnostic > m_diagnostics
Definition editor.hpp:212
void setUniformValue(const QString &name, double value)
Definition editor.cpp:972
void updateWindowTitle()
Definition editor.cpp:1230
void increaseFontSize()
Definition editor.cpp:1677
void updateDiagnosticActions()
Definition editor.cpp:1224
QString filename
Definition editor.hpp:217
void setShaderContext(bool acmxvk, const QVector< ShaderEditorUniform > &uniforms, const QString &libraryDirectory=QString())
Configure ACMXVK completion and snippets from the active manifest.
Definition editor.cpp:961
void fileSaved(const QString &path)
Emitted after the editor successfully writes its contents to disk.
void updateCompletionWords()
Definition editor.cpp:1086
QVBoxLayout * m_uniformLayout
Definition editor.hpp:206
QStatusBar * m_statusBar
Definition editor.hpp:198
QWidget * m_uniformPanel
Definition editor.hpp:204
void previewRequested(const QString &path, const QString &source)
void revealLocation(int lineNumber, int columnNumber=0, int matchLength=0)
Move to a one-based line and select the requested source match.
Definition editor.cpp:904
void setCompilePending()
Mark the associated shader as being compiled.
Definition editor.cpp:922
void findNext()
Definition editor.cpp:1588
void uniformValueChanged(const QString &name, double value)
void insertSnippet(const QString &snippet)
Definition editor.cpp:1147
void init()
Definition editor.cpp:1241
void findText()
Definition editor.cpp:1575
void requestPreview()
Definition editor.cpp:1004
void replaceText()
Definition editor.cpp:1639
void keyPressEvent(QKeyEvent *event) override
Definition editor.cpp:1741
QString customUniformDefines() const
Definition editor.cpp:1136
void saveAs()
Definition editor.cpp:1563
GlslSyntaxHighlighter * m_highlighter
Definition editor.hpp:197
QTimer * m_previewTimer
Definition editor.hpp:209
bool writeFile(const QString &filePath)
Atomically write the document to disk.
Definition editor.cpp:1534
void openFileRequested(const QString &path, int lineNumber)
void openInclude(const QString &includeName)
Definition editor.cpp:991
void gotoLine()
Definition editor.cpp:1667
void setFileName(const QString &filename)
Associate editor with a shader file path.
Definition editor.cpp:897
QScrollArea * m_uniformScroll
Definition editor.hpp:205
QCheckBox * m_loopSafetyCheck
Definition editor.hpp:208
void resetFontSize()
Definition editor.cpp:1693
QCheckBox * m_livePreviewCheck
Definition editor.hpp:207
bool maybePromptSave()
Show a Save/Discard/Cancel prompt if the document is modified.
Definition editor.cpp:1753
void rebuildUniformControls()
Definition editor.cpp:1025
QVector< ShaderDiagnostic > parseDiagnostics(const QString &compilerOutput) const
Definition editor.cpp:1164
void setCompileResult(bool success, const QString &compilerOutput)
Show compiler status and source diagnostics for the associated shader.
Definition editor.cpp:932
QString fileName() const
Return the file currently associated with this editor.
Definition editor.cpp:902
QMenu * m_snippetsMenu
Definition editor.hpp:203
int m_fontSize
Definition editor.hpp:219
void navigateDiagnostic(int offset)
Definition editor.cpp:1212
void setText(const QString &text)
Replace editor contents and reset displayed text.
Definition editor.cpp:889
void closeEvent(QCloseEvent *event) override
Definition editor.cpp:1732
QAction * m_previousDiagnosticAction
Definition editor.hpp:202
QAction * m_nextDiagnosticAction
Definition editor.hpp:201
void revertContents()
Definition editor.cpp:1013
QString m_libraryDirectory
Definition editor.hpp:216
QLabel * m_compileStatusLabel
Definition editor.hpp:200
void findPrevious()
Definition editor.cpp:1614
static QColor currentLineBackground(const QPalette &palette)
Definition editor.cpp:176
static QChar matchingBracket(QChar ch)
Definition editor.cpp:348
QString inject_safety_counters(const QString &glsl_source)
Add bounded iteration guards and preserve original GLSL diagnostic lines.
Definition editor.cpp:107
static bool isOpenBracket(QChar ch)
Definition editor.cpp:364
static QColor diagnosticColor(ShaderDiagnosticSeverity severity)
Definition editor.cpp:186
Shader text editor widgets with line numbers and GLSL highlighting.
QString inject_safety_counters(const QString &glsl_source)
Add bounded iteration guards and preserve original GLSL diagnostic lines.
Definition editor.cpp:107
ShaderDiagnosticSeverity
Definition editor.hpp:37
bool isCustomStyleEnabled()
void applyCustomStyleIfEnabled(QWidget *widget)
int skip_glsl_trivia(const QString &source, int position)
Definition editor.cpp:52
const QString LOOP_SAFETY_MARKER
Definition editor.cpp:48
int find_matching_glsl_delimiter(const QString &source, int opening, QChar open, QChar close)
Definition editor.cpp:72
constexpr int LOOP_SAFETY_LIMIT
Definition editor.cpp:47
bool is_identifier_character(QChar character)
Definition editor.cpp:50
ShaderDiagnosticSeverity severity
Definition editor.hpp:42
QString message
Definition editor.hpp:43