#include "VariableCompleter.h" #include "CodeEditor.h" #include #include #include #include #include #include #include #include VariableCompleter::VariableCompleter(CodeEditor *editor) : QObject(editor) , m_editor(editor) { // Popup als reines Anzeige-Fenster — dieselbe Fensterart wie // SignatureTooltip, die nachweislich nie den Fokus an sich reißt. // Wichtig: Ein echtes Elternfenster (editor->window()) angeben, sonst // behandelt der Fenstermanager das Popup als eigenständiges Fenster. // Das führte dazu, dass bei mehreren offenen Dateien/Tabs verwaiste // Popups entstehen konnten, die den Fokus übernehmen und die // Texteingabe blockieren. m_popup = new QListWidget(editor->window()); m_popup->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint); m_popup->setAttribute(Qt::WA_ShowWithoutActivating); m_popup->setFocusPolicy(Qt::NoFocus); m_popup->setMaximumHeight(200); m_popup->setMinimumWidth(180); m_popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_popup->setStyleSheet( "QListWidget {" " border: 1px solid palette(mid);" " background: palette(toolTipBase);" " color: palette(toolTipText);" " font-family: Monospace;" "}" "QListWidget::item:selected {" " background: palette(highlight);" " color: palette(highlightedText);" "}" ); connect(m_popup, &QListWidget::itemActivated, this, &VariableCompleter::onItemActivated); } // --------------------------------------------------------------------------- // Nach jedem Tastendruck aufrufen // --------------------------------------------------------------------------- void VariableCompleter::handleKeyPress(QKeyEvent *event) { // Popup-Navigation if (m_popup->isVisible()) { if (event->key() == Qt::Key_Escape) { hidePopup(); return; } if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { if (m_popup->currentItem()) { onItemActivated(m_popup->currentItem()); } hidePopup(); return; } if (event->key() == Qt::Key_Down) { const int next = qMin(m_popup->currentRow() + 1, m_popup->count() - 1); m_popup->setCurrentRow(next); return; } if (event->key() == Qt::Key_Up) { const int prev = qMax(m_popup->currentRow() - 1, 0); m_popup->setCurrentRow(prev); return; } } // Nach dem normalen Tastendruck Popup aktualisieren // (der Event wurde bereits an QPlainTextEdit weitergegeben) updatePopup(); } // --------------------------------------------------------------------------- // Popup aktualisieren // --------------------------------------------------------------------------- void VariableCompleter::updatePopup() { const QString prefix = currentPrefix(); // Nur bei $ und mindestens einem weiteren Zeichen anzeigen if (prefix.length() < 2 || !prefix.startsWith('$')) { hidePopup(); return; } const QStringList allVars = collectVariables(); const QString filter = prefix.toLower(); QStringList matches; for (const QString &var : allVars) { if (var.toLower().startsWith(filter) && var != prefix) { matches.append(var); } } if (matches.isEmpty()) { hidePopup(); return; } // Popup befüllen m_popup->clear(); for (const QString &var : matches) { m_popup->addItem(var); } m_popup->setCurrentRow(0); // Größe anpassen const int itemHeight = m_popup->sizeHintForRow(0) + 2; const int height = qMin(matches.size() * itemHeight + 4, 200); m_popup->setFixedHeight(height); // Position unter dem Cursor const QRect cursorRect = m_editor->cursorRect(); QPoint pos = m_editor->viewport()->mapToGlobal( QPoint(cursorRect.left(), cursorRect.bottom() + 2) ); // Nicht aus dem Bildschirm herausragen const QRect screen = QApplication::primaryScreen()->availableGeometry(); if (pos.x() + m_popup->width() > screen.right()) { pos.setX(screen.right() - m_popup->width()); } if (pos.y() + height > screen.bottom()) { pos.setY(cursorRect.top() - height - 2); } m_popup->move(pos); m_popup->show(); m_popup->raise(); } void VariableCompleter::hidePopup() { m_popup->hide(); m_popup->clear(); } // --------------------------------------------------------------------------- // Editor hat den Fokus verloren (z. B. Tab-Wechsel) — Popup schließen // --------------------------------------------------------------------------- void VariableCompleter::notifyFocusLost() { hidePopup(); } // --------------------------------------------------------------------------- // Variablen im aktuellen Dokument sammeln // --------------------------------------------------------------------------- QStringList VariableCompleter::collectVariables() const { static const QRegularExpression varRegex(R"(\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)"); QSet seen; QStringList result; const QString text = m_editor->toPlainText(); QRegularExpressionMatchIterator it = varRegex.globalMatch(text); while (it.hasNext()) { const QString var = it.next().captured(0); if (!seen.contains(var)) { seen.insert(var); result.append(var); } } result.sort(Qt::CaseInsensitive); return result; } // --------------------------------------------------------------------------- // Text links vom Cursor — das angefangene $variable // --------------------------------------------------------------------------- QString VariableCompleter::currentPrefix() const { const QTextCursor cursor = m_editor->textCursor(); const QString line = cursor.block().text(); const int col = cursor.columnNumber(); if (col == 0) { return QString(); } // Rückwärts gehen bis zum $ oder einem Nicht-Bezeichner-Zeichen int start = col - 1; while (start > 0) { const QChar c = line[start - 1]; if (!c.isLetterOrNumber() && c != '_' && c != '$') { break; } --start; } const QString token = line.mid(start, col - start); return token.startsWith('$') ? token : QString(); } // --------------------------------------------------------------------------- // Klick oder Enter — Variable einfügen // --------------------------------------------------------------------------- void VariableCompleter::onItemActivated(QListWidgetItem *item) { if (!item) { return; } const QString selected = item->text(); const QString prefix = currentPrefix(); if (prefix.isEmpty()) { hidePopup(); return; } // Prefix durch den vollständigen Variablennamen ersetzen QTextCursor cursor = m_editor->textCursor(); cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, prefix.length()); cursor.insertText(selected); m_editor->setTextCursor(cursor); hidePopup(); }