Erste Version

This commit is contained in:
2026-05-23 13:14:30 +02:00
commit 36e074f43d
39 changed files with 3430 additions and 0 deletions

26
src/editor/CMakeLists.txt Normal file
View File

@@ -0,0 +1,26 @@
set(EDITOR_SOURCES
EditorPanel.cpp
EditorPanel.h
CodeEditor.cpp
CodeEditor.h
LineNumberArea.cpp
LineNumberArea.h
EditorTab.cpp
EditorTab.h
SearchPanel.cpp
SearchPanel.h
)
add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})
target_link_libraries(BareCode_Editor PUBLIC
Qt6::Core
Qt6::Gui
Qt6::Widgets
BareCode_Highlighter
)
target_include_directories(BareCode_Editor PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
)

345
src/editor/CodeEditor.cpp Normal file
View File

@@ -0,0 +1,345 @@
#include "CodeEditor.h"
#include "LineNumberArea.h"
#include "core/Settings.h"
#include "highlighter/HighlighterFactory.h"
#include <QPainter>
#include <QTextBlock>
#include <QPaintEvent>
#include <QResizeEvent>
#include <QKeyEvent>
#include <QScrollBar>
#include <QFile>
#include <QTextStream>
#include <QFileInfo>
#include <QFileDialog>
#include <QMessageBox>
CodeEditor::CodeEditor(Settings *settings, QWidget *parent)
: QPlainTextEdit(parent)
, m_settings(settings)
{
m_lineNumberArea = new LineNumberArea(this);
setupEditor();
connect(this, &CodeEditor::blockCountChanged,
this, &CodeEditor::updateLineNumberAreaWidth);
connect(this, &CodeEditor::updateRequest,
this, &CodeEditor::updateLineNumberArea);
connect(this, &CodeEditor::cursorPositionChanged,
this, &CodeEditor::highlightCurrentLine);
updateLineNumberAreaWidth(0);
highlightCurrentLine();
}
CodeEditor::~CodeEditor() = default;
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
void CodeEditor::setupEditor()
{
applySettings();
setLineWrapMode(QPlainTextEdit::NoWrap);
}
void CodeEditor::applySettings()
{
setFont(m_settings->editorFont());
const int tabStop = m_settings->tabSize();
// Set tab stop width in pixels using font metrics
QFontMetrics fm(m_settings->editorFont());
setTabStopDistance(static_cast<qreal>(tabStop) * fm.horizontalAdvance(' '));
}
// ---------------------------------------------------------------------------
// File I/O
// ---------------------------------------------------------------------------
void CodeEditor::loadFile(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QMessageBox::warning(this, tr("Open File"),
tr("Cannot open file:\n%1").arg(filePath));
return;
}
m_filePath = filePath;
QTextStream in(&file);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
in.setEncoding(QStringConverter::Utf8);
#else
in.setCodec("UTF-8");
#endif
setPlainText(in.readAll());
document()->setModified(false);
installHighlighter(filePath);
}
QString CodeEditor::filePath() const
{
return m_filePath;
}
bool CodeEditor::save()
{
if (m_filePath.isEmpty())
{
return saveAs();
}
return writeToFile(m_filePath);
}
bool CodeEditor::saveAs()
{
const QString path = QFileDialog::getSaveFileName(
this,
tr("Speichern unter"),
m_filePath
);
if (path.isEmpty())
{
return false;
}
m_filePath = path;
installHighlighter(m_filePath);
return writeToFile(m_filePath);
}
bool CodeEditor::writeToFile(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QMessageBox::warning(this, tr("Speichern"),
tr("Datei konnte nicht gespeichert werden:\n%1").arg(filePath));
return false;
}
QTextStream out(&file);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
out.setEncoding(QStringConverter::Utf8);
#else
out.setCodec("UTF-8");
#endif
out << toPlainText();
document()->setModified(false);
emit fileSaved(filePath);
return true;
}
void CodeEditor::installHighlighter(const QString &filePath)
{
// Remove old highlighter first
delete m_highlighter;
m_highlighter = nullptr;
m_highlighter = HighlighterFactory::createForFile(filePath, document());
}
bool CodeEditor::isModified() const
{
return document()->isModified();
}
// ---------------------------------------------------------------------------
// Line number area
// ---------------------------------------------------------------------------
int CodeEditor::lineNumberAreaWidth() const
{
int digits = 1;
int max = qMax(1, blockCount());
while (max >= 10)
{
max /= 10;
++digits;
}
const int padding = 8;
return fontMetrics().horizontalAdvance('9') * digits + padding * 2;
}
void CodeEditor::updateLineNumberAreaWidth(int /*newBlockCount*/)
{
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
}
void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
{
if (dy != 0)
{
m_lineNumberArea->scroll(0, dy);
}
else
{
m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height());
}
if (rect.contains(viewport()->rect()))
{
updateLineNumberAreaWidth(0);
}
}
void CodeEditor::resizeEvent(QResizeEvent *event)
{
QPlainTextEdit::resizeEvent(event);
const QRect cr = contentsRect();
m_lineNumberArea->setGeometry(
QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())
);
}
void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
{
QPainter painter(m_lineNumberArea);
// Background
const QColor bgColor = palette().color(QPalette::Window).darker(110);
painter.fillRect(event->rect(), bgColor);
const QColor lineNumColor = palette().color(QPalette::Mid);
const QColor activeColor = palette().color(QPalette::Text);
const int currentLine = textCursor().blockNumber();
QTextBlock block = firstVisibleBlock();
int blockNumber = block.blockNumber();
int top = static_cast<int>(blockBoundingGeometry(block).translated(contentOffset()).top());
int bottom = top + static_cast<int>(blockBoundingRect(block).height());
while (block.isValid() && top <= event->rect().bottom())
{
if (block.isVisible() && bottom >= event->rect().top())
{
const QString number = QString::number(blockNumber + 1);
painter.setPen(blockNumber == currentLine ? activeColor : lineNumColor);
painter.drawText(
0,
top,
m_lineNumberArea->width() - 4,
fontMetrics().height(),
Qt::AlignRight,
number
);
}
block = block.next();
top = bottom;
bottom = top + static_cast<int>(blockBoundingRect(block).height());
++blockNumber;
}
}
// ---------------------------------------------------------------------------
// Current line highlight
// ---------------------------------------------------------------------------
void CodeEditor::highlightCurrentLine()
{
QList<QTextEdit::ExtraSelection> extraSelections;
if (!isReadOnly())
{
QTextEdit::ExtraSelection selection;
const QColor lineColor = palette().color(QPalette::AlternateBase);
selection.format.setBackground(lineColor);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
}
setExtraSelections(extraSelections);
}
// ---------------------------------------------------------------------------
// Key handling auto-indent + Tab → spaces
// ---------------------------------------------------------------------------
void CodeEditor::keyPressEvent(QKeyEvent *event)
{
// Tab key: insert spaces instead of a real tab character
if (event->key() == Qt::Key_Tab && m_settings->useSpacesForTabs())
{
const int tabSize = m_settings->tabSize();
QTextCursor cursor = textCursor();
if (cursor.hasSelection())
{
// Indent selected lines
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
cursor.beginEditBlock();
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
{
QTextCursor lineCursor(b);
lineCursor.insertText(QString(tabSize, ' '));
}
cursor.endEditBlock();
}
else
{
// Calculate spaces needed to reach next tab stop
const int col = cursor.columnNumber();
const int spacesNeeded = tabSize - (col % tabSize);
cursor.insertText(QString(spacesNeeded, ' '));
}
return;
}
// Enter / Return: auto-indent
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
{
QTextCursor cursor = textCursor();
const QString currentLine = cursor.block().text();
// Count leading whitespace
int leadingSpaces = 0;
for (const QChar &ch : currentLine)
{
if (ch == ' ')
{
++leadingSpaces;
}
else if (ch == '\t')
{
leadingSpaces += m_settings->tabSize();
}
else
{
break;
}
}
// Let the base class insert the newline first
QPlainTextEdit::keyPressEvent(event);
// Then re-indent
if (leadingSpaces > 0)
{
const QString indent = m_settings->useSpacesForTabs()
? QString(leadingSpaces, ' ')
: QString(leadingSpaces / m_settings->tabSize(), '\t');
textCursor().insertText(indent);
}
return;
}
QPlainTextEdit::keyPressEvent(event);
}

