diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..10b4096 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 4.0) +project(Compiler_GUI) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +set(CMAKE_PREFIX_PATH "/opt/homebrew/Cellar/qt/6.9.3") + +find_package(Qt6 COMPONENTS + Core + Gui + Widgets + REQUIRED) + +add_executable(Compiler_GUI main.cpp + mainwindow.cpp + mainwindow.h + ui/filebrowserwidget.cpp + ui/filebrowserwidget.h + ui/codeeditorwidget.cpp + ui/codeeditorwidget.h + backend/compileprocessmanager.cpp + backend/compileprocessmanager.h) +target_link_libraries(Compiler_GUI + Qt::Core + Qt::Gui + Qt::Widgets +) + diff --git a/backend/compileprocessmanager.cpp b/backend/compileprocessmanager.cpp new file mode 100644 index 0000000..45f3279 --- /dev/null +++ b/backend/compileprocessmanager.cpp @@ -0,0 +1,51 @@ +#include "compileprocessmanager.h" + +#include + +CompileProcessManager::CompileProcessManager(QObject *parent) + : QObject(parent) +{ + m_process = new QProcess(this); + connect(m_process, &QProcess::finished, + this, &CompileProcessManager::onProcessFinished); +} + +void CompileProcessManager::runCompile(const QString &sourceCode) +{ + if (m_process->state() != QProcess::NotRunning) { + // 简单处理:如果已经在运行,就先杀掉,或直接返回 + m_process->kill(); + m_process->waitForFinished(); + } + + // TODO: 这里根据你的编译器协议生成指令和参数 + // 假设你已存在一个后端可执行文件 "my_compiler_backend" + QString program = "my_compiler_backend"; + + QStringList args = buildArguments(sourceCode); + + m_process->setProgram(program); + m_process->setArguments(args); + m_process->start(); +} + +QStringList CompileProcessManager::buildArguments(const QString &sourceCode) +{ + QStringList args; + + // TODO: 把 sourceCode 写入临时文件 / 管道 / 特定协议 + // 这里只是示例:假设后端接受一个 -code "xxx" 的参数 + args << "-code" << sourceCode; + + return args; +} + +void CompileProcessManager::onProcessFinished(int exitCode, QProcess::ExitStatus status) +{ + Q_UNUSED(status); + + QString stdoutText = QString::fromLocal8Bit(m_process->readAllStandardOutput()); + QString stderrText = QString::fromLocal8Bit(m_process->readAllStandardError()); + + emit compileFinished(stdoutText, stderrText, exitCode); +} diff --git a/backend/compileprocessmanager.h b/backend/compileprocessmanager.h new file mode 100644 index 0000000..933da5c --- /dev/null +++ b/backend/compileprocessmanager.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include // ✅ 关键:要完整引入 QProcess + +class CompileProcessManager : public QObject +{ + Q_OBJECT +public: + explicit CompileProcessManager(QObject *parent = nullptr); + + // 目前简单接受代码字符串,后面可以加更多参数 + void runCompile(const QString &sourceCode); + + signals: + void compileFinished(const QString &stdoutText, + const QString &stderrText, + int exitCode); + +private slots: + void onProcessFinished(int exitCode, QProcess::ExitStatus status); + +private: + QProcess *m_process = nullptr; + + QStringList buildArguments(const QString &sourceCode); +}; diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..8216d01 --- /dev/null +++ b/main.cpp @@ -0,0 +1,12 @@ +#include +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..2cb9593 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,175 @@ +#include "mainwindow.h" + +#include +#include +#include +#include +#include +#include + +#include "ui/filebrowserwidget.h" +#include "ui/codeeditorwidget.h" +#include "backend/compileprocessmanager.h" + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) +{ + createActions(); + createMenus(); + setupCentralWidgets(); + + m_compileManager = new CompileProcessManager(this); + + connectSignals(); + + setWindowTitle(tr("My Compiler IDE")); + resize(1200, 800); +} + +MainWindow::~MainWindow() +{ +} + +void MainWindow::createActions() +{ + // File + m_actNew = new QAction(tr("&New"), this); + m_actOpen = new QAction(tr("&Open..."), this); + m_actSave = new QAction(tr("&Save"), this); + m_actExit = new QAction(tr("E&xit"), this); + + // Build / Run + m_actBuild = new QAction(tr("&Build"), this); + m_actRun = new QAction(tr("&Run"), this); + + // 可以设置快捷键 + m_actNew->setShortcut(QKeySequence::New); + m_actOpen->setShortcut(QKeySequence::Open); + m_actSave->setShortcut(QKeySequence::Save); + m_actExit->setShortcut(QKeySequence::Quit); + + m_actBuild->setShortcut(Qt::Key_F5); + m_actRun->setShortcut(Qt::Key_F6); +} + +void MainWindow::createMenus() +{ + // File 菜单 + QMenu *fileMenu = menuBar()->addMenu(tr("&File")); + fileMenu->addAction(m_actNew); + fileMenu->addAction(m_actOpen); + fileMenu->addAction(m_actSave); + fileMenu->addSeparator(); + fileMenu->addAction(m_actExit); + + // Edit 菜单(先占位,之后可以加撤销/重做/查找等) + QMenu *editMenu = menuBar()->addMenu(tr("&Edit")); + Q_UNUSED(editMenu); + + // Build 菜单 + QMenu *buildMenu = menuBar()->addMenu(tr("&Build")); + buildMenu->addAction(m_actBuild); + buildMenu->addAction(m_actRun); + + // Help 菜单(占位) + QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); + Q_UNUSED(helpMenu); +} + +void MainWindow::setupCentralWidgets() +{ + // 左右分割:左文件列表、右代码编辑区 + QSplitter *splitter = new QSplitter(Qt::Horizontal, this); + + m_fileBrowser = new FileBrowserWidget(splitter); + m_codeEditor = new CodeEditorWidget(splitter); + + splitter->setStretchFactor(0, 1); // 文件列表 + splitter->setStretchFactor(1, 3); // 代码编辑 + + setCentralWidget(splitter); +} + +void MainWindow::connectSignals() +{ + // 菜单动作 + connect(m_actNew, &QAction::triggered, this, &MainWindow::onActionNewFile); + connect(m_actOpen, &QAction::triggered, this, &MainWindow::onActionOpenFile); + connect(m_actSave, &QAction::triggered, this, &MainWindow::onActionSaveFile); + connect(m_actExit, &QAction::triggered, this, &MainWindow::onActionExit); + + connect(m_actBuild, &QAction::triggered, this, &MainWindow::onActionBuild); + connect(m_actRun, &QAction::triggered, this, &MainWindow::onActionRun); + + // 编译完成信号 + connect(m_compileManager, &CompileProcessManager::compileFinished, + this, &MainWindow::onCompileFinished); + + // 文件列表点击打开文件(后面可以在 FileBrowserWidget 里发信号) + connect(m_fileBrowser, &FileBrowserWidget::fileOpenRequested, + m_codeEditor, &CodeEditorWidget::loadFromFile); +} + +// ========== 菜单槽函数(先简单占位) ========== + +void MainWindow::onActionNewFile() +{ + m_codeEditor->clearEditor(); +} + +void MainWindow::onActionOpenFile() +{ + QString filePath = QFileDialog::getOpenFileName(this, tr("Open File")); + if (filePath.isEmpty()) + return; + + m_codeEditor->loadFromFile(filePath); +} + +void MainWindow::onActionSaveFile() +{ + // 简单实现:如果 CodeEditor 里自己记录了当前文件路径,就直接保存 + // 否则先弹出另存为。这里先调用一个占位接口,具体实现之后我们再做。 + if (!m_codeEditor->save()) + { + QString filePath = QFileDialog::getSaveFileName(this, tr("Save File")); + if (!filePath.isEmpty()) + { + m_codeEditor->saveAs(filePath); + } + } +} + +void MainWindow::onActionExit() +{ + close(); +} + +void MainWindow::onActionBuild() +{ + // 获取当前编辑器中的代码文本 + QString code = m_codeEditor->currentText(); + + // TODO: 这里可以根据 code / 当前选中的编译选项,生成指令、参数等 + // 目前先简单把 code 直接发给后端,后面你告诉我后端协议,我们再细化 + m_compileManager->runCompile(code); +} + +void MainWindow::onActionRun() +{ + // TODO: 之后可以根据编译结果调用可执行文件运行,或者编译+运行 + QMessageBox::information(this, tr("Run"), tr("Run action triggered (TODO).")); +} + +void MainWindow::onCompileFinished(const QString &stdoutText, + const QString &stderrText, + int exitCode) +{ + // 这里先简单弹个窗口,后面我们可以加“底部控制台”面板输出 + QString msg = tr("Exit code: %1\n\nSTDOUT:\n%2\n\nSTDERR:\n%3") + .arg(exitCode) + .arg(stdoutText) + .arg(stderrText); + + QMessageBox::information(this, tr("Build Result"), msg); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..618cbf8 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +class FileBrowserWidget; +class CodeEditorWidget; +class CompileProcessManager; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private slots: + void onActionNewFile(); + void onActionOpenFile(); + void onActionSaveFile(); + void onActionExit(); + + void onActionBuild(); + void onActionRun(); + + // 后端编译结果回调 + void onCompileFinished(const QString &stdoutText, + const QString &stderrText, + int exitCode); + +private: + void createActions(); + void createMenus(); + void setupCentralWidgets(); + void connectSignals(); + +private: + // UI 部件 + FileBrowserWidget *m_fileBrowser = nullptr; + CodeEditorWidget *m_codeEditor = nullptr; + + // 后端模块 + CompileProcessManager *m_compileManager = nullptr; + + // 菜单动作 + QAction *m_actNew = nullptr; + QAction *m_actOpen = nullptr; + QAction *m_actSave = nullptr; + QAction *m_actExit = nullptr; + + QAction *m_actBuild = nullptr; + QAction *m_actRun = nullptr; +}; diff --git a/ui/codeeditorwidget.cpp b/ui/codeeditorwidget.cpp new file mode 100644 index 0000000..cf06c59 --- /dev/null +++ b/ui/codeeditorwidget.cpp @@ -0,0 +1,1305 @@ +#include "codeeditorwidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + + +// ==================== 辅助类:行号区域 ==================== + +class LineNumberArea : public QWidget +{ +public: + explicit LineNumberArea(CodeEditor *editor) + : QWidget(editor) + , m_codeEditor(editor) + {} + + QSize sizeHint() const override + { + return QSize(m_codeEditor->lineNumberAreaWidth(), 0); + } + +protected: + void paintEvent(QPaintEvent *event) override + { + m_codeEditor->lineNumberAreaPaintEvent(event); + } + + void mousePressEvent(QMouseEvent *event) override + { + m_codeEditor->lineNumberAreaMousePressEvent(event); + } + +private: + CodeEditor *m_codeEditor; +}; + +// ==================== 辅助类:C 语言风格高亮 ==================== + +class CSyntaxHighlighter : public QSyntaxHighlighter +{ +public: + explicit CSyntaxHighlighter(QTextDocument *parent = nullptr) + : QSyntaxHighlighter(parent) + { + initRules(); + } + +protected: + void highlightBlock(const QString &text) override + { + // 普通规则 + for (const HighlightingRule &rule : m_highlightingRules) { + QRegularExpressionMatchIterator i = rule.pattern.globalMatch(text); + while (i.hasNext()) { + QRegularExpressionMatch match = i.next(); + setFormat(match.capturedStart(), match.capturedLength(), rule.format); + } + } + + // 多行注释 + setCurrentBlockState(0); + + int startIndex = 0; + if (previousBlockState() != 1) + startIndex = text.indexOf(m_commentStartExpression); + + while (startIndex >= 0) { + QRegularExpressionMatch match = m_commentEndExpression.match(text, startIndex); + int endIndex = match.capturedStart(); + int commentLength; + if (endIndex == -1) { + setCurrentBlockState(1); + commentLength = text.length() - startIndex; + } else { + commentLength = endIndex - startIndex + match.capturedLength(); + } + setFormat(startIndex, commentLength, m_multiLineCommentFormat); + startIndex = text.indexOf(m_commentStartExpression, startIndex + commentLength); + } + } + +private: + struct HighlightingRule + { + QRegularExpression pattern; + QTextCharFormat format; + }; + + void initRules() + { + HighlightingRule rule; + + // 关键字 + QTextCharFormat keywordFormat; + keywordFormat.setForeground(Qt::blue); + keywordFormat.setFontWeight(QFont::Bold); + + const QStringList keywordPatterns = { + QStringLiteral("\\bauto\\b"), + QStringLiteral("\\bbreak\\b"), + QStringLiteral("\\bcase\\b"), + QStringLiteral("\\bchar\\b"), + QStringLiteral("\\bconst\\b"), + QStringLiteral("\\bcontinue\\b"), + QStringLiteral("\\bdefault\\b"), + QStringLiteral("\\bdo\\b"), + QStringLiteral("\\bdouble\\b"), + QStringLiteral("\\belse\\b"), + QStringLiteral("\\benum\\b"), + QStringLiteral("\\bextern\\b"), + QStringLiteral("\\bfloat\\b"), + QStringLiteral("\\bfor\\b"), + QStringLiteral("\\bgoto\\b"), + QStringLiteral("\\bif\\b"), + QStringLiteral("\\binline\\b"), + QStringLiteral("\\bint\\b"), + QStringLiteral("\\blong\\b"), + QStringLiteral("\\bregister\\b"), + QStringLiteral("\\brestrict\\b"), + QStringLiteral("\\breturn\\b"), + QStringLiteral("\\bshort\\b"), + QStringLiteral("\\bsigned\\b"), + QStringLiteral("\\bsizeof\\b"), + QStringLiteral("\\bstatic\\b"), + QStringLiteral("\\bstruct\\b"), + QStringLiteral("\\bswitch\\b"), + QStringLiteral("\\btypedef\\b"), + QStringLiteral("\\bunion\\b"), + QStringLiteral("\\bunsigned\\b"), + QStringLiteral("\\bvoid\\b"), + QStringLiteral("\\bvolatile\\b"), + QStringLiteral("\\bwhile\\b"), + + // C++ / 常用扩展 + QStringLiteral("\\bclass\\b"), + QStringLiteral("\\btemplate\\b"), + QStringLiteral("\\btypename\\b"), + QStringLiteral("\\bnamespace\\b"), + QStringLiteral("\\busing\\b"), + QStringLiteral("\\bbool\\b"), + QStringLiteral("\\btrue\\b"), + QStringLiteral("\\bfalse\\b"), + }; + + for (const QString &pattern : keywordPatterns) { + rule.pattern = QRegularExpression(pattern); + rule.format = keywordFormat; + m_highlightingRules.append(rule); + } + + // 单行注释 // + QTextCharFormat singleLineCommentFormat; + singleLineCommentFormat.setForeground(Qt::darkGreen); + rule.pattern = QRegularExpression(QStringLiteral("//[^\n]*")); + rule.format = singleLineCommentFormat; + m_highlightingRules.append(rule); + + // 字符串 "xxx" + QTextCharFormat quotationFormat; + quotationFormat.setForeground(Qt::darkRed); + rule.pattern = QRegularExpression(QStringLiteral("\".*\"")); + rule.format = quotationFormat; + m_highlightingRules.append(rule); + + // 字符常量 'x' + QTextCharFormat charFormat; + charFormat.setForeground(Qt::darkRed); + rule.pattern = QRegularExpression(QStringLiteral("'[^']*'")); + rule.format = charFormat; + m_highlightingRules.append(rule); + + // 函数名 foo(...) + QTextCharFormat functionFormat; + functionFormat.setForeground(Qt::darkMagenta); + rule.pattern = QRegularExpression(QStringLiteral("\\b[A-Za-z_][A-Za-z0-9_]*(?=\\()")); + rule.format = functionFormat; + m_highlightingRules.append(rule); + + // 多行注释 /* ... */ + m_commentStartExpression = QRegularExpression(QStringLiteral("/\\*")); + m_commentEndExpression = QRegularExpression(QStringLiteral("\\*/")); + + m_multiLineCommentFormat.setForeground(Qt::darkGreen); + m_multiLineCommentFormat.setFontItalic(true); + } + +private: + QVector m_highlightingRules; + QRegularExpression m_commentStartExpression; + QRegularExpression m_commentEndExpression; + QTextCharFormat m_multiLineCommentFormat; +}; + +// ==================== CodeEditor 实现 ==================== + +class CompletionItemDelegate : public QStyledItemDelegate +{ +public: + explicit CompletionItemDelegate(QObject *parent = nullptr) + : QStyledItemDelegate(parent) {} + + void setPrefix(const QString &prefix) { m_prefix = prefix; } + + void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override + { + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + QString text = opt.text; + opt.text.clear(); // 不让默认的绘制文本 + + QStyle *style = opt.widget ? opt.widget->style() + : QApplication::style(); + + // 背景:选中时淡蓝色 + if (opt.state & QStyle::State_Selected) { + painter->fillRect(opt.rect, QColor(173, 216, 230, 200)); // light blue + opt.state &= ~QStyle::State_Selected; + } else if (opt.state & QStyle::State_MouseOver) { + painter->fillRect(opt.rect, QColor(230, 240, 250)); + } + + style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget); + + // 构造 HTML:匹配前缀加粗 + QString escaped = text.toHtmlEscaped(); + QString html; + if (!m_prefix.isEmpty() && + text.startsWith(m_prefix, Qt::CaseInsensitive)) { + int len = m_prefix.length(); + QString head = escaped.left(len); + QString tail = escaped.mid(len); + html = QString("%1%2").arg(head, tail); + } else { + html = escaped; + } + + QTextDocument doc; + doc.setDefaultFont(opt.font); + doc.setHtml(html); + + painter->save(); + painter->setClipRect(opt.rect.adjusted(2, 0, -2, 0)); + painter->translate(opt.rect.left() + 4, opt.rect.top()); + QRectF clip(0, 0, opt.rect.width() - 8, opt.rect.height()); + doc.drawContents(painter, clip); + painter->restore(); + } + +private: + QString m_prefix; +}; + +class CompletionPopup : public QFrame +{ +public: + explicit CompletionPopup(QWidget *parent = nullptr) + : QFrame(parent) + { + setWindowFlags(Qt::Popup | Qt::FramelessWindowHint); + setFrameStyle(QFrame::Box | QFrame::Plain); + + m_view = new QListView(this); + m_model = new QStringListModel(this); + m_delegate = new CompletionItemDelegate(this); + + m_view->setModel(m_model); + m_view->setItemDelegate(m_delegate); + m_view->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_view->setSelectionBehavior(QAbstractItemView::SelectRows); + m_view->setSelectionMode(QAbstractItemView::SingleSelection); + m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_view); + } + + void setWords(const QStringList &words, const QString &prefix) + { + m_model->setStringList(words); + m_delegate->setPrefix(prefix); + + if (words.isEmpty()) { + hide(); + return; + } + + m_view->setCurrentIndex(m_model->index(0, 0)); + resizeToContent(); + } + + bool hasItems() const + { + return m_model->rowCount() > 0; + } + + void selectNext() + { + if (!hasItems()) return; + QModelIndex idx = m_view->currentIndex(); + int row = idx.isValid() ? idx.row() : 0; + row = (row + 1) % m_model->rowCount(); + m_view->setCurrentIndex(m_model->index(row, 0)); + } + + void selectPrevious() + { + if (!hasItems()) return; + QModelIndex idx = m_view->currentIndex(); + int row = idx.isValid() ? idx.row() : 0; + row = (row - 1 + m_model->rowCount()) % m_model->rowCount(); + m_view->setCurrentIndex(m_model->index(row, 0)); + } + + QString currentText() const + { + QModelIndex idx = m_view->currentIndex(); + if (!idx.isValid()) return QString(); + return idx.data(Qt::DisplayRole).toString(); + } + + QListView *view() const { return m_view; } + +protected: + void focusOutEvent(QFocusEvent *event) override + { + QFrame::focusOutEvent(event); + hide(); + } + +private: + void resizeToContent() + { + QFontMetrics fm(font()); + int w = 0; + int h = 0; + int count = m_model->rowCount(); + + for (int i = 0; i < count; ++i) { + QString text = m_model->data(m_model->index(i, 0), + Qt::DisplayRole).toString(); + int width = fm.horizontalAdvance(text) + 16; + w = qMax(w, width); + h += fm.height() + 4; + } + + const int maxVisible = 8; + if (count > maxVisible) { + h = (fm.height() + 4) * maxVisible; + } + + resize(w, h); + } + +private: + QListView *m_view = nullptr; + QStringListModel *m_model = nullptr; + CompletionItemDelegate *m_delegate = nullptr; +}; + + +// ==================== 查找/替换条(位于编辑器上方) ==================== + +class FindReplaceBar : public QWidget +{ +public: + explicit FindReplaceBar(CodeEditor *editor, QWidget *parent = nullptr) + : QWidget(parent) + , m_editor(editor) + { + setAutoFillBackground(true); + QPalette pal = palette(); + pal.setColor(QPalette::Window, QColor(230, 230, 230)); + setPalette(pal); + + m_searchEdit = new QLineEdit(this); + m_searchEdit->setPlaceholderText(QStringLiteral("查找...")); + + m_replaceEdit = new QLineEdit(this); + m_replaceEdit->setPlaceholderText(QStringLiteral("替换为...")); + + m_replaceToggle = new QToolButton(this); + m_replaceToggle->setText(QStringLiteral("替换")); + m_replaceToggle->setCheckable(true); + + m_closeButton = new QToolButton(this); + m_closeButton->setText(QStringLiteral("×")); + + auto *replaceLayout = new QHBoxLayout; + replaceLayout->setContentsMargins(0, 0, 0, 0); + replaceLayout->addWidget(new QLabel(QStringLiteral("替换为:"), this)); + replaceLayout->addWidget(m_replaceEdit); + + m_replacePanel = new QWidget(this); + m_replacePanel->setLayout(replaceLayout); + m_replacePanel->setVisible(false); // 默认隐藏,只有点“替换”才展开 + + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(4, 2, 4, 2); + layout->addWidget(new QLabel(QStringLiteral("查找:"), this)); + layout->addWidget(m_searchEdit, 1); + layout->addWidget(m_replaceToggle); + layout->addWidget(m_replacePanel, 2); + layout->addWidget(m_closeButton); + + // 查找:在查找框按回车 -> searchNext + connect(m_searchEdit, &QLineEdit::returnPressed, + this, [this]() { + if (!m_editor) return; + m_editor->searchNext(m_searchEdit->text()); + }); + + // 切换“替换模式” + connect(m_replaceToggle, &QToolButton::toggled, + this, [this](bool on) { + m_replacePanel->setVisible(on); + }); + + // 关闭条:同时清除高亮 + connect(m_closeButton, &QToolButton::clicked, + this, [this]() { + this->hide(); + if (m_editor) { + m_editor->searchNext(QString()); // 空字符串 -> 清除匹配 + } + }); + + // 在“替换为”输入框按回车 -> 替换当前匹配并跳到下一处 + connect(m_replaceEdit, &QLineEdit::returnPressed, + this, [this]() { + if (!m_editor) return; + const QString searchText = m_searchEdit->text(); + const QString replaceText = m_replaceEdit->text(); + if (searchText.isEmpty()) return; + + QTextCursor cursor = m_editor->textCursor(); + QString selected = cursor.selectedText(); + if (!selected.isEmpty() && + selected.compare(searchText, Qt::CaseInsensitive) == 0) { + cursor.insertText(replaceText); + m_editor->setTextCursor(cursor); + } + + // 替换完后继续查找下一处 + m_editor->searchNext(searchText); + }); + } + + void focusSearch() + { + m_searchEdit->setFocus(); + m_searchEdit->selectAll(); + } + + void setSearchText(const QString &text) + { + if (!text.isEmpty()) + m_searchEdit->setText(text); + } + +private: + CodeEditor *m_editor = nullptr; + QLineEdit *m_searchEdit = nullptr; + QLineEdit *m_replaceEdit = nullptr; + QWidget *m_replacePanel = nullptr; + QToolButton *m_replaceToggle = nullptr; + QToolButton *m_closeButton = nullptr; +}; + + + + +CodeEditor::CodeEditor(QWidget *parent) + : QPlainTextEdit(parent) +{ + m_lineNumberArea = new LineNumberArea(this); + + connect(this, &QPlainTextEdit::blockCountChanged, + this, &CodeEditor::updateLineNumberAreaWidth); + connect(this, &QPlainTextEdit::updateRequest, + this, &CodeEditor::updateLineNumberArea); + connect(this, &QPlainTextEdit::cursorPositionChanged, + this, &CodeEditor::highlightCurrentLine); + + // 默认等宽字体 + QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont); + font.setPointSize(12); + setFont(font); + + // 自动换行,但行号以逻辑行(回车分隔)为准 + setLineWrapMode(QPlainTextEdit::WidgetWidth); + + updateLineNumberAreaWidth(0); + + // C 语言高亮 + m_highlighter = new CSyntaxHighlighter(document()); + + highlightCurrentLine(); + + initCompletion(); +} + +void CodeEditor::initCompletion() +{ + // 简单放一批 C/C++ 关键字和常用单词 + m_completionWords = QStringList{ + "auto","break","case","char","const","continue","default","do","double","else", + "enum","extern","float","for","goto","if","inline","int","long","register", + "restrict","return","short","signed","sizeof","static","struct","switch", + "typedef","union","unsigned","void","volatile","while", + "bool","class","template","typename","namespace","using", + "printf","scanf","main" + }; + + m_completionPopup = new CompletionPopup(this); + m_completionPopup->hide(); + + // 点击候选项时插入补全 + connect(m_completionPopup->view(), &QListView::clicked, + this, [this](const QModelIndex &index) { + const QString text = index.data(Qt::DisplayRole).toString(); + insertCompletion(text); + }); +} + +QString CodeEditor::wordUnderCursor() const +{ + QTextCursor cursor = textCursor(); + QTextBlock block = cursor.block(); + QString text = block.text(); + int posInBlock = cursor.position() - block.position(); + + int start = posInBlock; + while (start > 0) { + QChar ch = text.at(start - 1); + if (ch.isLetterOrNumber() || ch == QLatin1Char('_')) + --start; + else + break; + } + + return text.mid(start, posInBlock - start); +} + +void CodeEditor::showCompletion(const QString &prefix) +{ + if (!m_completionPopup) + return; + + QString p = prefix.trimmed(); + if (p.isEmpty()) { + m_completionPopup->hide(); + return; + } + + QStringList matches; + for (const QString &w : m_completionWords) { + if (w.startsWith(p, Qt::CaseInsensitive)) + matches << w; + } + + if (matches.isEmpty()) { + m_completionPopup->hide(); + return; + } + + m_completionPopup->setWords(matches, p); + + // 计算弹窗位置:光标下方一点 + QRect cr = cursorRect(); + QPoint pos = cr.bottomLeft(); + pos = mapToGlobal(pos); + pos.setY(pos.y() + 2); + + m_completionPopup->move(pos); + m_completionPopup->show(); +} + +void CodeEditor::hideCompletion() +{ + if (m_completionPopup) + m_completionPopup->hide(); +} + +void CodeEditor::insertCompletion(const QString &completion) +{ + if (completion.isEmpty()) + return; + + hideCompletion(); + + QTextCursor cursor = textCursor(); + QTextBlock block = cursor.block(); + QString text = block.text(); + int posInBlock = cursor.position() - block.position(); + + int start = posInBlock; + while (start > 0) { + QChar ch = text.at(start - 1); + if (ch.isLetterOrNumber() || ch == QLatin1Char('_')) + --start; + else + break; + } + + int prefixLen = posInBlock - start; + + cursor.beginEditBlock(); + cursor.setPosition(block.position() + start); + cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, prefixLen); + cursor.removeSelectedText(); + cursor.insertText(completion); + cursor.endEditBlock(); + + setTextCursor(cursor); +} + + +int CodeEditor::lineNumberAreaWidth() const +{ + int digits = 1; + int max = qMax(1, blockCount()); + while (max >= 10) { + max /= 10; + ++digits; + } + + int space = 4 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits; + return space + 6; // 再多一点留白 +} + +void CodeEditor::updateLineNumberAreaWidth(int) +{ + 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); + + QRect cr = contentsRect(); + m_lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), + lineNumberAreaWidth(), cr.height())); +} + +void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event) +{ + QPainter painter(m_lineNumberArea); + painter.fillRect(event->rect(), QColor(245, 245, 245)); + + QTextBlock block = firstVisibleBlock(); + int blockNumber = block.blockNumber(); + int top = static_cast(blockBoundingGeometry(block) + .translated(contentOffset()).top()); + int bottom = top + static_cast(blockBoundingRect(block).height()); + + const int width = m_lineNumberArea->width(); + const QFontMetrics fm(font()); + + while (block.isValid() && top <= event->rect().bottom()) { + if (block.isVisible() && bottom >= event->rect().top()) { + int lineNumber = blockNumber + 1; + + QRect lineRect(0, top, width, fm.height()); + + // 断点背景(淡蓝色) + if (m_breakpoints.contains(lineNumber)) { + QColor bpColor(173, 216, 230, 180); // light blue + painter.fillRect(lineRect, bpColor); + } + + painter.setPen(Qt::gray); + painter.drawText(lineRect.adjusted(0, 0, -4, 0), + Qt::AlignRight | Qt::AlignVCenter, + QString::number(lineNumber)); + } + + block = block.next(); + top = bottom; + bottom = top + static_cast(blockBoundingRect(block).height()); + ++blockNumber; + } +} + +void CodeEditor::lineNumberAreaMousePressEvent(QMouseEvent *event) +{ + int y = event->pos().y(); + + QTextBlock block = firstVisibleBlock(); + int blockNumber = block.blockNumber(); + int top = static_cast(blockBoundingGeometry(block) + .translated(contentOffset()).top()); + int bottom = top + static_cast(blockBoundingRect(block).height()); + + while (block.isValid() && top <= y) { + if (block.isVisible() && y <= bottom) { + int line = blockNumber + 1; + toggleBreakpoint(line); + break; + } + + block = block.next(); + top = bottom; + bottom = top + static_cast(blockBoundingRect(block).height()); + ++blockNumber; + } +} + +void CodeEditor::toggleBreakpoint(int line) +{ + bool enabled; + + if (m_breakpoints.contains(line)) { + m_breakpoints.remove(line); + enabled = false; + } else { + m_breakpoints.insert(line); + enabled = true; + } + + // 只需要刷新行号区域 + m_lineNumberArea->update(); + emit breakpointToggled(line, enabled); +} + +void CodeEditor::highlightCurrentLine() +{ + refreshExtraSelections(); +} + +// 将:当前行高亮 + 自定义行/字符背景,一起统一设置 +void CodeEditor::refreshExtraSelections() +{ + QList selections; + + // 当前行高亮 + if (!isReadOnly()) { + QTextEdit::ExtraSelection selection; + QColor lineColor(232, 242, 254); // 很浅的蓝色 + selection.format.setBackground(lineColor); + selection.format.setProperty(QTextFormat::FullWidthSelection, true); + selection.cursor = textCursor(); + selection.cursor.clearSelection(); + selections.append(selection); + } + + // 行背景 + for (auto it = m_lineBackgrounds.constBegin(); it != m_lineBackgrounds.constEnd(); ++it) { + int line = it.key(); + const LineBackgroundInfo &info = it.value(); + + QTextBlock block = document()->findBlockByNumber(line - 1); + if (!block.isValid()) + continue; + + QTextCursor cursor(block); + cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); + + QTextEdit::ExtraSelection selection; + QColor c = info.color; + c.setAlphaF(qBound(0.0, info.alpha, 1.0)); + selection.format.setBackground(c); + selection.cursor = cursor; + selections.append(selection); + } + + // 字符背景 + for (auto it = m_charBackgrounds.constBegin(); it != m_charBackgrounds.constEnd(); ++it) { + int line = it.key(); + const QVector &infos = it.value(); + + QTextBlock block = document()->findBlockByNumber(line - 1); + if (!block.isValid()) + continue; + + const QString text = block.text(); + int blockStartPos = block.position(); + + for (const CharBackgroundInfo &info : infos) { + if (info.column < 0 || info.column >= text.size()) + continue; + + QTextCursor cursor(document()); + cursor.setPosition(blockStartPos + info.column); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + + QTextEdit::ExtraSelection selection; + QColor c = info.color; + c.setAlphaF(qBound(0.0, info.alpha, 1.0)); + selection.format.setBackground(c); + selection.cursor = cursor; + selections.append(selection); + } + } + + setExtraSelections(selections); +} + + +// =============== 行/字符背景接口实现 =============== + +void CodeEditor::setLineBackground(int line, const QColor &color, qreal alpha) +{ + if (line <= 0) + return; + + LineBackgroundInfo info; + info.color = color; + info.alpha = alpha; + m_lineBackgrounds.insert(line, info); + + refreshExtraSelections(); +} + +void CodeEditor::clearLineBackground(int line) +{ + m_lineBackgrounds.remove(line); + refreshExtraSelections(); +} + +void CodeEditor::clearAllLineBackgrounds() +{ + m_lineBackgrounds.clear(); + refreshExtraSelections(); +} + +void CodeEditor::setCharBackground(int line, int column, const QColor &color, qreal alpha) +{ + if (line <= 0 || column < 0) + return; + + CharBackgroundInfo info; + info.column = column; + info.color = color; + info.alpha = alpha; + + auto &vec = m_charBackgrounds[line]; + vec.append(info); + + refreshExtraSelections(); +} + +void CodeEditor::clearCharBackground(int line, int column) +{ + if (!m_charBackgrounds.contains(line)) + return; + + auto &vec = m_charBackgrounds[line]; + for (int i = 0; i < vec.size(); ++i) { + if (vec[i].column == column) { + vec.removeAt(i); + break; + } + } + + if (vec.isEmpty()) + m_charBackgrounds.remove(line); + + refreshExtraSelections(); +} + +void CodeEditor::clearAllCharBackgrounds() +{ + m_charBackgrounds.clear(); + refreshExtraSelections(); +} + +// =============== 键盘处理:括号补全 & { } 缩进 =============== + +void CodeEditor::keyPressEvent(QKeyEvent *event) +{ + bool completionVisible = (m_completionPopup && m_completionPopup->isVisible()); + + // 如果联想窗口是打开的,先拦截上下键、回车、ESC + if (completionVisible) { + if (event->key() == Qt::Key_Down) { + m_completionPopup->selectNext(); + return; + } else if (event->key() == Qt::Key_Up) { + m_completionPopup->selectPrevious(); + return; + } else if (event->key() == Qt::Key_Return || + event->key() == Qt::Key_Enter || + event->key() == Qt::Key_Tab) { + QString text = m_completionPopup->currentText(); + if (!text.isEmpty()) + insertCompletion(text); + else + hideCompletion(); + return; + } else if (event->key() == Qt::Key_Escape) { + hideCompletion(); + return; + } + } + + // 回车:用我们的缩进逻辑,完成后关闭联想 + if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { + handleReturnKey(event); + hideCompletion(); + return; + } + + // 括号自动补全:(), [], {} + const QString text = event->text(); + if (text == "(" || text == "[" || text == "{") { + QChar closing; + if (text == "(") closing = ')'; + else if (text == "[") closing = ']'; + else closing = '}'; + + QPlainTextEdit::keyPressEvent(event); // 先插入左括号 + + QTextCursor cursor = textCursor(); + cursor.insertText(QString(closing)); + cursor.movePosition(QTextCursor::Left); // 光标移动到中间 + setTextCursor(cursor); + + // 更新联想 + QString prefix = wordUnderCursor(); + if (!prefix.isEmpty()) + showCompletion(prefix); + else + hideCompletion(); + return; + } + + // 默认行为:让基类先处理输入 + QPlainTextEdit::keyPressEvent(event); + + // 根据刚刚输入的字符决定是否弹出联想 + bool isWordChar = !text.isEmpty() + && (text[0].isLetterOrNumber() || text[0] == QLatin1Char('_')); + + if (isWordChar) { + QString prefix = wordUnderCursor(); + if (!prefix.isEmpty()) + showCompletion(prefix); + else + hideCompletion(); + } else { + // 非单词字符:关闭联想 + hideCompletion(); + } +} + + +// 处理回车键: +// - 普通行:复制当前行缩进 +// - 光标在 '{' 后: +// {} +// 变为: +// { +// +// } +void CodeEditor::handleReturnKey(QKeyEvent *event) +{ + QTextCursor cursor = textCursor(); + QTextBlock block = cursor.block(); + QString blockText = block.text(); + int posInBlock = cursor.position() - block.position(); + + // 当前行的基础缩进(行首空白) + int leadingCount = 0; + while (leadingCount < blockText.size() && + blockText.at(leadingCount).isSpace()) { + ++leadingCount; + } + const QString baseIndent = blockText.left(leadingCount); + + // 特殊情况:光标在 {█} 之间(上一字符是 '{',当前字符是 '}') + bool beforeRightBrace = (posInBlock < blockText.size() && + blockText.at(posInBlock) == QLatin1Char('}')); + bool afterLeftBraceInSameLine = (posInBlock > 0 && + blockText.at(posInBlock - 1) == QLatin1Char('{')); + + if (beforeRightBrace && afterLeftBraceInSameLine) { + // 原行内容拆成三行: + // line1: 原来光标之前的内容(包括 '{') + // line2: baseIndent + 一个额外缩进(块内缩进) + // line3: baseIndent + 剩余的 '}' 和后续内容 + QString before = blockText.left(posInBlock); // ...{ + QString after = blockText.mid(posInBlock); // }... + + QString innerIndent = baseIndent + QStringLiteral(" "); // 块内缩进 4 空格 + + QTextCursor editCursor(block); + editCursor.beginEditBlock(); + editCursor.select(QTextCursor::BlockUnderCursor); + editCursor.insertText(before + "\n" + + innerIndent + "\n" + + baseIndent + after); + editCursor.endEditBlock(); + + // 光标放到中间那一行的末尾(空行、缩进之后) + QTextBlock innerBlock = document()->findBlockByNumber(block.blockNumber() + 1); + QTextCursor newCursor(innerBlock); + newCursor.movePosition(QTextCursor::EndOfLine); + setTextCursor(newCursor); + return; + } + + // ---------------- 其它普通情况:保持安全策略 ---------------- + // 1)先让基类正常插入换行(不会改上一行/已有内容) + QPlainTextEdit::keyPressEvent(event); + + // 2)在新行开头插入合适缩进 + cursor = textCursor(); + QTextBlock currentBlock = cursor.block(); + QTextBlock prevBlock = currentBlock.previous(); + + if (!prevBlock.isValid()) { + // 没有上一行(例如第一行),直接结束 + return; + } + + QString prevText = prevBlock.text(); + + int leadingPrev = 0; + while (leadingPrev < prevText.size() && + prevText.at(leadingPrev).isSpace()) { + ++leadingPrev; + } + QString prevIndent = prevText.left(leadingPrev); + + // 判断上一行是否以 '{' 结尾 + int lastNonSpace = prevText.size() - 1; + while (lastNonSpace >= 0 && prevText.at(lastNonSpace).isSpace()) { + --lastNonSpace; + } + bool prevEndsWithLeftBrace = (lastNonSpace >= 0 && + prevText.at(lastNonSpace) == QLatin1Char('{')); + + QString indent = prevIndent; + if (prevEndsWithLeftBrace) { + indent += QStringLiteral(" "); // 多缩进一层 + } + + cursor.insertText(indent); + setTextCursor(cursor); +} + + +// ==================== CodeEditorWidget 外壳 ==================== + +CodeEditorWidget::CodeEditorWidget(QWidget *parent) + : QWidget(parent) +{ + m_editor = new CodeEditor(this); + m_findBar = new FindReplaceBar(m_editor, this); + m_findBar->setVisible(false); // 默认隐藏 + + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_findBar); + layout->addWidget(m_editor); + setLayout(layout); + + // Ctrl+F / Cmd+F -> 打开查找条 + auto *shortcutFind = new QShortcut(QKeySequence::Find, this); + connect(shortcutFind, &QShortcut::activated, + this, [this]() { + if (!m_findBar) return; + + m_findBar->show(); + m_findBar->raise(); + + // 选中文本自动填入查找框 + QString sel = m_editor->textCursor().selectedText(); + if (!sel.isEmpty()) + m_findBar->setSearchText(sel); + + m_findBar->focusSearch(); + }); +} + + +void CodeEditorWidget::loadFromFile(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return; + + QTextStream in(&file); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + in.setEncoding(QStringConverter::Utf8); +#endif + QString text = in.readAll(); + file.close(); + + m_editor->setPlainText(text); + m_currentFilePath = filePath; +} + +bool CodeEditorWidget::save() +{ + if (m_currentFilePath.isEmpty()) + return false; + + return saveAs(m_currentFilePath); +} + +bool CodeEditorWidget::saveAs(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + return false; + + QTextStream out(&file); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + out.setEncoding(QStringConverter::Utf8); +#endif + out << m_editor->toPlainText(); + file.close(); + + m_currentFilePath = filePath; + return true; +} + +QString CodeEditorWidget::currentText() const +{ + return m_editor->toPlainText(); +} + +void CodeEditorWidget::clearEditor() +{ + m_editor->clear(); + m_currentFilePath.clear(); +} + +// 下面这些接口先直接转发到内部 CodeEditor,后面你可以拿来做可视化用 + +void CodeEditorWidget::setLineBackground(int line, const QColor &color, qreal alpha) +{ + m_editor->setLineBackground(line, color, alpha); +} + +void CodeEditorWidget::clearLineBackground(int line) +{ + m_editor->clearLineBackground(line); +} + +void CodeEditorWidget::setCharBackground(int line, int column, const QColor &color, qreal alpha) +{ + m_editor->setCharBackground(line, column, color, alpha); +} + +void CodeEditorWidget::clearCharBackground(int line, int column) +{ + m_editor->clearCharBackground(line, column); +} + +void CodeEditor::searchNext(const QString &text) +{ + QString trimmed = text; + if (trimmed.isEmpty()) { + // 清除所有查找状态和高亮 + m_searchText.clear(); + m_searchMatches.clear(); + m_currentMatchIndex = -1; + m_charBackgrounds.clear(); + refreshExtraSelections(); + return; + } + + // 搜索内容变化 -> 重新构建所有匹配 + if (m_searchText.compare(trimmed, Qt::CaseInsensitive) != 0) { + m_searchText = trimmed; + rebuildSearchMatches(); + m_currentMatchIndex = -1; + } + + if (m_searchMatches.isEmpty()) { + // 没有匹配 -> 清除高亮 + m_charBackgrounds.clear(); + refreshExtraSelections(); + return; + } + + // 循环前进到下一处 + m_currentMatchIndex = (m_currentMatchIndex + 1) % m_searchMatches.size(); + + QTextCursor c = m_searchMatches[m_currentMatchIndex]; + setTextCursor(c); + centerCursor(); + + applySearchHighlights(); +} + +void CodeEditor::rebuildSearchMatches() +{ + m_searchMatches.clear(); + + if (m_searchText.isEmpty()) + return; + + const QString needleLower = m_searchText.toLower(); + const int needleLen = m_searchText.length(); + + QTextBlock block = document()->begin(); + while (block.isValid()) { + QString text = block.text(); + QString lower = text.toLower(); + + int idx = lower.indexOf(needleLower); + while (idx >= 0) { + QTextCursor c(document()); + c.setPosition(block.position() + idx); + c.setPosition(block.position() + idx + needleLen, QTextCursor::KeepAnchor); + m_searchMatches.append(c); + + idx = lower.indexOf(needleLower, idx + needleLen); + } + + block = block.next(); + } +} + +void CodeEditor::applySearchHighlights() +{ + // 先清空之前的“字符级背景”(查找高亮会覆盖之前的字符背景) + m_charBackgrounds.clear(); + + if (m_searchMatches.isEmpty()) { + refreshExtraSelections(); + return; + } + + for (int i = 0; i < m_searchMatches.size(); ++i) { + const QTextCursor &c = m_searchMatches[i]; + int start = c.selectionStart(); + int end = c.selectionEnd(); + int len = end - start; + if (len <= 0) continue; + + // 当前匹配:深黄;其他匹配:浅黄 + QColor color = (i == m_currentMatchIndex) + ? QColor(255, 215, 0, 220) // 深黄色(当前) + : QColor(255, 255, 150, 160); // 淡黄色(其他) + qreal alpha = color.alphaF(); + + for (int offset = 0; offset < len; ++offset) { + int pos = start + offset; + QTextBlock block = document()->findBlock(pos); + if (!block.isValid()) continue; + + int line = block.blockNumber() + 1; // 1-based + int column = pos - block.position(); // 列号 + + CharBackgroundInfo info; + info.column = column; + info.color = color; + info.alpha = alpha; + + m_charBackgrounds[line].append(info); + } + } + + // 一次性刷新所有 ExtraSelection + refreshExtraSelections(); +} + diff --git a/ui/codeeditorwidget.h b/ui/codeeditorwidget.h new file mode 100644 index 0000000..3919aa6 --- /dev/null +++ b/ui/codeeditorwidget.h @@ -0,0 +1,132 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + + + +class LineNumberArea; +class CSyntaxHighlighter; +class CompletionPopup; +class FindReplaceBar; + +// 每个字符的背景信息 +struct CharBackgroundInfo +{ + int column = 0; // 这一行中的列号(从 0 开始) + QColor color; + qreal alpha = 1.0; // 0.0 ~ 1.0 +}; + +// 每一行的背景信息 +struct LineBackgroundInfo +{ + QColor color; + qreal alpha = 1.0; // 0.0 ~ 1.0 +}; + +// 真正的代码编辑器,继承 QPlainTextEdit +class CodeEditor : public QPlainTextEdit +{ + Q_OBJECT +public: + explicit CodeEditor(QWidget *parent = nullptr); + + int lineNumberAreaWidth() const; + + // 预留接口:设置行/字符背景(带透明度) + void setLineBackground(int line, const QColor &color, qreal alpha = 1.0); + void clearLineBackground(int line); + void clearAllLineBackgrounds(); + + void setCharBackground(int line, int column, const QColor &color, qreal alpha = 1.0); + void clearCharBackground(int line, int column); + void clearAllCharBackgrounds(); + + // 行号区域的绘制 & 点击 + void lineNumberAreaPaintEvent(QPaintEvent *event); + void lineNumberAreaMousePressEvent(QMouseEvent *event); + + void searchNext(const QString &text); + + signals: + // 断点状态变化(暂时只发信号,不做具体逻辑) + void breakpointToggled(int line, bool enabled); + +protected: + void resizeEvent(QResizeEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + +private slots: + void updateLineNumberAreaWidth(int newBlockCount); + void updateLineNumberArea(const QRect &rect, int dy); + void highlightCurrentLine(); + + // ✅ 新增:来自联想窗口的补全插入 + void insertCompletion(const QString &completion); + +private: + void refreshExtraSelections(); + void handleReturnKey(QKeyEvent *event); + void toggleBreakpoint(int line); + + // ✅ 新增:联想相关辅助函数 + void initCompletion(); + void showCompletion(const QString &prefix); + void hideCompletion(); + QString wordUnderCursor() const; + void rebuildSearchMatches(); + void applySearchHighlights(); + +private: + LineNumberArea *m_lineNumberArea = nullptr; + CSyntaxHighlighter *m_highlighter = nullptr; + + QSet m_breakpoints; + + QHash m_lineBackgrounds; + QHash> m_charBackgrounds; + + // ✅ 新增:联想窗口和候选词列表 + CompletionPopup *m_completionPopup = nullptr; + QStringList m_completionWords; + + QString m_searchText; + QVector m_searchMatches; + int m_currentMatchIndex = -1; +}; + + + +// 对外仍然叫 CodeEditorWidget,内部包一个 CodeEditor +class CodeEditorWidget : public QWidget +{ + Q_OBJECT +public: + explicit CodeEditorWidget(QWidget *parent = nullptr); + + void loadFromFile(const QString &filePath); + bool save(); + bool saveAs(const QString &filePath); + + QString currentText() const; + void clearEditor(); + + // 代理到内部 CodeEditor 的接口(以后你可以直接拿来做可视化用) + void setLineBackground(int line, const QColor &color, qreal alpha = 1.0); + void clearLineBackground(int line); + + void setCharBackground(int line, int column, const QColor &color, qreal alpha = 1.0); + void clearCharBackground(int line, int column); + +private: + CodeEditor *m_editor = nullptr; + QString m_currentFilePath; + FindReplaceBar *m_findBar = nullptr; +}; diff --git a/ui/filebrowserwidget.cpp b/ui/filebrowserwidget.cpp new file mode 100644 index 0000000..eebdffb --- /dev/null +++ b/ui/filebrowserwidget.cpp @@ -0,0 +1,51 @@ +#include "filebrowserwidget.h" + +#include +#include +#include + +FileBrowserWidget::FileBrowserWidget(QWidget *parent) + : QWidget(parent) +{ + setupUi(); + connectSignals(); + + // 默认浏览当前工作目录 + setRootPath(QDir::currentPath()); +} + +void FileBrowserWidget::setupUi() +{ + m_model = new QFileSystemModel(this); + m_model->setRootPath(QString()); // 稍后在 setRootPath 里设置 + + m_view = new QTreeView(this); + m_view->setModel(m_model); + m_view->setHeaderHidden(true); + m_view->setAnimated(true); + + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_view); + setLayout(layout); +} + +void FileBrowserWidget::connectSignals() +{ + connect(m_view, &QTreeView::doubleClicked, + this, [this](const QModelIndex &index) { + if (!m_model) + return; + QString path = m_model->filePath(index); + if (QFileInfo(path).isFile()) + emit fileOpenRequested(path); + }); +} + +void FileBrowserWidget::setRootPath(const QString &path) +{ + if (!m_model) + return; + QModelIndex idx = m_model->setRootPath(path); + m_view->setRootIndex(idx); +} diff --git a/ui/filebrowserwidget.h b/ui/filebrowserwidget.h new file mode 100644 index 0000000..9bbca52 --- /dev/null +++ b/ui/filebrowserwidget.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +class QTreeView; + +class FileBrowserWidget : public QWidget +{ + Q_OBJECT +public: + explicit FileBrowserWidget(QWidget *parent = nullptr); + + void setRootPath(const QString &path); + + signals: + // 当用户双击或选择某个文件时,通知 MainWindow/CodeEditor 打开 + void fileOpenRequested(const QString &filePath); + +private: + QFileSystemModel *m_model = nullptr; + QTreeView *m_view = nullptr; + + void setupUi(); + void connectSignals(); +};