63
src/editor/CodeEditor.h Normal file
View File

@@ -0,0 +1,63 @@
#pragma once
#include <QPlainTextEdit>
#include <QFont>
#include <QString>
class LineNumberArea;
class Settings;
class SyntaxHighlighter;
// ---------------------------------------------------------------------------
// CodeEditor Core editing widget.
// Features:
// • Line number gutter
// • Current-line highlight
// • Auto-indent on Enter
// • Tab → spaces (configurable)
// • Syntax highlighting (via pluggable SyntaxHighlighter)
// ---------------------------------------------------------------------------
class CodeEditor : public QPlainTextEdit
{
Q_OBJECT
public:
explicit CodeEditor(Settings *settings, QWidget *parent = nullptr);
~CodeEditor() override;
void loadFile(const QString &filePath);
void applySettings();
// Speichern
bool save();
bool saveAs();
// Called by LineNumberArea
int lineNumberAreaWidth() const;
void lineNumberAreaPaintEvent(QPaintEvent *event);
QString filePath() const;
bool isModified() const;
signals:
void fileSaved(const QString &filePath);
protected:
void resizeEvent(QResizeEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private slots:
void updateLineNumberAreaWidth(int newBlockCount);
void highlightCurrentLine();
void updateLineNumberArea(const QRect &rect, int dy);
private:
void setupEditor();
void installHighlighter(const QString &filePath);
bool writeToFile(const QString &filePath);
Settings *m_settings = nullptr;
LineNumberArea *m_lineNumberArea = nullptr;
SyntaxHighlighter *m_highlighter = nullptr;
QString m_filePath;
};

157
src/editor/EditorPanel.cpp Normal file
View File

@@ -0,0 +1,157 @@
#include "EditorPanel.h"
#include "EditorTab.h"
#include "CodeEditor.h"
#include "SearchPanel.h"
#include <QFileInfo>
EditorPanel::EditorPanel(Settings *settings, QWidget *parent)
: QWidget(parent)
, m_settings(settings)
{
setupUi();
}
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
void EditorPanel::setupUi()
{
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(0, 0, 0, 0);
m_layout->setSpacing(0);
m_tabWidget = new QTabWidget(this);
m_tabWidget->setTabsClosable(true);
m_tabWidget->setMovable(true);
m_tabWidget->setDocumentMode(true);
m_searchPanel = new SearchPanel(this);
m_layout->addWidget(m_tabWidget, 1);
m_layout->addWidget(m_searchPanel, 0);
connect(m_tabWidget, &QTabWidget::tabCloseRequested,
this, &EditorPanel::onTabCloseRequested);
connect(m_tabWidget, &QTabWidget::currentChanged,
this, &EditorPanel::onCurrentTabChanged);
}
// ---------------------------------------------------------------------------
// Hilfsmethoden
// ---------------------------------------------------------------------------
EditorTab *EditorPanel::currentTab() const
{
return qobject_cast<EditorTab *>(m_tabWidget->currentWidget());
}
int EditorPanel::findTabForFile(const QString &filePath) const
{
EditorTab *tab = m_openTabs.value(filePath, nullptr);
return tab ? m_tabWidget->indexOf(tab) : -1;
}
// ---------------------------------------------------------------------------
// Öffentliche Slots
// ---------------------------------------------------------------------------
void EditorPanel::openFile(const QString &filePath)
{
const int existing = findTabForFile(filePath);
if (existing != -1)
{
m_tabWidget->setCurrentIndex(existing);
return;
}
EditorTab *tab = new EditorTab(filePath, m_settings, m_tabWidget);
const int index = m_tabWidget->addTab(tab, tab->fileName());
m_tabWidget->setCurrentIndex(index);
m_tabWidget->setTabToolTip(index, filePath);
m_openTabs.insert(filePath, tab);
// Tab-Titel nach "Speichern unter" aktualisieren
connect(tab->editor(), &CodeEditor::fileSaved, this, [this, tab](const QString &savedPath)
{
const int idx = m_tabWidget->indexOf(tab);
if (idx != -1)
{
m_tabWidget->setTabText(idx, QFileInfo(savedPath).fileName());
m_tabWidget->setTabToolTip(idx, savedPath);
}
emit currentFileSaved(savedPath);
});
}
void EditorPanel::saveCurrentFile()
{
if (EditorTab *tab = currentTab())
{
tab->save();
}
}
void EditorPanel::saveCurrentFileAs()
{
if (EditorTab *tab = currentTab())
{
tab->saveAs();
}
}
void EditorPanel::saveAllFiles()
{
for (int i = 0; i < m_tabWidget->count(); ++i)
{
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(i));
if (tab && tab->isModified())
{
tab->save();
}
}
}
void EditorPanel::showSearchPanel()
{
m_searchPanel->activate();
}
void EditorPanel::undo()
{
if (EditorTab *tab = currentTab())
{
tab->editor()->undo();
}
}
void EditorPanel::redo()
{
if (EditorTab *tab = currentTab())
{
tab->editor()->redo();
}
}
// ---------------------------------------------------------------------------
// Private Slots
// ---------------------------------------------------------------------------
void EditorPanel::onTabCloseRequested(int index)
{
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(index));
if (!tab)
{
return;
}
m_openTabs.remove(tab->filePath());
m_tabWidget->removeTab(index);
tab->deleteLater();
m_searchPanel->setEditor(currentTab() ? currentTab()->editor() : nullptr);
}
void EditorPanel::onCurrentTabChanged(int /*index*/)
{
EditorTab *tab = currentTab();
m_searchPanel->setEditor(tab ? tab->editor() : nullptr);
}

50
src/editor/EditorPanel.h Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
#include <QWidget>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QHash>
#include <QString>
class EditorTab;
class Settings;
class SearchPanel;
// ---------------------------------------------------------------------------
// EditorPanel Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel.
// ---------------------------------------------------------------------------
class EditorPanel : public QWidget
{
Q_OBJECT
public:
explicit EditorPanel(Settings *settings, QWidget *parent = nullptr);
public slots:
void openFile(const QString &filePath);
void saveCurrentFile();
void saveCurrentFileAs();
void saveAllFiles();
void showSearchPanel();
void undo();
void redo();
signals:
void currentFileSaved(const QString &filePath);
private slots:
void onTabCloseRequested(int index);
void onCurrentTabChanged(int index);
private:
void setupUi();
int findTabForFile(const QString &filePath) const;
EditorTab *currentTab() const;
Settings *m_settings = nullptr;
QVBoxLayout *m_layout = nullptr;
QTabWidget *m_tabWidget = nullptr;
SearchPanel *m_searchPanel = nullptr;
QHash<QString, EditorTab *> m_openTabs;
};

53
src/editor/EditorTab.cpp Normal file
View File

@@ -0,0 +1,53 @@
#include "EditorTab.h"
#include "CodeEditor.h"
#include <QFileInfo>
EditorTab::EditorTab(const QString &filePath, Settings *settings, QWidget *parent)
: QWidget(parent)
, m_filePath(filePath)
{
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(0, 0, 0, 0);
m_layout->setSpacing(0);
m_editor = new CodeEditor(settings, this);
m_editor->loadFile(filePath);
m_layout->addWidget(m_editor);
}
QString EditorTab::filePath() const
{
return m_filePath;
}
QString EditorTab::fileName() const
{
return QFileInfo(m_filePath).fileName();
}
CodeEditor *EditorTab::editor() const
{
return m_editor;
}
bool EditorTab::isModified() const
{
return m_editor->isModified();
}
bool EditorTab::save()
{
const bool ok = m_editor->save();
// Path may have changed if this was an untitled buffer saved for the first time
m_filePath = m_editor->filePath();
return ok;
}
bool EditorTab::saveAs()
{
const bool ok = m_editor->saveAs();
m_filePath = m_editor->filePath();
return ok;
}

33
src/editor/EditorTab.h Normal file
View File

@@ -0,0 +1,33 @@
#pragma once
#include <QWidget>
#include <QString>
#include <QVBoxLayout>
class CodeEditor;
class Settings;
// ---------------------------------------------------------------------------
// EditorTab Widget placed inside each tab of the tab bar.
// Owns a CodeEditor for a single file.
// ---------------------------------------------------------------------------
class EditorTab : public QWidget
{
Q_OBJECT
public:
explicit EditorTab(const QString &filePath, Settings *settings, QWidget *parent = nullptr);
QString filePath() const;
QString fileName() const;
CodeEditor *editor() const;
bool isModified() const;
bool save();
bool saveAs();
private:
QString m_filePath;
QVBoxLayout *m_layout = nullptr;
CodeEditor *m_editor = nullptr;
};

View File

@@ -0,0 +1,18 @@
#include "LineNumberArea.h"
#include "CodeEditor.h"
LineNumberArea::LineNumberArea(CodeEditor *editor)
: QWidget(editor)
, m_codeEditor(editor)
{
}
QSize LineNumberArea::sizeHint() const
{
return QSize(m_codeEditor->lineNumberAreaWidth(), 0);
}
void LineNumberArea::paintEvent(QPaintEvent *event)
{
m_codeEditor->lineNumberAreaPaintEvent(event);
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <QWidget>
class CodeEditor;
// ---------------------------------------------------------------------------
// LineNumberArea Thin widget painted on the left side of the CodeEditor.
// Painted by CodeEditor::lineNumberAreaPaintEvent().
// ---------------------------------------------------------------------------
class LineNumberArea : public QWidget
{
Q_OBJECT
public:
explicit LineNumberArea(CodeEditor *editor);
QSize sizeHint() const override;
protected:
void paintEvent(QPaintEvent *event) override;
private:
CodeEditor *m_codeEditor;
};

492
src/editor/SearchPanel.cpp Normal file
View File

@@ -0,0 +1,492 @@
#include "SearchPanel.h"
#include "CodeEditor.h"
#include <QTextCursor>
#include <QTextBlock>
#include <QRegularExpression>
#include <QMessageBox>
#include <QKeyEvent>
#include <QShortcut>
SearchPanel::SearchPanel(QWidget *parent)
: QWidget(parent)
{
setupUi();
hide();
}
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
void SearchPanel::setupUi()
{
m_grid = new QGridLayout(this);
m_grid->setContentsMargins(6, 4, 6, 4);
m_grid->setSpacing(4);
// ---- Row 0: Suchen ----
m_searchEdit = new QLineEdit(this);
m_searchEdit->setPlaceholderText(tr("Suchen…"));
m_searchEdit->setClearButtonEnabled(true);
m_btnPrev = new QPushButton(tr(""), this);
m_btnNext = new QPushButton(tr(""), this);
m_btnPrev->setFixedWidth(28);
m_btnNext->setFixedWidth(28);
m_btnPrev->setToolTip(tr("Vorheriger Treffer (Shift+F3)"));
m_btnNext->setToolTip(tr("Nächster Treffer (F3)"));
m_matchLabel = new QLabel(this);
m_matchLabel->setMinimumWidth(80);
m_btnClose = new QPushButton(tr(""), this);
m_btnClose->setFixedWidth(24);
m_btnClose->setToolTip(tr("Schließen (Esc)"));
m_btnClose->setFlat(true);
QHBoxLayout *searchRow = new QHBoxLayout();
searchRow->addWidget(new QLabel(tr("Suchen:"), this));
searchRow->addWidget(m_searchEdit, 1);
searchRow->addWidget(m_btnPrev);
searchRow->addWidget(m_btnNext);
searchRow->addWidget(m_matchLabel);
searchRow->addWidget(m_btnClose);
m_grid->addLayout(searchRow, 0, 0);
// ---- Row 1: Ersetzen ----
m_replaceEdit = new QLineEdit(this);
m_replaceEdit->setPlaceholderText(tr("Ersetzen durch…"));
m_replaceEdit->setClearButtonEnabled(true);
m_btnReplace = new QPushButton(tr("Ersetzen"), this);
m_btnReplaceAll = new QPushButton(tr("Alle ersetzen"), this);
m_btnReplaceSelection = new QPushButton(tr("In Auswahl ersetzen"), this);
QHBoxLayout *replaceRow = new QHBoxLayout();
replaceRow->addWidget(new QLabel(tr("Ersetzen:"), this));
replaceRow->addWidget(m_replaceEdit, 1);
replaceRow->addWidget(m_btnReplace);
replaceRow->addWidget(m_btnReplaceAll);
replaceRow->addWidget(m_btnReplaceSelection);
m_grid->addLayout(replaceRow, 1, 0);
// ---- Row 2: Optionen ----
m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this);
m_chkWord = new QCheckBox(tr("Ganzes Wort"), this);
m_chkRegex = new QCheckBox(tr("Regulärer Ausdruck"), this);
QHBoxLayout *optRow = new QHBoxLayout();
optRow->addWidget(m_chkCase);
optRow->addWidget(m_chkWord);
optRow->addWidget(m_chkRegex);
optRow->addStretch();
m_grid->addLayout(optRow, 2, 0);
// ---- Connections ----
connect(m_searchEdit, &QLineEdit::textChanged,
this, &SearchPanel::onSearchTextChanged);
connect(m_searchEdit, &QLineEdit::returnPressed,
this, &SearchPanel::findNext);
connect(m_btnNext, &QPushButton::clicked, this, &SearchPanel::findNext);
connect(m_btnPrev, &QPushButton::clicked, this, &SearchPanel::findPrevious);
connect(m_btnReplace, &QPushButton::clicked, this, &SearchPanel::replaceCurrent);
connect(m_btnReplaceAll, &QPushButton::clicked, this, &SearchPanel::replaceAll);
connect(m_btnReplaceSelection, &QPushButton::clicked, this, &SearchPanel::replaceInSelection);
connect(m_btnClose, &QPushButton::clicked, this, &SearchPanel::onCloseClicked);
connect(m_chkCase, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
connect(m_chkWord, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
connect(m_chkRegex, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
}
// ---------------------------------------------------------------------------
// Public interface
// ---------------------------------------------------------------------------
void SearchPanel::setEditor(CodeEditor *editor)
{
clearHighlights();
m_editor = editor;
}
void SearchPanel::activate()
{
show();
m_searchEdit->setFocus();
m_searchEdit->selectAll();
// Pre-fill with selected text if short enough
if (m_editor)
{
const QString sel = m_editor->textCursor().selectedText();
if (!sel.isEmpty() && !sel.contains('\n') && sel.length() < 200)
{
m_searchEdit->setText(sel);
}
}
updateMatchLabel();
}
// ---------------------------------------------------------------------------
// Find helpers
// ---------------------------------------------------------------------------
QTextDocument::FindFlags SearchPanel::buildFindFlags(bool backwards) const
{
QTextDocument::FindFlags flags;
if (backwards) { flags |= QTextDocument::FindBackward; }
if (m_chkCase->isChecked()) { flags |= QTextDocument::FindCaseSensitively; }
if (m_chkWord->isChecked()) { flags |= QTextDocument::FindWholeWords; }
return flags;
}
bool SearchPanel::performFind(bool backwards)
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
return false;
}
const QTextDocument::FindFlags flags = buildFindFlags(backwards);
bool found = false;
if (m_chkRegex->isChecked())
{
QRegularExpression re(m_searchEdit->text());
if (m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::NoPatternOption);
}
else
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
found = m_editor->find(re, flags);
// Wrap around
if (!found)
{
QTextCursor c = m_editor->textCursor();
c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start);
m_editor->setTextCursor(c);
found = m_editor->find(re, flags);
}
}
else
{
found = m_editor->find(m_searchEdit->text(), flags);
// Wrap around
if (!found)
{
QTextCursor c = m_editor->textCursor();
c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start);
m_editor->setTextCursor(c);
found = m_editor->find(m_searchEdit->text(), flags);
}
}
return found;
}
void SearchPanel::highlightAllMatches()
{
if (!m_editor)
{
return;
}
QList<QTextEdit::ExtraSelection> extras;
const QString needle = m_searchEdit->text();
if (needle.isEmpty())
{
m_editor->setExtraSelections(extras);
return;
}
QTextCharFormat fmt;
fmt.setBackground(QColor("#3a3a00"));
fmt.setForeground(QColor("#ffff80"));
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
const QTextDocument::FindFlags flags = buildFindFlags(false);
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
QTextEdit::ExtraSelection sel;
sel.cursor = cursor;
sel.format = fmt;
extras.append(sel);
}
m_editor->setExtraSelections(extras);
}
void SearchPanel::clearHighlights()
{
if (m_editor)
{
m_editor->setExtraSelections({});
}
}
void SearchPanel::updateMatchLabel()
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
m_matchLabel->setText(QString());
return;
}
// Count total matches
int count = 0;
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
const QTextDocument::FindFlags flags = buildFindFlags(false);
const QString needle = m_searchEdit->text();
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
++count;
}
if (count == 0)
{
m_matchLabel->setText(tr("Kein Treffer"));
m_matchLabel->setStyleSheet("color: #cc4444;");
}
else
{
m_matchLabel->setText(tr("%1 Treffer").arg(count));
m_matchLabel->setStyleSheet(QString());
}
}
// ---------------------------------------------------------------------------
// Public slots
// ---------------------------------------------------------------------------
void SearchPanel::findNext()
{
performFind(false);
}
void SearchPanel::findPrevious()
{
performFind(true);
}
void SearchPanel::replaceCurrent()
{
if (!m_editor)
{
return;
}
QTextCursor cursor = m_editor->textCursor();
// If current selection matches the search term, replace it
// Otherwise just find the next occurrence first
const bool hasMatch = !cursor.selectedText().isEmpty();
if (!hasMatch)
{
performFind(false);
return;
}
cursor.insertText(m_replaceEdit->text());
// Move to next match
performFind(false);
updateMatchLabel();
highlightAllMatches();
}
void SearchPanel::replaceAll()
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
return;
}
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
cursor.beginEditBlock();
int count = 0;
const QTextDocument::FindFlags flags = buildFindFlags(false);
const QString needle = m_searchEdit->text();
const QString replacement = m_replaceEdit->text();
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
cursor.insertText(replacement);
++count;
}
cursor.endEditBlock();
updateMatchLabel();
clearHighlights();
QMessageBox::information(this, tr("Alle ersetzen"),
tr("%1 Ersetzung(en) durchgeführt.").arg(count));
}
void SearchPanel::replaceInSelection()
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
return;
}
QTextCursor selCursor = m_editor->textCursor();
if (!selCursor.hasSelection())
{
QMessageBox::information(this, tr("In Auswahl ersetzen"),
tr("Es ist kein Text ausgewählt."));
return;
}
// Work only within the selected region
const int selStart = selCursor.selectionStart();
const int selEnd = selCursor.selectionEnd();
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
cursor.setPosition(selStart);
cursor.beginEditBlock();
int count = 0;
int offset = 0; // Replacement may be longer/shorter than search term
const QTextDocument::FindFlags flags = buildFindFlags(false);
const QString needle = m_searchEdit->text();
const QString replacement = m_replaceEdit->text();
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
// Stop if we've left the original selection
if (cursor.selectionEnd() > selEnd + offset)
{
break;
}
offset += replacement.length() - cursor.selectedText().length();
cursor.insertText(replacement);
++count;
}
cursor.endEditBlock();
updateMatchLabel();
clearHighlights();
QMessageBox::information(this, tr("In Auswahl ersetzen"),
tr("%1 Ersetzung(en) in der Auswahl durchgeführt.").arg(count));
}
// ---------------------------------------------------------------------------
// Private slots
// ---------------------------------------------------------------------------
void SearchPanel::onSearchTextChanged(const QString &/*text*/)
{
highlightAllMatches();
updateMatchLabel();
}
void SearchPanel::onOptionChanged()
{
highlightAllMatches();
updateMatchLabel();
}
void SearchPanel::onCloseClicked()
{
clearHighlights();
m_matchLabel->setText(QString());
hide();
if (m_editor)
{
m_editor->setFocus();
}
}

79
src/editor/SearchPanel.h Normal file
View File

@@ -0,0 +1,79 @@
#pragma once
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QLabel>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QTextDocument>
class CodeEditor;
// ---------------------------------------------------------------------------
// SearchPanel Collapsible find/replace bar that operates on a CodeEditor.
//
// Capabilities:
// • Nächsten / Vorherigen Treffer suchen
// • Einzeln ersetzen
// • Alle ersetzen
// • Nur in Auswahl ersetzen
// • Optionen: Groß-/Kleinschreibung, Ganzes Wort, Reguläre Ausdrücke
// ---------------------------------------------------------------------------
class SearchPanel : public QWidget
{
Q_OBJECT
public:
explicit SearchPanel(QWidget *parent = nullptr);
// Must be called whenever the active editor changes
void setEditor(CodeEditor *editor);
// Toggle visibility and focus the search field
void activate();
public slots:
void findNext();
void findPrevious();
void replaceCurrent();
void replaceAll();
void replaceInSelection();
private slots:
void onSearchTextChanged(const QString &text);
void onOptionChanged(); // Für Checkbox-Signale (bool-Parameter wird ignoriert)
void onCloseClicked();
private:
void setupUi();
QTextDocument::FindFlags buildFindFlags(bool backwards = false) const;
bool performFind(bool backwards = false);
void highlightAllMatches();
void clearHighlights();
void updateMatchLabel();
CodeEditor *m_editor = nullptr;
// Search row
QLineEdit *m_searchEdit = nullptr;
QPushButton *m_btnPrev = nullptr;
QPushButton *m_btnNext = nullptr;
QLabel *m_matchLabel = nullptr;
QPushButton *m_btnClose = nullptr;
// Replace row
QLineEdit *m_replaceEdit = nullptr;
QPushButton *m_btnReplace = nullptr;
QPushButton *m_btnReplaceAll = nullptr;
QPushButton *m_btnReplaceSelection = nullptr;
// Options row
QCheckBox *m_chkCase = nullptr;
QCheckBox *m_chkWord = nullptr;
QCheckBox *m_chkRegex = nullptr;
QGridLayout *m_grid = nullptr;
};