diff --git a/src/base/ai/abstractllm.cpp b/src/base/ai/abstractllm.cpp index a26e1dfbc..a4a9dc8db 100644 --- a/src/base/ai/abstractllm.cpp +++ b/src/base/ai/abstractllm.cpp @@ -5,7 +5,21 @@ #include "abstractllm.h" AbstractLLM::AbstractLLM(QObject *parent) - : QObject(parent) + : QObject(parent) { qRegisterMetaType("ResponseState"); } + +void AbstractLLM::setModelState(ModelState st) +{ + if (state.loadAcquire() == st) + return; + + state.storeRelease(st); + Q_EMIT modelStateChanged(); +} + +AbstractLLM::ModelState AbstractLLM::modelState() const +{ + return static_cast(state.loadAcquire()); +} diff --git a/src/base/ai/abstractllm.h b/src/base/ai/abstractllm.h index 7a762281e..6d439b4f3 100644 --- a/src/base/ai/abstractllm.h +++ b/src/base/ai/abstractllm.h @@ -22,6 +22,13 @@ class AbstractLLM : public QObject Canceled }; + enum ModelState { + Idle = 0, + Busy + }; + + using ResponseHandler = std::function; + explicit AbstractLLM(QObject *parent = nullptr); virtual ~AbstractLLM() {} @@ -30,18 +37,23 @@ class AbstractLLM : public QObject virtual bool checkValid(QString *errStr) = 0; virtual QJsonObject create(const Conversation &conversation) = 0; virtual void request(const QJsonObject &data) = 0; - virtual void request(const QString &prompt) = 0; + virtual void request(const QString &prompt, ResponseHandler handler = nullptr) = 0; virtual void generate(const QString &prompt, const QString &suffix) = 0; virtual void setTemperature(double temperature) = 0; virtual void setStream(bool isStream) = 0; - virtual void processResponse(QNetworkReply *reply) = 0; virtual void cancel() = 0; virtual void setMaxTokens(int maxToken) = 0; virtual Conversation *getCurrentConversation() = 0; - virtual bool isIdle() = 0; + + void setModelState(ModelState st); + ModelState modelState() const; signals: void dataReceived(const QString &data, ResponseState statu); + void modelStateChanged(); + +private: + QAtomicInt state { Idle }; }; #endif diff --git a/src/common/util/spinnerpainter.cpp b/src/common/util/spinnerpainter.cpp new file mode 100644 index 000000000..8a539bc15 --- /dev/null +++ b/src/common/util/spinnerpainter.cpp @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "spinnerpainter.h" + +#include +#include + +SpinnerPainter::SpinnerPainter() +{ + refreshTimer.setInterval(30); + QObject::connect(&refreshTimer, &QTimer::timeout, &refreshTimer, + [=]() { + currentDegree += 14; + callback(); + }); +} + +void SpinnerPainter::paint(QPainter &painter, const QColor &color, const QRect &rect) +{ + painter.save(); + if (currentColor != color) { + currentColor = color; + indicatorColors.clear(); + } + + if (indicatorColors.isEmpty()) { + for (int i = 0; i < 3; ++i) + indicatorColors << createDefaultIndicatorColorList(color); + } + + painter.setRenderHints(QPainter::Antialiasing); + auto center = QRectF(rect).center(); + auto radius = qMin(rect.width(), rect.height()) / 2.0; + auto indicatorRadius = radius / 2 / 2 * 1.1; + auto indicatorDegreeDelta = 360 / indicatorColors.count(); + + for (int i = 0; i < indicatorColors.count(); ++i) { + auto colors = indicatorColors.value(i); + for (int j = 0; j < colors.count(); ++j) { + double degreeCurrent = currentDegree - j * indicatorShadowOffset + indicatorDegreeDelta * i; + auto x = (radius - indicatorRadius) * qCos(qDegreesToRadians(degreeCurrent)); + auto y = (radius - indicatorRadius) * qSin(qDegreesToRadians(degreeCurrent)); + + x = center.x() + x; + y = center.y() + y; + auto tl = QPointF(x - 1 * indicatorRadius, y - 1 * indicatorRadius); + QRectF rf(tl.x(), tl.y(), indicatorRadius * 2, indicatorRadius * 2); + + QPainterPath path; + path.addEllipse(rf); + + painter.fillPath(path, colors.value(j)); + } + } + painter.restore(); +} + +void SpinnerPainter::setUpdateCallback(const UpdateCallback &cb) +{ + callback = cb; +} + +void SpinnerPainter::startAnimation() +{ + refreshTimer.start(); +} + +void SpinnerPainter::stopAnimation() +{ + refreshTimer.stop(); +} + +QList SpinnerPainter::createDefaultIndicatorColorList(QColor color) +{ + QList colors; + QList opacitys; + opacitys << 100 << 30 << 15 << 10 << 5 << 4 << 3 << 2 << 1; + for (int i = 0; i < opacitys.count(); ++i) { + color.setAlpha(255 * opacitys.value(i) / 100); + colors << color; + } + return colors; +} diff --git a/src/common/util/spinnerpainter.h b/src/common/util/spinnerpainter.h new file mode 100644 index 000000000..d752fc11b --- /dev/null +++ b/src/common/util/spinnerpainter.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef SPINNERPAINTER_H +#define SPINNERPAINTER_H + +#include +#include + +#include + +class SpinnerPainter +{ +public: + SpinnerPainter(); + + void paint(QPainter &painter, const QColor &color, const QRect &rect); + + using UpdateCallback = std::function; + void setUpdateCallback(const UpdateCallback &cb); + + void startAnimation(); + void stopAnimation(); + +protected: + QList createDefaultIndicatorColorList(QColor color); + +private: + QTimer refreshTimer; + + double indicatorShadowOffset = 10; + double currentDegree = 0.0; + + QList> indicatorColors; + QColor currentColor; + UpdateCallback callback; +}; + +#endif // SPINNERPAINTER_H diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index 3d1ff7b82..2f66f01b9 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -46,3 +46,4 @@ add_subdirectory(codegeex) add_subdirectory(git) add_subdirectory(linglong) add_subdirectory(aimanager) +add_subdirectory(smartut) diff --git a/src/plugins/aimanager/aiplugin.cpp b/src/plugins/aimanager/aiplugin.cpp index 0614b0c85..32938b3ee 100644 --- a/src/plugins/aimanager/aiplugin.cpp +++ b/src/plugins/aimanager/aiplugin.cpp @@ -29,8 +29,8 @@ bool AiPlugin::start() auto optionService = dpfGetService(dpfservice::OptionService); if (optionService) { -// TODO:uncomment the code when everything is ok -// optionService->implGenerator(option::GROUP_AI, OptionCustomModelsGenerator::kitName()); +// TODO:uncomment the code when everything is ok + optionService->implGenerator(option::GROUP_AI, OptionCustomModelsGenerator::kitName()); } return true; diff --git a/src/plugins/aimanager/openai/openaicompatiblellm.cpp b/src/plugins/aimanager/openai/openaicompatiblellm.cpp index 9cb510b9c..41495c04d 100644 --- a/src/plugins/aimanager/openai/openaicompatiblellm.cpp +++ b/src/plugins/aimanager/openai/openaicompatiblellm.cpp @@ -41,7 +41,7 @@ QJsonObject parseNonStreamContent(const QByteArray &data) return parseResult; } -class OpenAiCompatibleLLMPrivate +class OpenAiCompatibleLLMPrivate : public QObject { public: OpenAiCompatibleLLMPrivate(OpenAiCompatibleLLM *qq); @@ -49,16 +49,17 @@ class OpenAiCompatibleLLMPrivate QNetworkReply *postMessage(const QString &url, const QString &apiKey, const QByteArray &body); QNetworkReply *getMessage(const QString &url, const QString &apiKey); + void replyMessage(const QString &data, AbstractLLM::ResponseState state, AbstractLLM::ResponseHandler handler); + void processResponse(QNetworkReply *reply, AbstractLLM::ResponseHandler handler = nullptr); QString modelName { "" }; QString modelPath { "" }; QString apiKey { "" }; double temprature { 1.0 }; - int maxTokens = 0; // default not set + int maxTokens = 0; // default not set bool stream { true }; QByteArray httpResult {}; - bool waitingResponse { false }; OpenAiCompatibleConversation *currentConversation = nullptr; QNetworkAccessManager *manager = nullptr; @@ -66,9 +67,9 @@ class OpenAiCompatibleLLMPrivate }; OpenAiCompatibleLLMPrivate::OpenAiCompatibleLLMPrivate(OpenAiCompatibleLLM *qq) - : q(qq) + : q(qq) { - manager = new QNetworkAccessManager(qq); + manager = new QNetworkAccessManager(); currentConversation = new OpenAiCompatibleConversation(); } @@ -86,7 +87,7 @@ QNetworkReply *OpenAiCompatibleLLMPrivate::postMessage(const QString &url, const request.setRawHeader("Authorization", "Bearer " + apiKey.toUtf8()); if (QThread::currentThread() != qApp->thread()) { - QNetworkAccessManager* threadManager(new QNetworkAccessManager); + QNetworkAccessManager *threadManager(new QNetworkAccessManager); OpenAiCompatibleLLM::connect(QThread::currentThread(), &QThread::finished, threadManager, &QNetworkAccessManager::deleteLater); return threadManager->post(request, body); } @@ -101,13 +102,59 @@ QNetworkReply *OpenAiCompatibleLLMPrivate::getMessage(const QString &url, const request.setRawHeader("Authorization", "Bearer " + apiKey.toUtf8()); if (QThread::currentThread() != qApp->thread()) { - QNetworkAccessManager* threadManager(new QNetworkAccessManager); + QNetworkAccessManager *threadManager(new QNetworkAccessManager); OpenAiCompatibleLLM::connect(QThread::currentThread(), &QThread::finished, threadManager, &QNetworkAccessManager::deleteLater); return threadManager->get(request); } return manager->get(request); } +void OpenAiCompatibleLLMPrivate::replyMessage(const QString &data, AbstractLLM::ResponseState state, AbstractLLM::ResponseHandler handler) +{ + if (handler) + handler(data, state); + else + emit q->dataReceived(data, state); +} + +void OpenAiCompatibleLLMPrivate::processResponse(QNetworkReply *reply, AbstractLLM::ResponseHandler handler) +{ + connect(reply, &QNetworkReply::readyRead, this, [=]() { + if (reply->error()) { + qCritical() << "Error:" << reply->errorString(); + replyMessage(reply->errorString(), AbstractLLM::Failed, handler); + } else { + auto data = reply->readAll(); + + // process {"code":,"msg":,"success":false} + QJsonDocument jsonDoc = QJsonDocument::fromJson(data); + if (!jsonDoc.isNull()) { + QJsonObject jsonObj = jsonDoc.object(); + if (jsonObj.contains("success") && !jsonObj.value("success").toBool()) { + replyMessage(jsonObj.value("msg").toString(), AbstractLLM::Failed, handler); + return; + } + } + + httpResult.append(data); + QString content; + QJsonObject retJson; + if (stream) { + retJson = OpenAiCompatibleConversation::parseContentString(QString(data)); + if (retJson.contains("content")) + content = retJson.value("content").toString(); + } else { + retJson = parseNonStreamContent(data); + } + + if (retJson["finish_reason"].toString() == "length") + replyMessage(content, AbstractLLM::ResponseState::CutByLength, handler); + else + replyMessage(retJson["content"].toString(), AbstractLLM::Receiving, handler); + } + }); +} + OpenAiCompatibleLLM::OpenAiCompatibleLLM(QObject *parent) : AbstractLLM(parent), d(new OpenAiCompatibleLLMPrivate(this)) { @@ -170,13 +217,13 @@ bool OpenAiCompatibleLLM::checkValid(QString *errStr) bool valid = false; QString errstr; - connect(this, &AbstractLLM::dataReceived, &loop, [&, this](const QString & data, ResponseState state){ + connect(this, &AbstractLLM::dataReceived, &loop, [&, this](const QString &data, ResponseState state) { if (state == ResponseState::Receiving) return; if (state == ResponseState::Success) { valid = true; - } else if (errStr != nullptr){ + } else if (errStr != nullptr) { *errStr = data; } loop.quit(); @@ -201,38 +248,31 @@ QJsonObject OpenAiCompatibleLLM::create(const Conversation &conversation) void OpenAiCompatibleLLM::request(const QJsonObject &data) { - if (d->waitingResponse) - return; - + setModelState(Busy); QByteArray body = QJsonDocument(data).toJson(); d->httpResult.clear(); - d->waitingResponse = true; d->currentConversation->update(body); QNetworkReply *reply = d->postMessage(modelPath() + "/v1/chat/completions", d->apiKey, body); connect(this, &OpenAiCompatibleLLM::requstCancel, reply, &QNetworkReply::abort); - connect(reply, &QNetworkReply::finished, this, [=](){ - d->waitingResponse = false; + connect(reply, &QNetworkReply::finished, this, [=]() { if (!d->httpResult.isEmpty()) d->currentConversation->update(d->httpResult); if (reply->error()) { qWarning() << "NetWork Error: " << reply->errorString(); emit dataReceived(reply->errorString(), AbstractLLM::ResponseState::Failed); - return; + } else { + emit dataReceived("", AbstractLLM::ResponseState::Success); } - emit dataReceived("", AbstractLLM::ResponseState::Success); + setModelState(Idle); }); - processResponse(reply); + d->processResponse(reply); } -void OpenAiCompatibleLLM::request(const QString &prompt) +void OpenAiCompatibleLLM::request(const QString &prompt, ResponseHandler handler) { - if (d->waitingResponse) - return; - - d->waitingResponse = true; - + setModelState(Busy); QJsonObject dataObject; dataObject.insert("model", d->modelName); dataObject.insert("prompt", prompt); @@ -241,28 +281,24 @@ void OpenAiCompatibleLLM::request(const QString &prompt) if (d->maxTokens != 0) dataObject.insert("max_tokens", d->maxTokens); - QNetworkReply *reply = d->postMessage(modelPath() + "/v1/completions", d->apiKey, QJsonDocument(dataObject).toJson()); + QNetworkReply *reply = d->postMessage(modelPath() + "/completions", d->apiKey, QJsonDocument(dataObject).toJson()); connect(this, &OpenAiCompatibleLLM::requstCancel, reply, &QNetworkReply::abort); - connect(reply, &QNetworkReply::finished, this, [=](){ - d->waitingResponse = false; + connect(reply, &QNetworkReply::finished, this, [=]() { if (reply->error()) { qWarning() << "NetWork Error: " << reply->errorString(); - emit dataReceived(reply->errorString(), AbstractLLM::ResponseState::Failed); - return; + d->replyMessage(reply->errorString(), AbstractLLM::Failed, handler); + } else { + d->replyMessage(reply->errorString(), AbstractLLM::Success, handler); } - emit dataReceived("", AbstractLLM::ResponseState::Success); + setModelState(AbstractLLM::Idle); }); - processResponse(reply); + d->processResponse(reply, handler); } void OpenAiCompatibleLLM::generate(const QString &prompt, const QString &suffix) { - if (d->waitingResponse) - return; - - d->waitingResponse = true; - + setModelState(Busy); QJsonObject dataObject; dataObject.insert("model", d->modelName); dataObject.insert("suffix", suffix); @@ -274,17 +310,17 @@ void OpenAiCompatibleLLM::generate(const QString &prompt, const QString &suffix) QNetworkReply *reply = d->postMessage(modelPath() + "/api/generate", d->apiKey, QJsonDocument(dataObject).toJson()); connect(this, &OpenAiCompatibleLLM::requstCancel, reply, &QNetworkReply::abort); - connect(reply, &QNetworkReply::finished, this, [=](){ - d->waitingResponse = false; + connect(reply, &QNetworkReply::finished, this, [=]() { if (reply->error()) { qWarning() << "NetWork Error: " << reply->errorString(); emit dataReceived(reply->errorString(), AbstractLLM::ResponseState::Failed); - return; + } else { + emit dataReceived("", AbstractLLM::ResponseState::Success); } - emit dataReceived("", AbstractLLM::ResponseState::Success); + setModelState(AbstractLLM::Idle); }); - processResponse(reply); + d->processResponse(reply); } void OpenAiCompatibleLLM::setTemperature(double temperature) @@ -297,47 +333,9 @@ void OpenAiCompatibleLLM::setStream(bool isStream) d->stream = isStream; } -void OpenAiCompatibleLLM::processResponse(QNetworkReply *reply) -{ - connect(reply, &QNetworkReply::readyRead, this, [=]() { - if (reply->error()) { - qCritical() << "Error:" << reply->errorString(); - emit dataReceived(reply->errorString(), AbstractLLM::ResponseState::Failed); - } else { - auto data = reply->readAll(); - - // process {"code":,"msg":,"success":false} - QJsonDocument jsonDoc = QJsonDocument::fromJson(data); - if (!jsonDoc.isNull()) { - QJsonObject jsonObj = jsonDoc.object(); - if (jsonObj.contains("success") && !jsonObj.value("success").toBool()) { - emit dataReceived(jsonObj.value("msg").toString(), AbstractLLM::ResponseState::Failed); - return; - } - } - - d->httpResult.append(data); - QString content; - QJsonObject retJson; - if (d->stream) { - retJson = OpenAiCompatibleConversation::parseContentString(QString(data)); - if (retJson.contains("content")) - content = retJson.value("content").toString(); - } else { - retJson = parseNonStreamContent(data); - } - - if (retJson["finish_reason"].toString() == "length") - emit dataReceived(content, AbstractLLM::ResponseState::CutByLength); - else - emit dataReceived(retJson["content"].toString(), AbstractLLM::ResponseState::Receiving); - } - }); -} - void OpenAiCompatibleLLM::cancel() { - d->waitingResponse = false; + setModelState(AbstractLLM::Idle); d->httpResult.clear(); emit requstCancel(); emit dataReceived("", AbstractLLM::ResponseState::Canceled); @@ -347,8 +345,3 @@ void OpenAiCompatibleLLM::setMaxTokens(int maxTokens) { d->maxTokens = maxTokens; } - -bool OpenAiCompatibleLLM::isIdle() -{ - return !d->waitingResponse; -} diff --git a/src/plugins/aimanager/openai/openaicompatiblellm.h b/src/plugins/aimanager/openai/openaicompatiblellm.h index b83becd38..90e56fad5 100644 --- a/src/plugins/aimanager/openai/openaicompatiblellm.h +++ b/src/plugins/aimanager/openai/openaicompatiblellm.h @@ -5,7 +5,7 @@ #include #ifndef OPENAICOMPATIBLELLM_H -#define OPENAICOMPATIBLELLM_H +# define OPENAICOMPATIBLELLM_H class OpenAiCompatibleLLMPrivate; class OpenAiCompatibleLLM : public AbstractLLM @@ -15,7 +15,7 @@ class OpenAiCompatibleLLM : public AbstractLLM explicit OpenAiCompatibleLLM(QObject *parent = nullptr); ~OpenAiCompatibleLLM() override; - Conversation* getCurrentConversation() override; + Conversation *getCurrentConversation() override; void setModelName(const QString &modelName); void setModelPath(const QString &path); void setApiKey(const QString &apiKey); @@ -24,15 +24,13 @@ class OpenAiCompatibleLLM : public AbstractLLM QString modelPath() const override; bool checkValid(QString *errStr) override; QJsonObject create(const Conversation &conversation) override; - void request(const QJsonObject &data) override; // v1/chat/compltions - void request(const QString &prompt) override; // v1/completions - void generate(const QString &prompt, const QString &suffix) override; // api/generate + void request(const QJsonObject &data) override; // v1/chat/compltions + void request(const QString &prompt, ResponseHandler handler = nullptr) override; // v1/completions + void generate(const QString &prompt, const QString &suffix) override; // api/generate void setTemperature(double temperature) override; - void setStream(bool isStream) override; - void processResponse(QNetworkReply *reply) override; + void setStream(bool isStream) override; void cancel() override; void setMaxTokens(int maxTokens) override; - bool isIdle() override; signals: void requstCancel(); @@ -41,4 +39,4 @@ class OpenAiCompatibleLLM : public AbstractLLM OpenAiCompatibleLLMPrivate *d; }; -#endif // OPENAICOMPATIBLELLM_H +#endif // OPENAICOMPATIBLELLM_H diff --git a/src/plugins/builder/CMakeLists.txt b/src/plugins/builder/CMakeLists.txt index 39c826065..7130d0c59 100644 --- a/src/plugins/builder/CMakeLists.txt +++ b/src/plugins/builder/CMakeLists.txt @@ -2,43 +2,19 @@ cmake_minimum_required(VERSION 3.0.2) project(buildercore) -set(CXX_CPP - tasks/taskmodel.cpp - tasks/taskmanager.cpp - tasks/taskdelegate.cpp - tasks/taskview.cpp - tasks/taskfilterproxymodel.cpp - transceiver/builderreceiver.cpp - transceiver/buildersender.cpp - mainframe/buildmanager.cpp - mainframe/compileoutputpane.cpp - mainframe/problemoutputpane.cpp - mainframe/commonparser.cpp - buildercore.cpp - buildercore.json - ) - -set(CXX_H - tasks/taskmodel.h - tasks/taskmanager.h - tasks/taskdelegate.h - tasks/taskview.h - tasks/taskfilterproxymodel.h - transceiver/builderreceiver.h - transceiver/buildersender.h - mainframe/buildmanager.h - mainframe/compileoutputpane.h - mainframe/problemoutputpane.h - mainframe/commonparser.h - buildercore.h - ) +FILE(GLOB SRC_FILES + "${CMAKE_CURRENT_SOURCE_DIR}/*.h" + "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/*/*.h" + "${CMAKE_CURRENT_SOURCE_DIR}/*/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/*.json" +) add_library(${PROJECT_NAME} SHARED - ${CXX_H} - ${CXX_CPP} + ${SRC_FILES} builder.qrc - ) +) target_link_libraries(${PROJECT_NAME} duc-framework @@ -47,7 +23,7 @@ target_link_libraries(${PROJECT_NAME} duc-common ${QtUseModules} ${PkgUserModules} - ) +) install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${PLUGIN_INSTALL_PATH}) diff --git a/src/plugins/builder/buildercore.cpp b/src/plugins/builder/buildercore.cpp index 1eda6dcf6..1de128bc5 100644 --- a/src/plugins/builder/buildercore.cpp +++ b/src/plugins/builder/buildercore.cpp @@ -45,4 +45,3 @@ dpf::Plugin::ShutdownFlag BuilderCore::stop() { return Sync; } - diff --git a/src/plugins/builder/mainframe/buildmanager.cpp b/src/plugins/builder/mainframe/buildmanager.cpp index e49890e53..00a00caa6 100644 --- a/src/plugins/builder/mainframe/buildmanager.cpp +++ b/src/plugins/builder/mainframe/buildmanager.cpp @@ -15,6 +15,7 @@ #include "compileoutputpane.h" #include "tasks/taskmodel.h" #include "common/util/utils.h" +#include "settingdialog.h" #include "services/builder/builderservice.h" #include "services/editor/editorservice.h" @@ -179,12 +180,20 @@ void BuildManager::initIssueList() filterButton->setContentsMargins(0, 0, 0, 0); filterButton->setToolTip(tr("Filter")); + DToolButton *settingButton = new DToolButton(d->compileWidget); + settingButton->setFixedSize(26, 26); + settingButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + settingButton->setIcon(QIcon::fromTheme("settings")); + settingButton->setContentsMargins(0, 0, 0, 0); + settingButton->setToolTip(tr("Settings")); + DFrame *issueTopWidget = new DFrame(d->compileWidget); DStyle::setFrameRadius(issueTopWidget, 0); issueTopWidget->setLineWidth(0); issueTopWidget->setFixedHeight(36); QHBoxLayout *hIssueTopLayout = new QHBoxLayout(issueTopWidget); hIssueTopLayout->addWidget(issusListText); + hIssueTopLayout->addWidget(settingButton); hIssueTopLayout->addWidget(filterButton); hIssueTopLayout->setSpacing(0); hIssueTopLayout->setContentsMargins(0, 0, 5, 0); @@ -236,6 +245,7 @@ void BuildManager::initIssueList() QPoint menuPos = buttonPos + QPoint(0, 5); filterMenu->popup(menuPos); }); + connect(settingButton, &DToolButton::clicked, this, &BuildManager::showSettingDialog); } void BuildManager::initCompileOutput() @@ -408,6 +418,12 @@ void BuildManager::slotResetBuildUI() uiController.switchContext(tr("&Build")); } +void BuildManager::showSettingDialog() +{ + SettingDialog dlg; + dlg.exec(); +} + void BuildManager::setActivatedProjectInfo(const QString &kitName, const QString &workingDir) { d->activedKitName = kitName; diff --git a/src/plugins/builder/mainframe/buildmanager.h b/src/plugins/builder/mainframe/buildmanager.h index 6968491e8..c20812238 100644 --- a/src/plugins/builder/mainframe/buildmanager.h +++ b/src/plugins/builder/mainframe/buildmanager.h @@ -58,6 +58,7 @@ public slots: void slotBuildState(const BuildState &buildstate); void slotOutputNotify(const BuildState &state, const BuildCommandInfo &commandInfo); void slotResetBuildUI(); + void showSettingDialog(); private: explicit BuildManager(QObject *parent = nullptr); diff --git a/src/plugins/builder/mainframe/problemoutputpane.cpp b/src/plugins/builder/mainframe/problemoutputpane.cpp index 16720876a..c8fe37763 100644 --- a/src/plugins/builder/mainframe/problemoutputpane.cpp +++ b/src/plugins/builder/mainframe/problemoutputpane.cpp @@ -11,7 +11,7 @@ #include ProblemOutputPane::ProblemOutputPane(QWidget *parent) - : DWidget (parent) + : DWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -20,7 +20,6 @@ ProblemOutputPane::ProblemOutputPane(QWidget *parent) ProblemOutputPane::~ProblemOutputPane() { - } void ProblemOutputPane::clearContents() @@ -37,27 +36,3 @@ void ProblemOutputPane::showSpecificTasks(ShowType type) { TaskManager::instance()->showSpecificTasks(type); } - -void ProblemOutputPane::contextMenuEvent(QContextMenuEvent * event) -{ - if (nullptr == menu) { - menu = new DMenu(this); - menu->setParent(this); - menu->addActions(actionFactory()); - } - - menu->move(event->globalX(), event->globalY()); - menu->exec(); -} - -QList ProblemOutputPane::actionFactory() -{ - QList list; - auto action = new QAction(this); - action->setText(tr("Clear")); - connect(action, &QAction::triggered, [this](){ - clearContents(); - }); - list.append(action); - return list; -} diff --git a/src/plugins/builder/mainframe/problemoutputpane.h b/src/plugins/builder/mainframe/problemoutputpane.h index 8076be54e..bf1cb6bb4 100644 --- a/src/plugins/builder/mainframe/problemoutputpane.h +++ b/src/plugins/builder/mainframe/problemoutputpane.h @@ -9,7 +9,6 @@ #include "tasks/taskmodel.h" #include -#include DWIDGET_USE_NAMESPACE @@ -23,13 +22,6 @@ class ProblemOutputPane : public DWidget void clearContents(); void addTask(const Task &task, int linkedOutputLines, int skipLines); void showSpecificTasks(ShowType type); - -protected: - void contextMenuEvent(QContextMenuEvent * event) override; - -private: - QList actionFactory(); - DMenu *menu = nullptr; }; -#endif // PROBLEMOUTPUTPANE_H +#endif // PROBLEMOUTPUTPANE_H diff --git a/src/plugins/builder/mainframe/settingdialog.cpp b/src/plugins/builder/mainframe/settingdialog.cpp new file mode 100644 index 000000000..2f10acdeb --- /dev/null +++ b/src/plugins/builder/mainframe/settingdialog.cpp @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "settingdialog.h" + +#include "services/option/optionmanager.h" + +#include +#include +#include + +#include + +DWIDGET_USE_NAMESPACE + +SettingDialog::SettingDialog(QWidget *parent) + : DDialog(parent) +{ + initUI(); + initConnection(); + updateSettings(); +} + +void SettingDialog::initUI() +{ + setFixedSize(550, 600); + setIcon(QIcon::fromTheme("ide")); + setWindowTitle(tr("Settings")); + setOnButtonClickedClose(false); + + auto createButton = [this](const QIcon &icon, const QString &tips) { + auto btn = new QPushButton(this); + btn->setIconSize({ 16, 16 }); + btn->setIcon(icon); + btn->setToolTip(tips); + return btn; + }; + + DFrame *contentFrame = new DFrame(this); + QVBoxLayout *mainLayout = new QVBoxLayout(contentFrame); + + DLabel *label = new DLabel(tr("Select Prompt"), this); + promptCB = new QComboBox(this); + promptEdit = new QTextEdit(this); + promptEdit->setFrameShape(QFrame::NoFrame); + promptEdit->viewport()->installEventFilter(this); + promptEdit->installEventFilter(this); + addBtn = createButton(DStyle::standardIcon(style(), DStyle::SP_IncreaseElement), tr("Add Prompt")); + delBtn = createButton(DStyle::standardIcon(style(), DStyle::SP_DecreaseElement), tr("Delete Prompt")); + + QHBoxLayout *topLayout = new QHBoxLayout; + topLayout->addWidget(label); + topLayout->addWidget(promptCB, 1); + + QVBoxLayout *btnLayout = new QVBoxLayout; + btnLayout->addWidget(addBtn); + btnLayout->addWidget(delBtn); + btnLayout->addStretch(1); + + QHBoxLayout *layout = new QHBoxLayout; + layout->addWidget(promptEdit); + layout->addLayout(btnLayout); + + mainLayout->addLayout(topLayout); + mainLayout->addLayout(layout); + + addContent(contentFrame); + addButton(tr("Cancel", "button")); + addButton(tr("OK", "button"), true, DDialog::ButtonRecommend); +} + +void SettingDialog::initConnection() +{ + connect(promptCB, &QComboBox::currentTextChanged, this, + [this] { + promptEdit->setPlainText(promptCB->currentData().toString()); + bool isDefault = promptCB->currentIndex() == 0; + delBtn->setEnabled(!isDefault); + promptEdit->setReadOnly(isDefault); + }); + connect(addBtn, &QPushButton::clicked, this, &SettingDialog::handleAddPrompt); + connect(delBtn, &QPushButton::clicked, this, &SettingDialog::handleDeletePrompt); +} + +void SettingDialog::updateSettings() +{ + promptCB->addItem("default", defaultIssueFixPrompt()); + const auto &prompts = OptionManager::getInstance()->getValue("Builder", "Prompts").toMap(); + for (auto iter = prompts.cbegin(); iter != prompts.cend(); ++iter) { + promptCB->addItem(iter.key(), iter.value()); + } + + const auto &curPrompt = OptionManager::getInstance()->getValue("Builder", "CurrentPrompt").toString(); + if (!curPrompt.isEmpty()) + promptCB->setCurrentText(curPrompt); +} + +void SettingDialog::handleAddPrompt() +{ + DDialog dlg(this); + dlg.setIcon(QIcon::fromTheme("ide")); + dlg.setWindowTitle(tr("Add Prompt")); + + DLineEdit *edit = new DLineEdit(&dlg); + edit->setPlaceholderText(tr("Please input the name of the prompt")); + dlg.addContent(edit); + dlg.addButton(tr("Cancel", "button")); + dlg.addButton(tr("OK", "button"), true, DDialog::ButtonRecommend); + dlg.getButton(1)->setEnabled(false); + dlg.setOnButtonClickedClose(false); + dlg.setFocusProxy(edit); + + connect(edit, &DLineEdit::textChanged, this, + [edit, &dlg] { + if (edit->isAlert()) + edit->setAlert(false); + dlg.getButton(1)->setEnabled(!edit->text().isEmpty()); + }); + connect(&dlg, &DDialog::buttonClicked, this, + [this, edit, &dlg](int index) { + if (index == 1) { + const auto &name = edit->text(); + if (promptCB->findText(name) != -1) { + edit->setAlert(true); + edit->showAlertMessage(tr("A prompt named \"%1\" already exists").arg(name)); + edit->lineEdit()->selectAll(); + edit->setFocus(); + return; + } else { + promptCB->addItem(name); + promptCB->setCurrentText(name); + } + } + dlg.close(); + }); + + dlg.exec(); +} + +void SettingDialog::handleDeletePrompt() +{ + DDialog dlg(this); + dlg.setIcon(QIcon::fromTheme("ide")); + dlg.setWindowTitle(tr("Delete Prompt")); + dlg.setMessage(tr("Are you sure you want to delete the \"%1\" prompt").arg(promptCB->currentText())); + dlg.addButton(tr("Cancel", "button")); + dlg.addButton(tr("OK", "button"), true, DDialog::ButtonRecommend); + + int ret = dlg.exec(); + if (ret == 1) + promptCB->removeItem(promptCB->currentIndex()); +} + +void SettingDialog::handleButtonClicked(int index) +{ + if (index != 1) + return reject(); + + QVariantMap prompts; + for (int i = 1; i < promptCB->count() - 1; ++i) { // skip the default prompt + prompts[promptCB->itemText(i)] = promptCB->itemData(i); + } + + if (!prompts.isEmpty()) { + OptionManager::getInstance()->setValue("Builder", "Prompts", prompts); + OptionManager::getInstance()->setValue("Builder", "CurrentPrompt", promptCB->currentText()); + } + + accept(); +} + +bool SettingDialog::eventFilter(QObject *obj, QEvent *e) +{ + if (promptEdit && obj == promptEdit->viewport() && e->type() == QEvent::Paint) { + QPainter painter(promptEdit->viewport()); + painter.setRenderHint(QPainter::Antialiasing); + + auto p = promptEdit->viewport()->palette(); + painter.setPen(Qt::NoPen); + painter.setBrush(p.brush(QPalette::Active, QPalette::AlternateBase)); + + QPainterPath path; + path.addRoundedRect(promptEdit->viewport()->rect(), 8, 8); + painter.drawPath(path); + } else if (promptEdit && obj == promptEdit && e->type() == QEvent::FocusOut && promptCB) { + promptCB->setItemData(promptCB->currentIndex(), promptEdit->toPlainText()); + } + + return DDialog::eventFilter(obj, e); +} + +QString SettingDialog::defaultIssueFixPrompt() +{ + return "How can I resolve this? If you propose a fix, please make it concise." + "For the code present, we get this error:"; +} diff --git a/src/plugins/builder/mainframe/settingdialog.h b/src/plugins/builder/mainframe/settingdialog.h new file mode 100644 index 000000000..857c2c132 --- /dev/null +++ b/src/plugins/builder/mainframe/settingdialog.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef SETTINGDIALOG_H +#define SETTINGDIALOG_H + +#include + +#include +#include +#include + +class SettingDialog : public DTK_WIDGET_NAMESPACE::DDialog +{ + Q_OBJECT +public: + explicit SettingDialog(QWidget *parent = nullptr); + + static QString defaultIssueFixPrompt(); + +protected: + bool eventFilter(QObject *obj, QEvent *e) override; + +private: + void initUI(); + void initConnection(); + void updateSettings(); + + void handleAddPrompt(); + void handleDeletePrompt(); + void handleButtonClicked(int index); + +private: + QComboBox *promptCB { nullptr }; + QPushButton *addBtn { nullptr }; + QPushButton *delBtn { nullptr }; + QTextEdit *promptEdit { nullptr }; +}; + +#endif // SETTINGDIALOG_H diff --git a/src/plugins/builder/tasks/taskmanager.cpp b/src/plugins/builder/tasks/taskmanager.cpp index 87bcf55c2..191887214 100644 --- a/src/plugins/builder/tasks/taskmanager.cpp +++ b/src/plugins/builder/tasks/taskmanager.cpp @@ -3,8 +3,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "taskmanager.h" +#include "mainframe/settingdialog.h" #include "transceiver/buildersender.h" #include "common/common.h" +#include "services/option/optionmanager.h" + +#include TaskManager *TaskManager::instance() { @@ -22,7 +26,8 @@ void TaskManager::clearTasks() model->clearTasks(); } -TaskManager::TaskManager(QObject *parent) : QObject(parent) +TaskManager::TaskManager(QObject *parent) + : QObject(parent) { view = new TaskView(); model.reset(new TaskModel()); @@ -34,6 +39,7 @@ TaskManager::TaskManager(QObject *parent) : QObject(parent) view->setFrameStyle(QFrame::NoFrame); view->setSelectionMode(QAbstractItemView::SingleSelection); + view->setContextMenuPolicy(Qt::CustomContextMenu); connect(view->selectionModel(), &QItemSelectionModel::currentChanged, tld, &TaskDelegate::currentChanged); @@ -42,6 +48,36 @@ TaskManager::TaskManager(QObject *parent) : QObject(parent) this, &TaskManager::currentChanged); connect(view, &QAbstractItemView::activated, this, &TaskManager::triggerDefaultHandler); + connect(view, &TaskView::customContextMenuRequested, + this, &TaskManager::showContextMenu); +} + +QString TaskManager::readContext(const QString &path, int codeLine) +{ + QStringList context; + int startLine = qMax(0, codeLine - 3); + int endLine = codeLine + 3; + QFile file(path); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QTextStream in(&file); + int line = 0; + while (!in.atEnd()) { + const QString lineContent = in.readLine(); + if (line >= startLine && line <= endLine) + context.append(lineContent); + ++line; + } + endLine = line - 1; + file.close(); + } + + if (!context.isEmpty()) { + QString prefix("```%1 (%2-%3)"); + context.prepend(prefix.arg(path, QString::number(startLine + 1), QString::number(endLine + 1))); + context.append("```"); + } + + return context.join('\n'); } void TaskManager::slotAddTask(const Task &task, int linkedOutputLines, int skipLines) @@ -57,6 +93,31 @@ void TaskManager::showSpecificTasks(ShowType type) filterModel->setFilterType(type); } +void TaskManager::showContextMenu() +{ + QMenu menu; + menu.addAction(tr("Clear"), this, &TaskManager::clearTasks); + menu.addAction(tr("Fix Issue"), this, &TaskManager::fixIssueWithAi); + + menu.exec(QCursor::pos()); +} + +void TaskManager::fixIssueWithAi() +{ + const auto &task = model->task(view->currentIndex()); + if (task.isNull()) + return; + + QString context; + if (!task.file.toString().isEmpty() && task.line > 0) + context = readContext(task.file.toString(), task.line - 1); + + QString prompt; + prompt += "\n\nFor the code present, we get this error:\n```\n"; + prompt += task.description; + prompt += "\n```\nHow can I resolve this? If you propose a fix, please make it concise."; +} + void TaskManager::currentChanged(const QModelIndex &index) { Q_UNUSED(index) diff --git a/src/plugins/builder/tasks/taskmanager.h b/src/plugins/builder/tasks/taskmanager.h index 2921309f4..f1baf11d2 100644 --- a/src/plugins/builder/tasks/taskmanager.h +++ b/src/plugins/builder/tasks/taskmanager.h @@ -33,13 +33,17 @@ public slots: void triggerDefaultHandler(const QModelIndex &index); void showSpecificTasks(ShowType type); + void showContextMenu(); + void fixIssueWithAi(); private: explicit TaskManager(QObject *parent = nullptr); + QString readContext(const QString &path, int codeLine); + TaskView *view = nullptr; QSharedPointer model; QSharedPointer filterModel; }; -#endif // TASKMANAGER_H +#endif // TASKMANAGER_H diff --git a/src/plugins/core/gui/workspacewidget.cpp b/src/plugins/core/gui/workspacewidget.cpp index 83b797254..35700aa9f 100644 --- a/src/plugins/core/gui/workspacewidget.cpp +++ b/src/plugins/core/gui/workspacewidget.cpp @@ -47,6 +47,14 @@ void WorkspaceWidget::registerToolBtnToWidget(DToolButton *btn, const QString &t if (!btn) return; + if (!btn->parent()) + btn->setParent(this); + + if (btn->isHidden()) + toolBtnState[btn] = false; + else + toolBtnState[btn] = true; + toolBtnOfWidget.insert(title, btn); } @@ -68,18 +76,14 @@ void WorkspaceWidget::switchWidgetWorkspace(const QString &title) emit expandStateChange(widget->property("canExpand").toBool()); - if (!addedToController || (lastTitle == title)) - return; - //update toolbtn`s state at header for (auto btn : getToolBtnByTitle(lastTitle)) { - toolBtnState[btn] = btn->isVisible(); + toolBtnState[btn] = !btn->isHidden(); btn->setVisible(false); } for (auto btn : getToolBtnByTitle(title)) { - if (toolBtnState.contains(btn)) - btn->setVisible(toolBtnState[btn]); + btn->setVisible(toolBtnState[btn]); } emit workSpaceWidgeSwitched(title); diff --git a/src/plugins/smartut/CMakeLists.txt b/src/plugins/smartut/CMakeLists.txt new file mode 100644 index 000000000..3e02524f3 --- /dev/null +++ b/src/plugins/smartut/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.0.2) + +project(smartut) + +FILE(GLOB_RECURSE PROJECT_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/*.h" + "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/*/*.h" + "${CMAKE_CURRENT_SOURCE_DIR}/*/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/*.json" +) + +add_library(${PROJECT_NAME} + SHARED + ${PROJECT_SOURCES} + smartut.qrc +) + +target_link_libraries(${PROJECT_NAME} + duc-framework + duc-base + duc-services + duc-common + ${QtUseModules} + ${PkgUserModules} + ${DtkWidget_LIBRARIES} + ) + +install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${PLUGIN_INSTALL_PATH}) + diff --git a/src/plugins/smartut/common/itemnode.cpp b/src/plugins/smartut/common/itemnode.cpp new file mode 100644 index 000000000..6dcbb0f8f --- /dev/null +++ b/src/plugins/smartut/common/itemnode.cpp @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "itemnode.h" +#include "utils/utils.h" + +#include "common/util/qtcassert.h" +#include "services/project/projectservice.h" + +#include + +#include + +DWIDGET_USE_NAMESPACE +using namespace dpfservice; + +ProjectNode *Node::parentProjectNode() const +{ + if (!nodeParentFolderNode) + return nullptr; + auto pn = nodeParentFolderNode->asProjectNode(); + if (pn) + return pn; + return nodeParentFolderNode->parentProjectNode(); +} + +FolderNode *Node::parentFolderNode() const +{ + return nodeParentFolderNode; +} + +dpfservice::ProjectInfo Node::projectInfo() const +{ + auto prjSrv = dpfGetService(ProjectService); + Q_ASSERT(prjSrv); + + const auto &allInfos = prjSrv->getAllProjectInfo(); + auto iter = std::find_if(allInfos.cbegin(), allInfos.cend(), + [this](const ProjectInfo &info) { + return nodeFilePath.startsWith(info.workspaceFolder()); + }); + + return iter == allInfos.cend() ? ProjectInfo() : *iter; +} + +QString Node::filePath() const +{ + return nodeFilePath; +} + +QString Node::displayName() const +{ + return QFileInfo(nodeFilePath).fileName(); +} + +QString Node::tooltip() const +{ + return nodeFilePath; +} + +QIcon Node::icon() const +{ + return DFileIconProvider::globalProvider()->icon(QFileInfo(nodeFilePath)); +} + +bool Node::sortByPath(const Node *a, const Node *b) +{ + return a->filePath() < b->filePath(); +} + +void Node::setParentFolderNode(FolderNode *parentFolder) +{ + nodeParentFolderNode = parentFolder; +} + +void Node::setFilePath(const QString &filePath) +{ + nodeFilePath = filePath; +} + +FileNode::FileNode(const QString &filePath) +{ + setFilePath(filePath); +} + +void FileNode::setSourceFiles(const QStringList &files) +{ + sourceList = files; +} + +QStringList FileNode::sourceFiles() const +{ + return sourceList; +} + +FolderNode::FolderNode(const QString &folderPath) +{ + setFilePath(folderPath); + nodeDisplayName = Node::displayName(); +} + +void FolderNode::setDisplayName(const QString &name) +{ + nodeDisplayName = name; +} + +QString FolderNode::displayName() const +{ + return nodeDisplayName; +} + +void FolderNode::addNode(std::unique_ptr &&node) +{ + QTC_ASSERT(node, return ); + QTC_ASSERT(!node->parentFolderNode(), qDebug("Node has already a parent folder")); + node->setParentFolderNode(this); + children.emplace_back(std::move(node)); +} + +const QList FolderNode::nodes() const +{ + QList nodeList; + std::transform(children.begin(), children.end(), std::back_inserter(nodeList), + [](const auto &pointer) { + return pointer.get(); + }); + return nodeList; +} + +FolderNode *FolderNode::folderNode(const QString &directory) const +{ + auto iter = std::find_if(children.cbegin(), children.cend(), + [directory](const std::unique_ptr &n) { + FolderNode *fn = n->asFolderNode(); + return fn && fn->filePath() == directory; + }); + + return iter == children.cend() ? nullptr : static_cast(iter->get()); +} + +FolderNode *FolderNode::findChildFolderNode(const std::function &predicate) const +{ + for (const std::unique_ptr &n : children) { + if (FolderNode *fn = n->asFolderNode()) + if (predicate(fn)) + return fn; + } + return nullptr; +} + +void FolderNode::addNestedNodes(std::vector> &&files, + const QString &workspace, + const FolderNodeFactory &factory) +{ + using DirWithNodes = std::pair>>; + std::vector fileNodesPerDir; + for (auto &f : files) { + if (!f->filePath().startsWith(workspace)) + continue; + + QFileInfo fileInfo(f->filePath()); + const QString parentDir = fileInfo.absolutePath(); + const auto it = std::lower_bound(fileNodesPerDir.begin(), fileNodesPerDir.end(), parentDir, + [](const DirWithNodes &nad, const QString &dir) { return nad.first < dir; }); + if (it != fileNodesPerDir.end() && it->first == parentDir) { + it->second.emplace_back(std::move(f)); + } else { + DirWithNodes dirWithNodes; + dirWithNodes.first = parentDir; + dirWithNodes.second.emplace_back(std::move(f)); + fileNodesPerDir.insert(it, std::move(dirWithNodes)); + } + } + + for (DirWithNodes &dirWithNodes : fileNodesPerDir) { + FolderNode *const folderNode = Utils::recursiveFindOrCreateFolderNode(this, dirWithNodes.first, + workspace, factory); + for (auto &f : dirWithNodes.second) + folderNode->addNode(std::move(f)); + } +} + +QIcon FolderNode::icon() const +{ + if (!QFile::exists(filePath())) + return QIcon::fromTheme("folder"); + + return Node::icon(); +} + +VirtualFolderNode::VirtualFolderNode(const QString &folderPath) + : FolderNode(folderPath) +{ + setFilePath(folderPath); +} + +ProjectNode::ProjectNode(const QString &projectFilePath) + : FolderNode(projectFilePath) +{ + setFilePath(projectFilePath); +} diff --git a/src/plugins/smartut/common/itemnode.h b/src/plugins/smartut/common/itemnode.h new file mode 100644 index 000000000..59f20fb8b --- /dev/null +++ b/src/plugins/smartut/common/itemnode.h @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ITEMNODE_H +#define ITEMNODE_H + +#include "common/project/projectinfo.h" + +#include + +enum ItemState { + None, + Waiting, + Generating, + Completed, + Failed, + Ignored +}; +Q_DECLARE_METATYPE(ItemState) + +class FileNode; +class FolderNode; +class ProjectNode; + +class Node +{ +public: + virtual ~Node() = default; + + virtual bool isFolderNodeType() const { return false; } + virtual bool isFileNodeType() const { return false; } + virtual bool isProjectNodeType() const { return false; } + virtual bool isVirtualFolderType() const { return false; } + + ProjectNode *parentProjectNode() const; + FolderNode *parentFolderNode() const; + + dpfservice::ProjectInfo projectInfo() const; + QString filePath() const; + virtual QString displayName() const; + virtual QString tooltip() const; + virtual QIcon icon() const; + + virtual FileNode *asFileNode() { return nullptr; } + virtual const FileNode *asFileNode() const { return nullptr; } + virtual FolderNode *asFolderNode() { return nullptr; } + virtual const FolderNode *asFolderNode() const { return nullptr; } + virtual ProjectNode *asProjectNode() { return nullptr; } + virtual const ProjectNode *asProjectNode() const { return nullptr; } + + static bool sortByPath(const Node *a, const Node *b); + void setParentFolderNode(FolderNode *parentFolder); + +protected: + Node() = default; + Node(const Node &other) = delete; + bool operator=(const Node &other) = delete; + + void setFilePath(const QString &filePath); + +private: + QString nodeFilePath; + FolderNode *nodeParentFolderNode { nullptr }; +}; + +class FileNode : public Node +{ +public: + explicit FileNode(const QString &filePath); + + void setSourceFiles(const QStringList &files); + QStringList sourceFiles() const; + + bool isFileNodeType() const { return true; } + FileNode *asFileNode() final { return this; } + const FileNode *asFileNode() const final { return this; } + +private: + QStringList sourceList; +}; + +class FolderNode : public Node +{ +public: + explicit FolderNode(const QString &folderPath); + + void setDisplayName(const QString &name); + QString displayName() const override; + void addNode(std::unique_ptr &&node); + const QList nodes() const; + FolderNode *folderNode(const QString &directory) const; + + FolderNode *findChildFolderNode(const std::function &predicate) const; + + using FolderNodeFactory = std::function(const QString &)>; + void addNestedNodes(std::vector> &&files, + const QString &workspace, + const FolderNodeFactory &factory = + [](const QString &fn) { return std::make_unique(fn); }); + + QIcon icon() const override; + bool isFolderNodeType() const override { return true; } + FolderNode *asFolderNode() override { return this; } + const FolderNode *asFolderNode() const override { return this; } + +protected: + std::vector> children; + +private: + QString nodeDisplayName; +}; + +class VirtualFolderNode : public FolderNode +{ +public: + explicit VirtualFolderNode(const QString &folderPath); + + bool isFolderNodeType() const override { return false; } + bool isVirtualFolderType() const override { return true; } +}; + +class ProjectNode : public FolderNode +{ +public: + explicit ProjectNode(const QString &projectFilePath); + + bool isFolderNodeType() const override { return false; } + bool isProjectNodeType() const override { return true; } + ProjectNode *asProjectNode() final { return this; } + const ProjectNode *asProjectNode() const final { return this; } +}; + +class NodeItem : public QStandardItem +{ +public: + explicit NodeItem(Node *node) + : itemNode(node) {} + + Node *itemNode { nullptr }; + ItemState state { None }; + QString userCache; +}; + +#endif // ITEMNODE_H diff --git a/src/plugins/smartut/common/projectitemdelegate.cpp b/src/plugins/smartut/common/projectitemdelegate.cpp new file mode 100644 index 000000000..c0f0347d1 --- /dev/null +++ b/src/plugins/smartut/common/projectitemdelegate.cpp @@ -0,0 +1,432 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "projectitemdelegate.h" +#include "projectitemmodel.h" +#include "utils/utils.h" +#include "common/itemnode.h" + +#include +#include +#ifdef DTKWIDGET_CLASS_DPaletteHelper +# include +#endif + +#include +#include +#include +#include + +inline constexpr int kPadding = { 6 }; +inline constexpr int kItemMargin { 4 }; +inline constexpr int kExpandArrowSize { 20 }; +inline constexpr int kCheckBoxSize { 16 }; +inline constexpr int kItemIndentation { 20 }; +inline constexpr int kItemSpacing { 4 }; + +DWIDGET_USE_NAMESPACE + +ProjectItemDelegate::ProjectItemDelegate(ProjectTreeView *parent) + : DStyledItemDelegate(parent), + view(parent) +{ +} + +ProjectItemDelegate::~ProjectItemDelegate() +{ + qDeleteAll(spinners); + spinners.clear(); +} + +void ProjectItemDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + if (!index.isValid()) + return DStyledItemDelegate::paint(painter, option, index); + + QStyleOptionViewItem opt = option; + DStyledItemDelegate::initStyleOption(&opt, index); + + opt.rect.adjust(10, 0, -10, 0); + painter->setRenderHint(QPainter::Antialiasing); + drawBackground(painter, opt); + int depth = itemDepth(index); + const auto &iconRect = drawFileIcon(depth, painter, opt, index); + drawFileNameItem(painter, opt, index, iconRect); +} + +QSize ProjectItemDelegate::sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + return QSize(view->width(), 24); +} + +bool ProjectItemDelegate::editorEvent(QEvent *event, + QAbstractItemModel *model, + const QStyleOptionViewItem &option, + const QModelIndex &index) +{ + if (!index.isValid() || !model || event->type() != QEvent::MouseButtonRelease) + return false; + + QStyleOptionViewItem opt = option; + opt.rect.adjust(10, 0, -10, 0); + QMouseEvent *mouseEvent = static_cast(event); + + int depth = itemDepth(index); + if (index.model()->flags(index).testFlag(Qt::ItemIsUserCheckable)) { + const QRect checkRect = checkBoxRect(depth, opt.rect); + if (checkRect.contains(mouseEvent->pos())) { + Qt::CheckState state = static_cast(index.data(Qt::CheckStateRole).toInt()); + Qt::CheckState newState = state == Qt::Checked ? Qt::Unchecked : Qt::Checked; + + updateChildrenCheckState(model, index, newState); + updateParentCheckState(model, index); + return true; + } + } + + const QRect arRect = arrowRect(depth, opt.rect); + if (arRect.contains(mouseEvent->pos())) { + if (view->isExpanded(index)) { + view->collapse(index); + } else { + view->expand(index); + } + return true; + } + + return false; +} + +bool ProjectItemDelegate::helpEvent(QHelpEvent *event, + QAbstractItemView *view, + const QStyleOptionViewItem &option, + const QModelIndex &index) +{ + if (event->type() == QEvent::ToolTip) { + auto opt = option; + opt.rect.adjust(10, 0, -10, 0); + QRect stateRect = itemStateRect(opt.rect); + if (index.data(ItemStateRole) == Failed && stateRect.contains(event->pos())) { + auto model = qobject_cast(view->model()); + auto item = model->itemForIndex(index); + QToolTip::showText(event->globalPos(), item->userCache, view); + return true; + } + } + + return DStyledItemDelegate::helpEvent(event, view, option, index); +} + +void ProjectItemDelegate::drawBackground(QPainter *painter, const QStyleOptionViewItem &option) const +{ + painter->save(); + if (option.state.testFlag(QStyle::State_Selected)) { + QColor bgColor = option.palette.color(QPalette::Normal, QPalette::Highlight); + painter->setBrush(bgColor); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(option.rect, 8, 8); + } else if (option.state.testFlag(QStyle::State_MouseOver)) { +#ifdef DTKWIDGET_CLASS_DPaletteHelper + DPalette palette = DPaletteHelper::instance()->palette(option.widget); +#else + DPalette palette = DGuiApplicationHelper::instance()->applicationPalette(); +#endif + painter->setBrush(palette.brush(DPalette::ItemBackground)); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(option.rect, 8, 8); + } + painter->restore(); +} + +QRect ProjectItemDelegate::drawFileIcon(int depth, + QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + auto iconRect = fileIconRect(depth, option.rect, index); + drawIcon(painter, option, option.icon, iconRect); + if (index.model()->flags(index).testFlag(Qt::ItemIsUserCheckable)) + drawCheckBox(depth, painter, option, index); + if (index.model()->hasChildren(index)) + drawExpandArrow(depth, painter, option, index); + return iconRect; +} + +QRect ProjectItemDelegate::drawExpandArrow(int depth, QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QStyleOptionViewItem opt = option; + opt.rect = arrowRect(depth, opt.rect).marginsRemoved(QMargins(5, 5, 5, 5)); + + painter->save(); + bool isSelected = (option.state & QStyle::State_Selected) && option.showDecorationSelected; + if (isSelected) { + painter->setPen(option.palette.color(QPalette::Active, QPalette::HighlightedText)); + } else { + painter->setPen(option.palette.color(QPalette::Active, QPalette::Text)); + } + + auto style = option.widget->style(); + if (view->isExpanded(index)) { + style->drawPrimitive(QStyle::PE_IndicatorArrowDown, &opt, painter, nullptr); + } else { + style->drawPrimitive(QStyle::PE_IndicatorArrowRight, &opt, painter, nullptr); + } + + painter->restore(); + return opt.rect; +} + +void ProjectItemDelegate::drawCheckBox(int depth, + QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionButton opt; + opt.rect = checkBoxRect(depth, option.rect); + opt.state = option.state; + + bool isSelected = (option.state & QStyle::State_Selected) && option.showDecorationSelected; + if (isSelected) { + opt.palette.setColor(QPalette::Foreground, option.palette.color(QPalette::HighlightedText)); + opt.palette.setColor(QPalette::Highlight, option.palette.color(QPalette::HighlightedText)); + } + + Qt::CheckState checkState = static_cast(index.data(Qt::CheckStateRole).toInt()); + if (checkState == Qt::Checked) + opt.state |= QStyle::State_On; + else if (checkState == Qt::PartiallyChecked) + opt.state |= QStyle::State_NoChange; + else + opt.state |= QStyle::State_Off; + + // Draw the checkbox using the widget's style + option.widget->style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &opt, painter, option.widget); +} + +QRect ProjectItemDelegate::drawItemState(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + if (index.model()->hasChildren(index)) + return {}; + + auto stopSpinner = [&] { + if (auto spinner = spinners.value(index)) { + delete spinner; + spinners.remove(index); + } + }; + + QRect stateRect = itemStateRect(option.rect); + auto state = static_cast(index.data(ItemStateRole).toInt()); + switch (state) { + case Generating: { + auto *spinner = findOrCreateSpinnerPainter(index); + QColor color = option.state & QStyle::State_Selected ? option.palette.color(QPalette::HighlightedText) + : option.palette.color(QPalette::Highlight); + + spinner->paint(*painter, color, stateRect); + return stateRect; + } + case Waiting: + stopSpinner(); + drawIcon(painter, option, QIcon::fromTheme("uc_wait"), stateRect); + return stateRect; + case Completed: + stopSpinner(); + drawIcon(painter, option, QIcon::fromTheme("uc_success"), stateRect); + return stateRect; + case Failed: + stopSpinner(); + drawIcon(painter, option, QIcon::fromTheme("uc_failure"), stateRect); + return stateRect; + case Ignored: + stopSpinner(); + drawIcon(painter, option, QIcon::fromTheme("uc_ignore"), stateRect); + return stateRect; + case None: + default: + break; + } + + stopSpinner(); + return {}; +} + +void ProjectItemDelegate::drawFileNameItem(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index, + const QRect &iconRect) const +{ + QStyleOptionViewItem opt = option; + painter->setFont(opt.font); + + QRect stateRect; + if (view->viewType() == ProjectTreeView::UnitTest) + stateRect = drawItemState(painter, opt, index); + +#ifdef DTKWIDGET_CLASS_DPaletteHelper + DPalette palette = DPaletteHelper::instance()->palette(option.widget); +#else + DPalette palette = DGuiApplicationHelper::instance()->applicationPalette(); +#endif + QRect nameRect = opt.rect; + nameRect.setLeft(iconRect.right() + kPadding); + if (stateRect.isValid()) + nameRect.setRight(stateRect.left() - kPadding); + + QString fileName = index.data(Qt::DisplayRole).toString(); + if (opt.state & QStyle::State_Selected) { + painter->setPen(opt.palette.color(QPalette::Normal, QPalette::HighlightedText)); + } else { + painter->setPen(opt.palette.color(QPalette::Normal, QPalette::Text)); + } + + QString displayText = opt.fontMetrics.elidedText(fileName, Qt::ElideRight, nameRect.width()); + painter->drawText(nameRect, displayText); +} + +void ProjectItemDelegate::drawIcon(QPainter *painter, const QStyleOptionViewItem &option, const QIcon &icon, const QRect &rect) const +{ + QIcon::Mode iconMode = QIcon::Normal; + if (!(option.state.testFlag(QStyle::State_Enabled))) + iconMode = QIcon::Disabled; + if (option.state.testFlag(QStyle::State_Selected)) + iconMode = QIcon::Selected; + + auto px = icon.pixmap(view->iconSize(), iconMode); + px.setDevicePixelRatio(qApp->devicePixelRatio()); + + qreal x = rect.x(); + qreal y = rect.y(); + qreal w = px.width() / px.devicePixelRatio(); + qreal h = px.height() / px.devicePixelRatio(); + y += (rect.size().height() - h) / 2.0; + x += (rect.size().width() - w) / 2.0; + + painter->drawPixmap(qRound(x), qRound(y), px); +} + +QRect ProjectItemDelegate::checkBoxRect(int depth, const QRect &itemRect) const +{ + QRect checkRect = itemRect; + checkRect.adjust(depth * kItemIndentation, 0, 0, 0); + checkRect.setSize({ kCheckBoxSize, kCheckBoxSize }); + + const auto &ar = arrowRect(depth, itemRect); + checkRect.moveLeft(ar.right()); + checkRect.moveTop(checkRect.top() + ((itemRect.bottom() - checkRect.bottom()) / 2)); + + return checkRect; +} + +QRect ProjectItemDelegate::fileIconRect(int depth, const QRect &itemRect, const QModelIndex &index) const +{ + QRect iconRect = itemRect; + iconRect.adjust(depth * kItemIndentation, 0, 0, 0); + QSize iconSize = view->iconSize(); + iconRect.setSize(iconSize); + + if (index.flags().testFlag(Qt::ItemIsUserCheckable)) { + const auto &checkRect = checkBoxRect(depth, itemRect); + iconRect.moveLeft(checkRect.right() + kItemSpacing); + } else { + const auto &ar = arrowRect(depth, itemRect); + iconRect.moveLeft(ar.right() + kItemSpacing); + } + iconRect.moveTop(iconRect.top() + ((itemRect.bottom() - iconRect.bottom()) / 2)); + + return iconRect; +} + +QRect ProjectItemDelegate::arrowRect(int depth, const QRect &itemRect) const +{ + QRect arrowRect = itemRect; + arrowRect.adjust(depth * kItemIndentation, 0, 0, 0); + + arrowRect.setSize(QSize(kExpandArrowSize, kExpandArrowSize)); + arrowRect.moveLeft(arrowRect.left() + kItemMargin); + arrowRect.moveTop(itemRect.top() + (itemRect.bottom() - arrowRect.bottom()) / 2); + + return arrowRect; +} + +QRect ProjectItemDelegate::itemStateRect(const QRect &itemRect) const +{ + QRect stateRect = itemRect; + stateRect.setSize(view->iconSize()); + + stateRect.moveLeft(itemRect.right() - stateRect.width() - 10); + stateRect.moveTop(itemRect.top() + (itemRect.bottom() - stateRect.bottom()) / 2); + + return stateRect; +} + +int ProjectItemDelegate::itemDepth(const QModelIndex &index) const +{ + int depth = 0; + QModelIndex parent = index.parent(); + while (parent.isValid()) { + depth++; + parent = parent.parent(); + } + return depth; +} + +SpinnerPainter *ProjectItemDelegate::findOrCreateSpinnerPainter(const QModelIndex &index) const +{ + SpinnerPainter *sp = spinners.value(index); + if (!sp) { + sp = new SpinnerPainter(); + sp->setUpdateCallback([index, this] { view->update(index); }); + sp->startAnimation(); + spinners.insert(index, sp); + } + return sp; +} + +void ProjectItemDelegate::updateChildrenCheckState(QAbstractItemModel *model, + const QModelIndex &index, + Qt::CheckState state) +{ + if (!index.isValid()) + return; + + // Update current node + model->setData(index, state, Qt::CheckStateRole); + + // Recursively update all children + if (model->hasChildren(index)) { + for (int i = 0; i < model->rowCount(index); ++i) { + QModelIndex child = model->index(i, 0, index); + updateChildrenCheckState(model, child, state); + } + } +} + +void ProjectItemDelegate::updateParentCheckState(QAbstractItemModel *model, const QModelIndex &index) +{ + QModelIndex parent = index.parent(); + while (parent.isValid()) { + bool allChecked = true; + bool anyChecked = false; + + for (int i = 0; i < model->rowCount(parent); ++i) { + QModelIndex child = model->index(i, 0, parent); + Qt::CheckState childState = static_cast(child.data(Qt::CheckStateRole).toInt()); + + if (childState != Qt::Checked) + allChecked = false; + if (childState == Qt::Checked || childState == Qt::PartiallyChecked) + anyChecked = true; + } + + Qt::CheckState parentState = allChecked ? Qt::Checked : anyChecked ? Qt::PartiallyChecked : Qt::Unchecked; + model->setData(parent, parentState, Qt::CheckStateRole); + parent = parent.parent(); + } +} diff --git a/src/plugins/smartut/common/projectitemdelegate.h b/src/plugins/smartut/common/projectitemdelegate.h new file mode 100644 index 000000000..2e340a473 --- /dev/null +++ b/src/plugins/smartut/common/projectitemdelegate.h @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PROJECTITEMDELEGATE_H +#define PROJECTITEMDELEGATE_H + +#include "gui/projecttreeview.h" + +#include "common/util/spinnerpainter.h" + +#include + +class ProjectItemDelegate : public DTK_WIDGET_NAMESPACE::DStyledItemDelegate +{ + Q_OBJECT +public: + explicit ProjectItemDelegate(ProjectTreeView *parent); + ~ProjectItemDelegate(); + + void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + bool editorEvent(QEvent *event, + QAbstractItemModel *model, + const QStyleOptionViewItem &option, + const QModelIndex &index) override; + bool helpEvent(QHelpEvent *event, + QAbstractItemView *view, + const QStyleOptionViewItem &option, + const QModelIndex &index) override; + +private: + void drawBackground(QPainter *painter, const QStyleOptionViewItem &option) const; + QRect drawFileIcon(int depth, + QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const; + QRect drawExpandArrow(int depth, + QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const; + void drawCheckBox(int depth, + QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const; + QRect drawItemState(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const; + void drawFileNameItem(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index, + const QRect &iconRect) const; + void drawIcon(QPainter *painter, const QStyleOptionViewItem &option, + const QIcon &icon, const QRect &rect) const; + + QRect checkBoxRect(int depth, const QRect &itemRect) const; + QRect fileIconRect(int depth, const QRect &itemRect, const QModelIndex &index) const; + QRect arrowRect(int depth, const QRect &itemRect) const; + QRect itemStateRect(const QRect &itemRect) const; + + int itemDepth(const QModelIndex &index) const; + SpinnerPainter *findOrCreateSpinnerPainter(const QModelIndex &index) const; + void updateChildrenCheckState(QAbstractItemModel *model, + const QModelIndex &index, + Qt::CheckState state); + void updateParentCheckState(QAbstractItemModel *model, const QModelIndex &index); + +private: + ProjectTreeView *view { nullptr }; + mutable QHash spinners; +}; + +#endif // PROJECTITEMDELEGATE_H diff --git a/src/plugins/smartut/common/projectitemmodel.cpp b/src/plugins/smartut/common/projectitemmodel.cpp new file mode 100644 index 000000000..68430dad6 --- /dev/null +++ b/src/plugins/smartut/common/projectitemmodel.cpp @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "projectitemmodel.h" +#include "utils/utils.h" + +#include + +class ProjectItemModelPrivate +{ +public: + QStandardItem *findChildItem(QStandardItem *item, const Node *node); + void addFolderNode(NodeItem *parent, FolderNode *folderNode, QSet *seen); + +public: + NodeItem *rootItem { nullptr }; + ProjectTreeView *view { nullptr }; +}; + +QStandardItem *ProjectItemModelPrivate::findChildItem(QStandardItem *item, const Node *node) +{ + auto find = [node](QStandardItem *item) { + NodeItem *witem = static_cast(item); + return witem ? witem->itemNode == node : false; + }; + + if (item) { + if (find(item)) + return item; + + for (int i = 0; i < item->rowCount(); ++i) { + if (auto found = findChildItem(item->child(i), node)) + return found; + } + } + + return nullptr; +} + +void ProjectItemModelPrivate::addFolderNode(NodeItem *parent, FolderNode *folderNode, QSet *seen) +{ + for (Node *node : folderNode->nodes()) { + if (FolderNode *subFolderNode = node->asFolderNode()) { + int oldSize = seen->size(); + seen->insert(subFolderNode); + if (seen->size() > oldSize) { + auto node = new NodeItem(subFolderNode); + parent->appendRow(node); + addFolderNode(node, subFolderNode, seen); + // TODO: sort + } else { + addFolderNode(parent, subFolderNode, seen); + } + } else if (FileNode *fileNode = node->asFileNode()) { + int oldSize = seen->size(); + seen->insert(fileNode); + if (seen->size() > oldSize) { + auto node = new NodeItem(fileNode); + parent->appendRow(node); + } + } + } +} + +ProjectItemModel::ProjectItemModel(ProjectTreeView *parent) + : QStandardItemModel(parent), + d(new ProjectItemModelPrivate()) +{ + d->view = parent; +} + +ProjectItemModel::~ProjectItemModel() +{ + clear(); + delete d; +} + +void ProjectItemModel::setRootProjectNode(ProjectNode *rootNode) +{ + setRootItem(new NodeItem(rootNode)); + + QSet seen; + d->addFolderNode(d->rootItem, rootNode, &seen); +} + +void ProjectItemModel::setRootItem(NodeItem *root) +{ + d->rootItem = root; + appendRow(d->rootItem); +} + +NodeItem *ProjectItemModel::rootItem() const +{ + return d->rootItem; +} + +void ProjectItemModel::clear() +{ + while (hasChildren()) { + removeRow(0); + } + d->rootItem = nullptr; +} + +QVariant ProjectItemModel::data(const QModelIndex &index, int role) const +{ + const Node *const node = nodeForIndex(index); + if (!node) + return {}; + + switch (role) { + case Qt::DisplayRole: + return node->displayName(); + case Qt::ToolTipRole: { + QString tips = node->tooltip(); + if (node->isFileNodeType() && !QFile::exists(node->filePath())) + tips += tr(" [Ungenerated]"); + return tips; + } + case Qt::DecorationRole: + return node->icon(); + case Qt::FontRole: { + QFont font; + if (node->isProjectNodeType()) + font.setBold(true); + return font; + } + case Qt::ForegroundRole: + if (!QFile::exists(node->filePath())) { + auto fore = d->view->palette().color(QPalette::Text); + fore.setAlpha(qRound(255 * 0.4)); + return fore; + } + break; + case ItemStateRole: { + auto item = itemForIndex(index); + if (item) + return item->state; + } break; + default: + break; + } + + return QStandardItemModel::data(index, role); +} + +int ProjectItemModel::rowCount(const QModelIndex &index) const +{ + if (!index.isValid()) + return d->rootItem->rowCount(); + const auto *item = itemFromIndex(index); + return item ? item->rowCount() : 0; +} + +int ProjectItemModel::columnCount(const QModelIndex &index) const +{ + Q_UNUSED(index) + return 1; +} + +Qt::ItemFlags ProjectItemModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return {}; + + Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + if (Node *node = nodeForIndex(index)) { + if (node->isProjectNodeType()) + return flags; + if (d->view->viewType() == ProjectTreeView::Project) { + flags |= Qt::ItemIsUserCheckable; + if (!node->isFileNodeType()) + flags |= Qt::ItemIsUserTristate; + } + } + + return flags; +} + +Node *ProjectItemModel::nodeForIndex(const QModelIndex &index) const +{ + NodeItem *item = itemForIndex(index); + return item ? item->itemNode : nullptr; +} + +NodeItem *ProjectItemModel::itemForNode(const Node *node) const +{ + auto item = d->findChildItem(d->rootItem, node); + return static_cast(item); +} + +NodeItem *ProjectItemModel::itemForIndex(const QModelIndex &index) const +{ + return static_cast(itemFromIndex(index)); +} + +QModelIndex ProjectItemModel::indexForNode(const Node *node) const +{ + NodeItem *item = itemForNode(node); + return item ? indexFromItem(item) : QModelIndex(); +} diff --git a/src/plugins/smartut/common/projectitemmodel.h b/src/plugins/smartut/common/projectitemmodel.h new file mode 100644 index 000000000..90661a092 --- /dev/null +++ b/src/plugins/smartut/common/projectitemmodel.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PROJECTITEMMODEL_H +#define PROJECTITEMMODEL_H + +#include "itemnode.h" +#include "gui/projecttreeview.h" + +#include + +class ProjectItemModelPrivate; +class ProjectItemModel : public QStandardItemModel +{ + Q_OBJECT +public: + explicit ProjectItemModel(ProjectTreeView *parent = nullptr); + ~ProjectItemModel(); + + void setRootProjectNode(ProjectNode *rootNode); + void setRootItem(NodeItem *root); + NodeItem *rootItem() const; + void clear(); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &index = QModelIndex()) const override; + int columnCount(const QModelIndex &index) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + Node *nodeForIndex(const QModelIndex &index) const; + NodeItem *itemForNode(const Node *node) const; + NodeItem *itemForIndex(const QModelIndex &index) const; + QModelIndex indexForNode(const Node *node) const; + +private: + ProjectItemModelPrivate *const d; +}; + +#endif // PROJECTITEMMODEL_H diff --git a/src/plugins/smartut/configure/smartut.json b/src/plugins/smartut/configure/smartut.json new file mode 100644 index 000000000..b0eb6631a --- /dev/null +++ b/src/plugins/smartut/configure/smartut.json @@ -0,0 +1,19 @@ +{ + "General": { + "Prompts": { + "Qt/C++": "根据函数签名和文档字符串为函数编写独特、多样化和直观的单元测试。\n\n关键原则:\n- 单元测试应该专注于测试一个特定的组件或功能模块\n- 单元测试代码生成到一个文件当中\n- 只需要生成相关的单元测试代码\n\n测试用例要求:\n- 包含基本功能测试\n- 包含边界条件测试\n- 包含错误处理测试\n- 包含内存管理测试\n\n额外条件:\n- 对于Qt特有的功能(如信号和槽),使用 QSignalSpy 进行测试\n- 使用 mock 对象来模拟用户交互和本地环境,比如模拟文件操作,避免依赖实际的文件系统。", + "test" : "test" + }, + "NameFormat": "ut_${filename}.cpp", + "TestFrameworks": [ + "Qt Test", + "Google Test", + "Boost Test" + ] + }, + "ActiveSettings": { + "ActivePrompt": "Qt/C++", + "ActiveTemplate": "", + "ActiveTestFramework": "Google Test" + } +} diff --git a/src/plugins/smartut/event/eventreceiver.cpp b/src/plugins/smartut/event/eventreceiver.cpp new file mode 100644 index 000000000..8b558ca01 --- /dev/null +++ b/src/plugins/smartut/event/eventreceiver.cpp @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "eventreceiver.h" + +#include "common/util/eventdefinitions.h" + +EventReceiver::EventReceiver(QObject *parent) + : dpf::EventHandler(parent), + dpf::AutoEventHandlerRegister() +{ + using namespace std::placeholders; + eventHandleMap.insert(ai.LLMChanged.name, std::bind(&EventReceiver::processLLMCountChanged, this, _1)); +} + +dpf::EventHandler::Type EventReceiver::type() +{ + return dpf::EventHandler::Type::Sync; +} + +QStringList EventReceiver::topics() +{ + return { ai.topic }; +} + +void EventReceiver::eventProcess(const dpf::Event &event) +{ + const auto &eventName = event.data().toString(); + if (!eventHandleMap.contains(eventName)) + return; + + eventHandleMap[eventName](event); +} + +void EventReceiver::processLLMCountChanged(const dpf::Event &event) +{ + Q_EMIT EventDistributeProxy::instance()->sigLLMCountChanged(); +} + +EventDistributeProxy *EventDistributeProxy::instance() +{ + static EventDistributeProxy ins; + return &ins; +} diff --git a/src/plugins/smartut/event/eventreceiver.h b/src/plugins/smartut/event/eventreceiver.h new file mode 100644 index 000000000..40406aefb --- /dev/null +++ b/src/plugins/smartut/event/eventreceiver.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef EVENTRECEIVER_H +#define EVENTRECEIVER_H + +#include + +class EventReceiver : public dpf::EventHandler, dpf::AutoEventHandlerRegister +{ + friend class dpf::AutoEventHandlerRegister; + +public: + explicit EventReceiver(QObject *parent = nullptr); + static Type type(); + static QStringList topics(); + void eventProcess(const dpf::Event &event) override; + +private: + void processLLMCountChanged(const dpf::Event &event); + +private: + QHash> eventHandleMap; +}; + +class EventDistributeProxy : public QObject +{ + Q_OBJECT +public: + static EventDistributeProxy *instance(); + +Q_SIGNALS: + void sigLLMCountChanged(); +}; + +#endif // EVENTRECEIVER_H diff --git a/src/plugins/smartut/gui/projecttreeview.cpp b/src/plugins/smartut/gui/projecttreeview.cpp new file mode 100644 index 000000000..0cd702ee1 --- /dev/null +++ b/src/plugins/smartut/gui/projecttreeview.cpp @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "projecttreeview.h" +#include "common/projectitemmodel.h" +#include "common/projectitemdelegate.h" +#include "utils/utils.h" + +#include "common/util/eventdefinitions.h" + +#include + +#include +#include +#include + +DWIDGET_USE_NAMESPACE + +class ProjectTreeViewPrivate : public QObject +{ +public: + explicit ProjectTreeViewPrivate(ProjectTreeView *qq); + + void initUI(); + void initConnecttion(); + + void setItemIgnoreState(NodeItem *item, bool ignore); + +public: + ProjectTreeView *q; + + ProjectItemModel *model { nullptr }; + ProjectTreeView::ViewType viewType; +}; + +ProjectTreeViewPrivate::ProjectTreeViewPrivate(ProjectTreeView *qq) + : q(qq) +{ +} + +void ProjectTreeViewPrivate::initUI() +{ + model = new ProjectItemModel(q); + q->setModel(model); + q->setItemDelegate(new ProjectItemDelegate(q)); + + q->setLineWidth(0); + q->setContentsMargins(0, 0, 0, 0); + q->setFrameShape(QFrame::NoFrame); + q->setIconSize(QSize(16, 16)); + + q->setRootIsDecorated(false); + q->setIndentation(0); + q->setMouseTracking(true); + q->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + q->setTextElideMode(Qt::ElideNone); + q->setHeaderHidden(true); + q->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + q->header()->setStretchLastSection(true); +} + +void ProjectTreeViewPrivate::initConnecttion() +{ + if (viewType == ProjectTreeView::UnitTest) { + connect(q, &ProjectTreeView::doubleClicked, this, + [this](const QModelIndex &index) { + auto node = model->nodeForIndex(index); + if (node && node->isFileNodeType() && QFile::exists(node->filePath())) + editor.openFile(QString(), node->filePath()); + }); + } +} + +void ProjectTreeViewPrivate::setItemIgnoreState(NodeItem *item, bool ignore) +{ + if (item->itemNode->isFileNodeType()) { + item->state = ignore ? Ignored : None; + q->updateItem(item); + } else if (item->hasChildren()) { + for (int i = 0; i < item->rowCount(); ++i) { + setItemIgnoreState(dynamic_cast(item->child(i)), ignore); + } + } +} + +ProjectTreeView::ProjectTreeView(ViewType type, QWidget *parent) + : DTreeView(parent), + d(new ProjectTreeViewPrivate(this)) +{ + d->viewType = type; + d->initUI(); + d->initConnecttion(); +} + +ProjectTreeView::ViewType ProjectTreeView::viewType() const +{ + return d->viewType; +} + +void ProjectTreeView::setRootProjectNode(ProjectNode *rootNode) +{ + d->model->setRootProjectNode(rootNode); +} + +NodeItem *ProjectTreeView::rootItem() const +{ + return d->model->rootItem(); +} + +void ProjectTreeView::clear() +{ + d->model->clear(); +} + +void ProjectTreeView::updateItem(NodeItem *item) +{ + auto index = d->model->indexFromItem(item); + if (index.isValid()) + update(index); +} + +void ProjectTreeView::contextMenuEvent(QContextMenuEvent *event) +{ + QModelIndex index = indexAt(event->pos()); + auto item = d->model->itemForIndex(index); + if (!item) + return; + + QMenu menu; + if (d->viewType == UnitTest) { + if (item->itemNode->isFileNodeType()) { + auto act = menu.addAction(tr("Open File"), this, [item] { + editor.openFile(QString(), item->itemNode->filePath()); + }); + act->setEnabled(QFile::exists(item->itemNode->filePath())); + } + + bool allIgnored = Utils::checkAllState(item, Ignored); + auto act = menu.addAction(tr("Generate UT"), this, std::bind(&ProjectTreeView::reqGenerateUTFile, this, item)); + act->setEnabled(!allIgnored); + + if (allIgnored) + menu.addAction(tr("Unignore"), this, std::bind(&ProjectTreeViewPrivate::setItemIgnoreState, d, item, false)); + else + menu.addAction(tr("Ignore"), this, std::bind(&ProjectTreeViewPrivate::setItemIgnoreState, d, item, true)); + + act = menu.addAction(tr("Show Containing Folder"), this, [item] { + DDesktopServices::showFileItem(item->itemNode->filePath()); + }); + act->setEnabled(QFile::exists(item->itemNode->filePath())); + } + + if (!item->itemNode->isFileNodeType()) { + menu.addSeparator(); + if (isExpanded(index)) + menu.addAction(tr("Collapse"), this, std::bind(&ProjectTreeView::collapse, this, index)); + else + menu.addAction(tr("Expand"), this, std::bind(&ProjectTreeView::expand, this, index)); + menu.addAction(tr("Collapse All"), this, &ProjectTreeView::collapseAll); + menu.addAction(tr("Expand All"), this, &ProjectTreeView::expandAll); + } + + menu.exec(QCursor::pos()); +} diff --git a/src/plugins/smartut/gui/projecttreeview.h b/src/plugins/smartut/gui/projecttreeview.h new file mode 100644 index 000000000..841a681ee --- /dev/null +++ b/src/plugins/smartut/gui/projecttreeview.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PROJECTTREEVIEW_H +#define PROJECTTREEVIEW_H + +#include "common/itemnode.h" + +#include + +class ProjectTreeViewPrivate; +class ProjectTreeView : public DTK_WIDGET_NAMESPACE::DTreeView +{ + Q_OBJECT +public: + enum ViewType { + Project, + UnitTest + }; + + explicit ProjectTreeView(ViewType type, QWidget *parent = nullptr); + + ViewType viewType() const; + void setRootProjectNode(ProjectNode *rootNode); + NodeItem *rootItem() const; + + void clear(); + +public Q_SLOTS: + void updateItem(NodeItem *item); + +Q_SIGNALS: + void reqGenerateUTFile(NodeItem *item); + +protected: + void contextMenuEvent(QContextMenuEvent *event) override; + +private: + ProjectTreeViewPrivate *const d; +}; + +#endif // PROJECTTREEVIEW_H diff --git a/src/plugins/smartut/gui/settingdialog.cpp b/src/plugins/smartut/gui/settingdialog.cpp new file mode 100644 index 000000000..1af13b241 --- /dev/null +++ b/src/plugins/smartut/gui/settingdialog.cpp @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "settingdialog.h" +#include "manager/smartutmanager.h" +#include "utils/utils.h" + +#include +#include + +#include +#include +#include +#include +#include + +DWIDGET_USE_NAMESPACE + +SettingDialog::SettingDialog(QWidget *parent) + : DDialog(parent) +{ + initUI(); + initConnection(); +} + +QStringList SettingDialog::selectedFileList() +{ + return resourceWidget->selectedFileList(); +} + +QString SettingDialog::selectedProject() +{ + return resourceWidget->selectedProject(); +} + +void SettingDialog::showEvent(QShowEvent *e) +{ + generalWidget->updateSettings(); + promptWidget->updateSettings(); + resourceWidget->updateSettings(); + DDialog::showEvent(e); +} + +void SettingDialog::initUI() +{ + setFixedSize(550, 618); + setIcon(QIcon::fromTheme("ide")); + setOnButtonClickedClose(false); + + QWidget *contentWidget = new QWidget(this); + QVBoxLayout *layout = new QVBoxLayout(contentWidget); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(8); + + btnBox = new DButtonBox(this); + DButtonBoxButton *generalBtn = new DButtonBoxButton(tr("General"), this); + DButtonBoxButton *promptBtn = new DButtonBoxButton(tr("Prompt"), this); + DButtonBoxButton *srcBtn = new DButtonBoxButton(tr("Resource"), this); + btnBox->setButtonList({ generalBtn, promptBtn, srcBtn }, true); + btnBox->setId(generalBtn, 0); + btnBox->setId(promptBtn, 1); + btnBox->setId(srcBtn, 2); + generalBtn->setChecked(true); + + mainWidget = new QStackedWidget(this); + generalWidget = new GeneralSettingWidget(this); + promptWidget = new PromptSettingWidget(this); + resourceWidget = new ResourceSettingWidget(this); + mainWidget->addWidget(generalWidget); + mainWidget->addWidget(promptWidget); + mainWidget->addWidget(resourceWidget); + + layout->addWidget(btnBox, 0, Qt::AlignTop | Qt::AlignHCenter); + layout->addWidget(mainWidget, 1); + addContent(contentWidget); + + addButton(tr("Cancel", "button")); + addButton(tr("OK", "button"), true, DDialog::ButtonRecommend); +} + +void SettingDialog::initConnection() +{ + connect(btnBox, &DButtonBox::buttonClicked, this, &SettingDialog::handleSwitchWidget); + connect(this, &SettingDialog::buttonClicked, this, &SettingDialog::handleButtonClicked); +} + +void SettingDialog::handleSwitchWidget(QAbstractButton *btn) +{ + auto index = btnBox->id(btn); + mainWidget->setCurrentIndex(index); +} + +void SettingDialog::handleButtonClicked(int index) +{ + if (index != 1) + return reject(); + + if (!generalWidget->apply()) { + mainWidget->setCurrentWidget(generalWidget); + return; + } + + if (!resourceWidget->apply()) { + mainWidget->setCurrentWidget(resourceWidget); + return; + } + + promptWidget->apply(); + accept(); +} diff --git a/src/plugins/smartut/gui/settingdialog.h b/src/plugins/smartut/gui/settingdialog.h new file mode 100644 index 000000000..959dc3993 --- /dev/null +++ b/src/plugins/smartut/gui/settingdialog.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef SETTINGDIALOG_H +#define SETTINGDIALOG_H + +#include "widget/generalsettingwidget.h" +#include "widget/promptsettingwidget.h" +#include "widget/resourcesettingwidget.h" + +#include +#include + +#include + +class SettingDialog : public DTK_WIDGET_NAMESPACE::DDialog +{ + Q_OBJECT +public: + explicit SettingDialog(QWidget *parent = nullptr); + + QStringList selectedFileList(); + QString selectedProject(); + +protected: + void showEvent(QShowEvent *e) override; + +private: + void initUI(); + void initConnection(); + + void handleSwitchWidget(QAbstractButton *btn); + void handleButtonClicked(int index); + + GeneralSettingWidget *generalWidget { nullptr }; + PromptSettingWidget *promptWidget { nullptr }; + ResourceSettingWidget *resourceWidget { nullptr }; + DTK_WIDGET_NAMESPACE::DButtonBox *btnBox { nullptr }; + QStackedWidget *mainWidget { nullptr }; +}; + +#endif // SETTINGDIALOG_H diff --git a/src/plugins/smartut/gui/smartutwidget.cpp b/src/plugins/smartut/gui/smartutwidget.cpp new file mode 100644 index 000000000..68f033b77 --- /dev/null +++ b/src/plugins/smartut/gui/smartutwidget.cpp @@ -0,0 +1,196 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "smartutwidget.h" +#include "settingdialog.h" +#include "projecttreeview.h" +#include "manager/smartutmanager.h" +#include "utils/utils.h" +#include "event/eventreceiver.h" + +#include +#include + +#include +#include +#include + +DWIDGET_USE_NAMESPACE +DGUI_USE_NAMESPACE + +SmartUTWidget::SmartUTWidget(QWidget *parent) + : QWidget(parent) +{ + initUI(); + initConnection(); +} + +void SmartUTWidget::showSettingDialog() +{ + if (!settingDlg) + settingDlg = new SettingDialog(this); + + if (settingDlg->exec() == QDialog::Accepted) { + const auto &fileList = settingDlg->selectedFileList(); + fillProjectView(settingDlg->selectedProject(), fileList); + } +} + +void SmartUTWidget::initUI() +{ + setAutoFillBackground(true); + setBackgroundRole(QPalette::Base); + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + mainWidget = new QStackedWidget(this); + mainWidget->addWidget(createBlankPage()); + mainWidget->addWidget(createMainWidget()); + + layout->addWidget(mainWidget); +} + +void SmartUTWidget::initConnection() +{ + connect(generateBtn, &DToolButton::clicked, this, qOverload<>(&SmartUTWidget::createUTFiles)); + connect(prjView, &ProjectTreeView::reqGenerateUTFile, this, qOverload(&SmartUTWidget::createUTFiles)); + connect(SmartUTManager::instance(), &SmartUTManager::itemStateChanged, prjView, &ProjectTreeView::updateItem); + connect(EventDistributeProxy::instance(), &EventDistributeProxy::sigLLMCountChanged, this, &SmartUTWidget::updateModelList); +} + +QWidget *SmartUTWidget::createBlankPage() +{ + QWidget *widget = new QWidget(this); + QVBoxLayout *layout = new QVBoxLayout(widget); + + DLabel *configureLabel = new DLabel(this); + configureLabel->setAlignment(Qt::AlignCenter); + + DLabel *titleLabel = new DLabel(tr("The current resource is not configured"), this); + titleLabel->setAlignment(Qt::AlignCenter); + titleLabel->setWordWrap(true); + + DLabel *msgLabel = new DLabel(this); + msgLabel->setAlignment(Qt::AlignCenter); + msgLabel->setWordWrap(true); + + auto updateIcon = [configureLabel, msgLabel]() { + configureLabel->setPixmap(QIcon::fromTheme("uc_configure").pixmap({ 234, 144 })); + QString msgFormat = tr("

Please click the Setting button \"%1\"" + " in the upper right corner to configure

"); + QString icon = DGuiApplicationHelper::instance()->themeType() == DGuiApplicationHelper::LightType + ? "" + : ""; + msgLabel->setText(msgFormat.arg(icon)); + }; + updateIcon(); + connect(DGuiApplicationHelper::instance(), &DGuiApplicationHelper::themeTypeChanged, this, [=] { updateIcon(); }); + + layout->addStretch(1); + layout->addWidget(configureLabel); + layout->addSpacing(50); + layout->addWidget(titleLabel); + layout->addWidget(msgLabel); + layout->addStretch(2); + + return widget; +} + +QWidget *SmartUTWidget::createMainWidget() +{ + auto createButton = [this](const QString &icon, const QString &tips) { + auto btn = new DToolButton(this); + btn->setIconSize({ 16, 16 }); + btn->setIcon(QIcon::fromTheme(icon)); + btn->setToolTip(tips); + return btn; + }; + + QWidget *widget = new QWidget(this); + QVBoxLayout *layout = new QVBoxLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + prjView = new ProjectTreeView(ProjectTreeView::UnitTest, this); + modelCB = new DComboBox(this); + modelCB->addItems(SmartUTManager::instance()->modelList()); + generateBtn = createButton("uc_generate", tr("Generate unit test files")); + runBtn = createButton("uc_run", tr("Run")); + reportBtn = createButton("uc_report", tr("Generate coverage report")); + + QHBoxLayout *bottomLayout = new QHBoxLayout; + bottomLayout->setContentsMargins(10, 10, 10, 10); + bottomLayout->addWidget(new DLabel(tr("Select Model:"), this)); + bottomLayout->addWidget(modelCB, 1); + bottomLayout->addWidget(generateBtn); + bottomLayout->addWidget(runBtn); + bottomLayout->addWidget(reportBtn); + + layout->addWidget(prjView, 1); + layout->addWidget(new DHorizontalLine(this)); + layout->addLayout(bottomLayout); + + // TODO: run and report + runBtn->setVisible(false); + reportBtn->setVisible(false); + + return widget; +} + +void SmartUTWidget::fillProjectView(const QString &workspace, const QStringList &fileList) +{ + prjView->clear(); + auto setting = SmartUTManager::instance()->utSetting(); + const auto &target = setting->value(kActiveGroup, kActiveTarget).toString(); + const auto &nameFormat = setting->value(kGeneralGroup, kNameFormat).toString(); + + QSet utFileCache; + std::vector> fileNodes; + for (const auto &f : fileList) { + if (!Utils::isCppFile(f) && !Utils::isCMakeFile(f)) + continue; + + const auto &utFile = Utils::createUTFile(workspace, f, target, nameFormat); + if (utFileCache.contains(utFile)) + continue; + + const auto &relatedFiles = Utils::relateFileList(f); + if (relatedFiles.isEmpty()) + continue; + + auto fileNode = std::make_unique(utFile); + fileNode->setSourceFiles(relatedFiles); + utFileCache.insert(utFile); + fileNodes.emplace_back(std::move(fileNode)); + } + + if (fileNodes.empty()) { + mainWidget->setCurrentIndex(0); + return; + } else { + mainWidget->setCurrentIndex(1); + } + + ProjectNode *prjNode = new ProjectNode(workspace); + prjNode->addNestedNodes(std::move(fileNodes), workspace); + prjView->setRootProjectNode(prjNode); +} + +void SmartUTWidget::createUTFiles() +{ + SmartUTManager::instance()->generateUTFiles(modelCB->currentText(), prjView->rootItem()); +} + +void SmartUTWidget::createUTFiles(NodeItem *item) +{ + SmartUTManager::instance()->generateUTFiles(modelCB->currentText(), item); +} + +void SmartUTWidget::updateModelList() +{ + const auto model = modelCB->currentText(); + modelCB->clear(); + modelCB->addItems(SmartUTManager::instance()->modelList()); + modelCB->setCurrentText(model); +} diff --git a/src/plugins/smartut/gui/smartutwidget.h b/src/plugins/smartut/gui/smartutwidget.h new file mode 100644 index 000000000..f23ac83da --- /dev/null +++ b/src/plugins/smartut/gui/smartutwidget.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef SMARTUTWIDGET_H +#define SMARTUTWIDGET_H + +#include +#include + +#include + +class ProjectTreeView; +class SettingDialog; +class NodeItem; +class SmartUTWidget : public QWidget +{ + Q_OBJECT +public: + explicit SmartUTWidget(QWidget *parent = nullptr); + + void showSettingDialog(); + +public Q_SLOTS: + void createUTFiles(); + void createUTFiles(NodeItem *item); + void updateModelList(); + +private: + void initUI(); + void initConnection(); + QWidget *createBlankPage(); + QWidget *createMainWidget(); + + void fillProjectView(const QString &workspace, const QStringList &fileList); + +private: + QStackedWidget *mainWidget { nullptr }; + DTK_WIDGET_NAMESPACE::DComboBox *modelCB { nullptr }; + DTK_WIDGET_NAMESPACE::DToolButton *generateBtn { nullptr }; + DTK_WIDGET_NAMESPACE::DToolButton *runBtn { nullptr }; + DTK_WIDGET_NAMESPACE::DToolButton *reportBtn { nullptr }; + ProjectTreeView *prjView { nullptr }; + SettingDialog *settingDlg { nullptr }; +}; + +#endif // SMARTUTWIDGET_H diff --git a/src/plugins/smartut/gui/widget/generalsettingwidget.cpp b/src/plugins/smartut/gui/widget/generalsettingwidget.cpp new file mode 100644 index 000000000..92c03e3f0 --- /dev/null +++ b/src/plugins/smartut/gui/widget/generalsettingwidget.cpp @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "generalsettingwidget.h" +#include "manager/smartutmanager.h" + +#include +#include +#include + +#include + +DWIDGET_USE_NAMESPACE + +GeneralSettingWidget::GeneralSettingWidget(QWidget *parent) + : DFrame(parent) +{ + initUI(); + initConnection(); +} + +void GeneralSettingWidget::initUI() +{ + QGridLayout *mainLayout = new QGridLayout(this); + mainLayout->setColumnStretch(1, 1); + mainLayout->setSpacing(10); + mainLayout->setAlignment(Qt::AlignTop); + + testFrameworkCB = new DComboBox(this); + templateCB = new DComboBox(this); + templateAddBtn = new DPushButton(this); + templateAddBtn->setIconSize({ 16, 16 }); + templateAddBtn->setIcon(DStyle::standardIcon(style(), DStyle::SP_IncreaseElement)); + templateDelBtn = new DPushButton(this); + templateDelBtn->setIconSize({ 16, 16 }); + templateDelBtn->setIcon(DStyle::standardIcon(style(), DStyle::SP_DecreaseElement)); + templateDelBtn->setEnabled(false); + + nameFormatEdit = new DLineEdit(this); + nameFormatEdit->setPlaceholderText("e.g.ut_${filename}.cpp"); + + mainLayout->addWidget(new DLabel(tr("Test Framework"), this), 0, 0); + mainLayout->addWidget(testFrameworkCB, 0, 1, 1, 3); + mainLayout->addWidget(new DLabel(tr("Template"), this), 1, 0); + mainLayout->addWidget(templateCB, 1, 1); + mainLayout->addWidget(templateAddBtn, 1, 2); + mainLayout->addWidget(templateDelBtn, 1, 3); + mainLayout->addWidget(new DLabel(tr("Name Format"), this), 2, 0); + mainLayout->addWidget(nameFormatEdit, 2, 1, 1, 3); +} + +void GeneralSettingWidget::initConnection() +{ + connect(templateCB, &DComboBox::currentTextChanged, this, &GeneralSettingWidget::handleTemplateChanged); + connect(templateAddBtn, &DPushButton::clicked, this, &GeneralSettingWidget::handleAddTemplate); + connect(templateDelBtn, &DPushButton::clicked, this, &GeneralSettingWidget::handleDeleteTemplate); + connect(nameFormatEdit, &DLineEdit::textChanged, this, + [this] { + if (nameFormatEdit->isAlert()) + nameFormatEdit->setAlert(false); + }); +} + +void GeneralSettingWidget::updateSettings() +{ + templateCB->clear(); + testFrameworkCB->clear(); + auto settings = SmartUTManager::instance()->utSetting(); + + const auto &frameworks = settings->value(kGeneralGroup, kTestFrameworks).toStringList(); + testFrameworkCB->addItems(frameworks); + const auto &activeFramework = settings->value(kActiveGroup, kActiveTestFramework).toString(); + if (frameworks.contains(activeFramework)) + testFrameworkCB->setCurrentIndex(frameworks.indexOf(activeFramework)); + + const auto &tempList = settings->value(kGeneralGroup, kTemplates).toStringList(); + templateCB->addItems(tempList); + const auto &activeTemp = settings->value(kActiveGroup, kActiveTemplate).toString(); + if (tempList.contains(activeTemp)) + templateCB->setCurrentIndex(tempList.indexOf(activeTemp)); + templateCB->insertItem(0, tr("None")); + + const auto &nameFormat = settings->value(kGeneralGroup, kNameFormat).toString(); + nameFormatEdit->setText(nameFormat); +} + +void GeneralSettingWidget::handleTemplateChanged() +{ + const auto &temp = templateCB->currentText(); + const auto &defaultTemps = SmartUTManager::instance()->utSetting()->defaultValue(kGeneralGroup, kTemplates).toString(); + // None or default + if (templateCB->currentIndex() == 0 || defaultTemps.contains(temp)) + templateDelBtn->setEnabled(false); + else + templateDelBtn->setEnabled(true); +} + +void GeneralSettingWidget::handleAddTemplate() +{ + const auto &fileName = QFileDialog::getOpenFileName(this, tr("Select Template"), "", "Template(*.cpp)"); + if (fileName.isEmpty()) + return; + + templateCB->addItem(fileName); + templateCB->setCurrentText(fileName); +} + +void GeneralSettingWidget::handleDeleteTemplate() +{ + DDialog dlg(this); + dlg.setIcon(QIcon::fromTheme("ide")); + dlg.setWindowTitle(tr("Delete Template")); + dlg.setMessage(tr("Are you sure to delete this template?")); + dlg.addButton(tr("Cancel", "button")); + dlg.addButton(tr("Ok", "button"), true, DDialog::ButtonRecommend); + + if (dlg.exec() == 1) + templateCB->removeItem(templateCB->currentIndex()); +} + +bool GeneralSettingWidget::apply() +{ + auto setting = SmartUTManager::instance()->utSetting(); + const auto &format = nameFormatEdit->text(); + if (format.isEmpty() || !format.contains("${filename}")) { + nameFormatEdit->setAlert(true); + nameFormatEdit->showAlertMessage(tr("Please input a valid format, e.g.ut_${filename}.cpp")); + nameFormatEdit->setFocus(); + nameFormatEdit->lineEdit()->selectAll(); + return false; + } + setting->setValue(kGeneralGroup, kNameFormat, format); + + QStringList templateList; + for (int i = 1; i < templateCB->count(); ++i) { + templateList << templateCB->itemText(i); + } + + setting->setValue(kGeneralGroup, kTemplates, templateList); + setting->setValue(kActiveGroup, kActiveTemplate, templateCB->currentIndex() == 0 ? "" : templateCB->currentText()); + setting->setValue(kActiveGroup, kActiveTestFramework, testFrameworkCB->currentText()); + return true; +} diff --git a/src/plugins/smartut/gui/widget/generalsettingwidget.h b/src/plugins/smartut/gui/widget/generalsettingwidget.h new file mode 100644 index 000000000..1b54d36c1 --- /dev/null +++ b/src/plugins/smartut/gui/widget/generalsettingwidget.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef GENERALSETTINGWIDGET_H +#define GENERALSETTINGWIDGET_H + +#include +#include +#include +#include + +class GeneralSettingWidget : public DTK_WIDGET_NAMESPACE::DFrame +{ + Q_OBJECT +public: + explicit GeneralSettingWidget(QWidget *parent = nullptr); + + bool apply(); + void updateSettings(); + +private: + void initUI(); + void initConnection(); + +private Q_SLOTS: + void handleTemplateChanged(); + void handleAddTemplate(); + void handleDeleteTemplate(); + +private: + DTK_WIDGET_NAMESPACE::DComboBox *testFrameworkCB { nullptr }; + DTK_WIDGET_NAMESPACE::DComboBox *templateCB { nullptr }; + DTK_WIDGET_NAMESPACE::DPushButton *templateAddBtn { nullptr }; + DTK_WIDGET_NAMESPACE::DPushButton *templateDelBtn { nullptr }; + DTK_WIDGET_NAMESPACE::DLineEdit *nameFormatEdit { nullptr }; +}; + +#endif // GENERALSETTINGWIDGET_H diff --git a/src/plugins/smartut/gui/widget/promptsettingwidget.cpp b/src/plugins/smartut/gui/widget/promptsettingwidget.cpp new file mode 100644 index 000000000..58b64e941 --- /dev/null +++ b/src/plugins/smartut/gui/widget/promptsettingwidget.cpp @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "promptsettingwidget.h" +#include "manager/smartutmanager.h" + +#include +#include +#include + +#include + +DWIDGET_USE_NAMESPACE + +PromptSettingWidget::PromptSettingWidget(QWidget *parent) + : DFrame(parent) +{ + initUI(); + initConnection(); +} + +void PromptSettingWidget::initUI() +{ + auto createButton = [this](const QIcon &icon, const QString &tips) { + auto btn = new DPushButton(this); + btn->setIconSize({ 16, 16 }); + btn->setIcon(icon); + btn->setToolTip(tips); + return btn; + }; + + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + DLabel *label = new DLabel(tr("Select Prompt"), this); + promptCB = new DComboBox(this); + promptEdit = new QTextEdit(this); + promptEdit->setFrameShape(QFrame::NoFrame); + promptEdit->viewport()->installEventFilter(this); + promptEdit->installEventFilter(this); + addBtn = createButton(DStyle::standardIcon(style(), DStyle::SP_IncreaseElement), tr("Add Prompt")); + delBtn = createButton(DStyle::standardIcon(style(), DStyle::SP_DecreaseElement), tr("Delete Prompt")); + + QHBoxLayout *topLayout = new QHBoxLayout; + topLayout->addWidget(label); + topLayout->addWidget(promptCB, 1); + + QVBoxLayout *btnLayout = new QVBoxLayout; + btnLayout->addWidget(addBtn); + btnLayout->addWidget(delBtn); + btnLayout->addStretch(1); + + QHBoxLayout *contentLayout = new QHBoxLayout; + contentLayout->addWidget(promptEdit); + contentLayout->addLayout(btnLayout); + + mainLayout->addLayout(topLayout); + mainLayout->addLayout(contentLayout); +} + +void PromptSettingWidget::initConnection() +{ + connect(promptCB, &DComboBox::currentTextChanged, this, + [this] { + promptEdit->setPlainText(promptCB->currentData().toString()); + const auto &defaultPrompts = SmartUTManager::instance()->utSetting()->defaultValue(kGeneralGroup, kPrompts).toMap(); + bool isDefault = defaultPrompts.contains(promptCB->currentText()); + delBtn->setEnabled(!isDefault); + promptEdit->setReadOnly(isDefault); + }); + connect(addBtn, &DPushButton::clicked, this, &PromptSettingWidget::handleAddPrompt); + connect(delBtn, &DPushButton::clicked, this, &PromptSettingWidget::handleDeletePrompt); +} + +void PromptSettingWidget::updateSettings() +{ + promptCB->clear(); + + auto setting = SmartUTManager::instance()->utSetting(); + const auto &prompts = setting->value(kGeneralGroup, kPrompts).toMap(); + for (auto iter = prompts.cbegin(); iter != prompts.cend(); ++iter) { + promptCB->addItem(iter.key(), iter.value()); + } + + const auto &activePrompt = setting->value(kActiveGroup, kActivePrompt).toString(); + promptEdit->setPlainText(prompts.value(activePrompt).toString()); +} + +void PromptSettingWidget::handleAddPrompt() +{ + DDialog dlg(this); + dlg.setIcon(QIcon::fromTheme("ide")); + dlg.setWindowTitle(tr("Add Prompt")); + + DLineEdit *edit = new DLineEdit(&dlg); + edit->setPlaceholderText(tr("Please input the name of the prompt")); + dlg.addContent(edit); + dlg.addButton(tr("Cancel", "button")); + dlg.addButton(tr("OK", "button"), true, DDialog::ButtonRecommend); + dlg.getButton(1)->setEnabled(false); + dlg.setOnButtonClickedClose(false); + dlg.setFocusProxy(edit); + + connect(edit, &DLineEdit::textChanged, this, + [edit, &dlg] { + if (edit->isAlert()) + edit->setAlert(false); + dlg.getButton(1)->setEnabled(!edit->text().isEmpty()); + }); + connect(&dlg, &DDialog::buttonClicked, this, + [this, edit, &dlg](int index) { + if (index == 1) { + const auto &name = edit->text(); + if (promptCB->findText(name) != -1) { + edit->setAlert(true); + edit->showAlertMessage(tr("A prompt named \"%1\" already exists").arg(name)); + edit->lineEdit()->selectAll(); + edit->setFocus(); + return; + } else { + promptCB->addItem(name); + promptCB->setCurrentText(name); + } + } + dlg.close(); + }); + + dlg.exec(); +} + +void PromptSettingWidget::handleDeletePrompt() +{ + DDialog dlg(this); + dlg.setIcon(QIcon::fromTheme("ide")); + dlg.setWindowTitle(tr("Delete Prompt")); + dlg.setMessage(tr("Are you sure you want to delete the \"%1\" prompt").arg(promptCB->currentText())); + dlg.addButton(tr("Cancel", "button")); + dlg.addButton(tr("Ok", "button"), true, DDialog::ButtonRecommend); + + int ret = dlg.exec(); + if (ret == 1) + promptCB->removeItem(promptCB->currentIndex()); +} + +void PromptSettingWidget::apply() +{ + QVariantMap prompts; + for (int i = 0; i < promptCB->count(); ++i) { + prompts.insert(promptCB->itemText(i), promptCB->itemData(i)); + } + + auto setting = SmartUTManager::instance()->utSetting(); + setting->setValue(kGeneralGroup, kPrompts, prompts); + setting->setValue(kActiveGroup, kActivePrompt, promptCB->currentText()); +} + +bool PromptSettingWidget::eventFilter(QObject *obj, QEvent *e) +{ + if (promptEdit && obj == promptEdit->viewport() && e->type() == QEvent::Paint) { + QPainter painter(promptEdit->viewport()); + painter.setRenderHint(QPainter::Antialiasing); + + auto p = promptEdit->viewport()->palette(); + painter.setPen(Qt::NoPen); + painter.setBrush(p.brush(QPalette::Active, QPalette::AlternateBase)); + + QPainterPath path; + path.addRoundedRect(promptEdit->viewport()->rect(), 8, 8); + painter.drawPath(path); + } else if (promptEdit && obj == promptEdit && e->type() == QEvent::FocusOut && promptCB) { + promptCB->setItemData(promptCB->currentIndex(), promptEdit->toPlainText()); + } + + return DFrame::eventFilter(obj, e); +} diff --git a/src/plugins/smartut/gui/widget/promptsettingwidget.h b/src/plugins/smartut/gui/widget/promptsettingwidget.h new file mode 100644 index 000000000..17d5d2d3e --- /dev/null +++ b/src/plugins/smartut/gui/widget/promptsettingwidget.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PROMPTSETTINGWIDGET_H +#define PROMPTSETTINGWIDGET_H + +#include +#include +#include + +#include + +class PromptSettingWidget : public DTK_WIDGET_NAMESPACE::DFrame +{ +public: + explicit PromptSettingWidget(QWidget *parent = nullptr); + + void apply(); + void updateSettings(); + +protected: + bool eventFilter(QObject *obj, QEvent *e) override; + +private: + void initUI(); + void initConnection(); + + void handleAddPrompt(); + void handleDeletePrompt(); + +private: + DTK_WIDGET_NAMESPACE::DComboBox *promptCB { nullptr }; + DTK_WIDGET_NAMESPACE::DPushButton *addBtn { nullptr }; + DTK_WIDGET_NAMESPACE::DPushButton *delBtn { nullptr }; + QTextEdit *promptEdit { nullptr }; +}; + +#endif // PROMPTSETTINGWIDGET_H diff --git a/src/plugins/smartut/gui/widget/resourcesettingwidget.cpp b/src/plugins/smartut/gui/widget/resourcesettingwidget.cpp new file mode 100644 index 000000000..4dbe8f969 --- /dev/null +++ b/src/plugins/smartut/gui/widget/resourcesettingwidget.cpp @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "resourcesettingwidget.h" +#include "manager/smartutmanager.h" +#include "utils/utils.h" + +#include +#include + +#include +#include +#include + +DWIDGET_USE_NAMESPACE + +ResourceSettingWidget::ResourceSettingWidget(QWidget *parent) + : DFrame(parent) +{ + initUI(); + initConnection(); +} + +void ResourceSettingWidget::initUI() +{ + QGridLayout *mainLayout = new QGridLayout(this); + mainLayout->setColumnStretch(1, 1); + mainLayout->setSpacing(10); + + projectCB = new DComboBox(this); + prjView = new ProjectTreeView(ProjectTreeView::Project, this); + prjView->viewport()->installEventFilter(this); + + targetLocationEdit = new DLineEdit(this); + targetSelBtn = new DSuggestButton(this); + targetSelBtn->setIconSize({ 16, 16 }); + targetSelBtn->setIcon(DStyle::standardIcon(style(), DStyle::SP_SelectElement)); + + mainLayout->addWidget(projectCB, 0, 0, 1, 3); + mainLayout->addWidget(new DLabel(tr("Source Files"), this), 1, 0); + mainLayout->addWidget(prjView, 1, 1, 1, 2); + mainLayout->addWidget(new DLabel(tr("Target Location"), this), 2, 0); + mainLayout->addWidget(targetLocationEdit, 2, 1); + mainLayout->addWidget(targetSelBtn, 2, 2); +} + +void ResourceSettingWidget::initConnection() +{ + connect(projectCB, &DComboBox::currentTextChanged, this, &ResourceSettingWidget::handleProjectChanged); + connect(targetSelBtn, &DSuggestButton::clicked, this, &ResourceSettingWidget::handleSelectLocation); + connect(targetLocationEdit, &DLineEdit::textChanged, this, + [this] { + if (targetLocationEdit->isAlert()) + targetLocationEdit->setAlert(false); + }); +} + +void ResourceSettingWidget::updateSettings() +{ + auto settings = SmartUTManager::instance()->utSetting(); + targetLocationEdit->setText(settings->value(kActiveGroup, kActiveTarget).toString()); + + const auto &infoList = SmartUTManager::instance()->projectList(); + QStringList projectList; + std::transform(infoList.cbegin(), infoList.cend(), std::back_inserter(projectList), + [](const dpfservice::ProjectInfo &info) { + return QFileInfo(info.workspaceFolder()).baseName(); + }); + + for (int i = projectCB->count() - 1; i >= 0; --i) { + if (!projectList.contains(projectCB->itemText(i))) + projectCB->removeItem(i); + } + + for (const auto &info : infoList) { + const auto &itemText = QFileInfo(info.workspaceFolder()).baseName(); + if (projectCB->findText(itemText) == -1) + projectCB->addItem(itemText, qVariantFromValue(info)); + } +} + +QStringList ResourceSettingWidget::selectedFileList(NodeItem *item) +{ + QStringList selList; + for (int i = 0; i < item->rowCount(); ++i) { + NodeItem *nodeItem = dynamic_cast(item->child(i)); + if (!nodeItem) + continue; + + bool isChecked = nodeItem->checkState() != Qt::Unchecked; + if (!isChecked) + continue; + + if (!nodeItem->itemNode->isFileNodeType()) + selList << selectedFileList(nodeItem); + else + selList << nodeItem->itemNode->filePath(); + } + + return selList; +} + +void ResourceSettingWidget::handleProjectChanged() +{ + prjView->clear(); + + const auto &prjInfo = projectCB->currentData().value(); + if (prjInfo.isEmpty()) + return; + auto prjNode = Utils::createProjectNode(prjInfo); + prjView->setRootProjectNode(prjNode); + + if (targetLocationEdit->text().isEmpty()) { + QString path = prjInfo.workspaceFolder() + QDir::separator() + "test"; + targetLocationEdit->setText(path); + } +} + +void ResourceSettingWidget::handleSelectLocation() +{ + const auto &prjInfo = projectCB->currentData().value(); + const auto &path = QFileDialog::getExistingDirectory(this, tr("Select target location"), + prjInfo.isEmpty() + ? QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + : prjInfo.workspaceFolder()); + if (!path.isEmpty()) + targetLocationEdit->setText(path); +} + +bool ResourceSettingWidget::apply() +{ + auto setting = SmartUTManager::instance()->utSetting(); + const auto &target = targetLocationEdit->text(); + if (target.isEmpty() || !Utils::isValidPath(target)) { + targetLocationEdit->setAlert(true); + targetLocationEdit->showAlertMessage(tr("Please input a valid path")); + targetLocationEdit->setFocus(); + targetLocationEdit->lineEdit()->selectAll(); + return false; + } + setting->setValue(kActiveGroup, kActiveTarget, target); + + return true; +} + +QStringList ResourceSettingWidget::selectedFileList() +{ + NodeItem *rootItem = prjView->rootItem(); + if (!rootItem) + return {}; + + return selectedFileList(rootItem); +} + +QString ResourceSettingWidget::selectedProject() +{ + const auto &prjInfo = projectCB->currentData().value(); + return prjInfo.workspaceFolder(); +} + +bool ResourceSettingWidget::eventFilter(QObject *obj, QEvent *e) +{ + if (prjView && obj == prjView->viewport() && e->type() == QEvent::Paint) { + QPainter painter(prjView->viewport()); + painter.setRenderHint(QPainter::Antialiasing); + + auto p = prjView->viewport()->palette(); + painter.setPen(Qt::NoPen); + painter.setBrush(p.brush(QPalette::Active, QPalette::AlternateBase)); + + QPainterPath path; + path.addRoundedRect(prjView->viewport()->rect(), 8, 8); + painter.drawPath(path); + } + + return DFrame::eventFilter(obj, e); +} diff --git a/src/plugins/smartut/gui/widget/resourcesettingwidget.h b/src/plugins/smartut/gui/widget/resourcesettingwidget.h new file mode 100644 index 000000000..5cf37b639 --- /dev/null +++ b/src/plugins/smartut/gui/widget/resourcesettingwidget.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RESOURCESETTINGWIDGET_H +#define RESOURCESETTINGWIDGET_H + +#include "common/itemnode.h" +#include "gui/projecttreeview.h" + +#include +#include +#include +#include + +class ResourceSettingWidget : public DTK_WIDGET_NAMESPACE::DFrame +{ + Q_OBJECT +public: + explicit ResourceSettingWidget(QWidget *parent = nullptr); + + bool apply(); + void updateSettings(); + QStringList selectedFileList(); + QString selectedProject(); + +protected: + bool eventFilter(QObject *obj, QEvent *e) override; + +private: + void initUI(); + void initConnection(); + QStringList selectedFileList(NodeItem *item); + +private Q_SLOTS: + void handleProjectChanged(); + void handleSelectLocation(); + +private: + DTK_WIDGET_NAMESPACE::DComboBox *projectCB { nullptr }; + DTK_WIDGET_NAMESPACE::DLineEdit *targetLocationEdit { nullptr }; + DTK_WIDGET_NAMESPACE::DSuggestButton *targetSelBtn { nullptr }; + ProjectTreeView *prjView { nullptr }; +}; + +#endif // RESOURCESETTINGWIDGET_H diff --git a/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_configure_248px.svg b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_configure_248px.svg new file mode 100644 index 000000000..b4aaf5885 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_configure_248px.svg @@ -0,0 +1,149 @@ + + + ICON/Default graph-dark + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_failure_16px.svg b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_failure_16px.svg new file mode 100644 index 000000000..9e4b7f660 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_failure_16px.svg @@ -0,0 +1,23 @@ + + + ICON / status / failure-dark + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_ignore_16px.svg b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_ignore_16px.svg new file mode 100644 index 000000000..f418f11f6 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_ignore_16px.svg @@ -0,0 +1,23 @@ + + + ICON / status /ignore-dark + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_settings-dark.svg b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_settings-dark.svg new file mode 100644 index 000000000..857c1912a --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_settings-dark.svg @@ -0,0 +1,7 @@ + + + ICON / list /settings-dark + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_success_16px.svg b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_success_16px.svg new file mode 100644 index 000000000..2e2844531 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_success_16px.svg @@ -0,0 +1,25 @@ + + + ICON / status /success-dark + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_wait_16px.svg b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_wait_16px.svg new file mode 100644 index 000000000..873604c95 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/dark/icons/uc_wait_16px.svg @@ -0,0 +1,25 @@ + + + ICON / status /wait-dark + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_configure_248px.svg b/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_configure_248px.svg new file mode 100644 index 000000000..076712d75 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_configure_248px.svg @@ -0,0 +1,149 @@ + + + ICON/Default graph-light + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_failure_16px.svg b/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_failure_16px.svg new file mode 100644 index 000000000..3bbc014a9 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_failure_16px.svg @@ -0,0 +1,25 @@ + + + ICON / status / failure-light + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_ignore_16px.svg b/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_ignore_16px.svg new file mode 100644 index 000000000..ca5678374 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_ignore_16px.svg @@ -0,0 +1,23 @@ + + + ICON / status /ignore-light + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_success_16px.svg b/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_success_16px.svg new file mode 100644 index 000000000..0949a07dd --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_success_16px.svg @@ -0,0 +1,23 @@ + + + ICON / status /success-light + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_wait_16px.svg b/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_wait_16px.svg new file mode 100644 index 000000000..b123340f8 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/light/icons/uc_wait_16px.svg @@ -0,0 +1,25 @@ + + + ICON / status /wait-light + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/texts/uc_generate_16px.svg b/src/plugins/smartut/icons/deepin/builtin/texts/uc_generate_16px.svg new file mode 100644 index 000000000..6b84db238 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/texts/uc_generate_16px.svg @@ -0,0 +1,7 @@ + + + ICON / button / Generating unit tests + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/texts/uc_report_16px.svg b/src/plugins/smartut/icons/deepin/builtin/texts/uc_report_16px.svg new file mode 100644 index 000000000..6328f2d59 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/texts/uc_report_16px.svg @@ -0,0 +1,7 @@ + + + ICON / button / report + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/texts/uc_run_16px.svg b/src/plugins/smartut/icons/deepin/builtin/texts/uc_run_16px.svg new file mode 100644 index 000000000..726afff0c --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/texts/uc_run_16px.svg @@ -0,0 +1,7 @@ + + + ICON / button / operation + + + + \ No newline at end of file diff --git a/src/plugins/smartut/icons/deepin/builtin/texts/uc_settings_16px.svg b/src/plugins/smartut/icons/deepin/builtin/texts/uc_settings_16px.svg new file mode 100644 index 000000000..12c4f4016 --- /dev/null +++ b/src/plugins/smartut/icons/deepin/builtin/texts/uc_settings_16px.svg @@ -0,0 +1,7 @@ + + + ICON / list /settings + + + + \ No newline at end of file diff --git a/src/plugins/smartut/manager/smartutmanager.cpp b/src/plugins/smartut/manager/smartutmanager.cpp new file mode 100644 index 000000000..295e6f457 --- /dev/null +++ b/src/plugins/smartut/manager/smartutmanager.cpp @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "smartutmanager.h" +#include "uttaskpool.h" +#include "utils/utils.h" + +#include "common/util/custompaths.h" +#include "services/ai/aiservice.h" +#include "services/project/projectservice.h" + +#include + +using namespace dpfservice; + +class SmartUTManagerPrivate +{ +public: + void init(); + QString configPath() const; + + QString chunkPrompt(const QStringList &fileList, const QString &workspace); + +public: + QString errorMsg; + Settings utSetting; + + AiService *aiSrv { nullptr }; + ProjectService *prjSrv { nullptr }; + UTTaskPool taskPool; +}; + +void SmartUTManagerPrivate::init() +{ + aiSrv = dpfGetService(AiService); + prjSrv = dpfGetService(ProjectService); + Q_ASSERT(aiSrv && prjSrv); + + utSetting.load(":/configure/smartut.json", configPath()); +} + +QString SmartUTManagerPrivate::configPath() const +{ + return CustomPaths::user(CustomPaths::Flags::Configures) + + "/SmartUT/smartut.json"; +} + +QString SmartUTManagerPrivate::chunkPrompt(const QStringList &fileList, const QString &workspace) +{ + const auto &result = aiSrv->query(workspace, "code for unit test", 20); + return Utils::createChunkPrompt(result); +} + +SmartUTManager::SmartUTManager(QObject *parent) + : QObject(parent), + d(new SmartUTManagerPrivate) +{ + d->init(); + connect(&d->taskPool, &UTTaskPool::finished, this, + [this](NodeItem *item, ItemState state) { + item->state = state; + Q_EMIT itemStateChanged(item); + }); + connect(&d->taskPool, &UTTaskPool::started, this, + [this](NodeItem *item) { + item->state = Generating; + Q_EMIT itemStateChanged(item); + }); + connect(&d->taskPool, &UTTaskPool::stoped, this, + [this](NodeItem *item) { + item->state = None; + Q_EMIT itemStateChanged(item); + }); +} + +SmartUTManager::~SmartUTManager() +{ + delete d; +} + +SmartUTManager *SmartUTManager::instance() +{ + static SmartUTManager ins; + return &ins; +} + +Settings *SmartUTManager::utSetting() +{ + return &d->utSetting; +} + +QStringList SmartUTManager::modelList() const +{ + const auto &models = d->aiSrv->getAllModel(); + QStringList names; + std::transform(models.cbegin(), models.cend(), std::back_inserter(names), + [](const LLMInfo &info) { + return info.modelName; + }); + return names; +} + +QList SmartUTManager::projectList() const +{ + return d->prjSrv->getAllProjectInfo(); +} + +AbstractLLM *SmartUTManager::findModel(const QString &model) +{ + const auto &models = d->aiSrv->getAllModel(); + auto iter = std::find_if(models.cbegin(), models.cend(), + [model](const LLMInfo &info) { + return info.modelName == model; + }); + if (iter == models.cend()) { + d->errorMsg = tr("A model named \"%1\" was not found").arg(model); + return nullptr; + } + + auto llm = d->aiSrv->getLLM(*iter); + // if (!llm->checkValid(&d->errorMsg)) { + // delete llm; + // return nullptr; + // } + + return llm; +} + +QString SmartUTManager::userPrompt(const QString &framework) const +{ + QString prompt = QString("单元测试框架为:%1\n").arg(framework); + const auto &prompts = d->utSetting.value(kGeneralGroup, kPrompts).toMap(); + const auto &title = d->utSetting.value(kActiveGroup, kActivePrompt).toString(); + const auto &tempFile = d->utSetting.value(kActiveGroup, kActiveTemplate).toString(); + + prompt += prompts.value(title, "").toString(); + if (!tempFile.isEmpty() && QFile::exists(tempFile)) { + QFile file(tempFile); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return prompt; + + const auto &content = file.readAll(); + if (content.isEmpty()) + return prompt; + + prompt += "\n参考下面提供的内容生成单元测试:\n```cpp\n"; + prompt += content; + prompt += "\n```"; + } + + return prompt; +} + +void SmartUTManager::generateUTFiles(const QString &model, NodeItem *item) +{ + auto checkValid = [](NodeItem *item) { + return !item->hasChildren() + && item->itemNode->isFileNodeType() + && item->state != Ignored + && item->state != Waiting + && item->state != Generating; + }; + + if (checkValid(item)) { + item->state = Waiting; + item->userCache.clear(); + Q_EMIT itemStateChanged(item); + d->taskPool.addGenerateTask({ model, item }); + } else if (item->hasChildren()) { + for (int i = 0; i < item->rowCount(); ++i) { + generateUTFiles(model, static_cast(item->child(i))); + } + } +} + +void SmartUTManager::runTest(const dpfservice::ProjectInfo &prjInfo) +{ + //TODO: +} + +void SmartUTManager::generateCoverageReport(const dpfservice::ProjectInfo &prjInfo) +{ + //TODO: +} + +void SmartUTManager::stop() +{ + d->taskPool.stop(); +} + +void SmartUTManager::sotp(NodeItem *item) +{ + d->taskPool.stop(item); +} + +QString SmartUTManager::lastError() const +{ + return d->errorMsg; +} diff --git a/src/plugins/smartut/manager/smartutmanager.h b/src/plugins/smartut/manager/smartutmanager.h new file mode 100644 index 000000000..01d8bd5a6 --- /dev/null +++ b/src/plugins/smartut/manager/smartutmanager.h @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef SMARTUTMANAGER_H +#define SMARTUTMANAGER_H + +#include "common/itemnode.h" + +#include "common/settings/settings.h" +#include "base/ai/abstractllm.h" + +constexpr char kGeneralGroup[] { "General" }; +constexpr char kActiveGroup[] { "ActiveSettings" }; + +constexpr char kPrompts[] { "Prompts" }; +constexpr char kTestFrameworks[] { "TestFrameworks" }; +constexpr char kTemplates[] { "Templates" }; +constexpr char kNameFormat[] { "NameFormat" }; +constexpr char kActivePrompt[] { "ActivePrompt" }; +constexpr char kActiveTemplate[] { "ActiveTemplate" }; +constexpr char kActiveTarget[] { "ActiveTarget" }; +constexpr char kActiveTestFramework[] { "ActiveTestFramework" }; + +class SmartUTManagerPrivate; +class SmartUTManager : public QObject +{ + Q_OBJECT +public: + static SmartUTManager *instance(); + + Settings *utSetting(); + QStringList modelList() const; + QList projectList() const; + AbstractLLM *findModel(const QString &model); + QString userPrompt(const QString &framework) const; + + void generateUTFiles(const QString &model, NodeItem *item); + void runTest(const dpfservice::ProjectInfo &prjInfo); + void generateCoverageReport(const dpfservice::ProjectInfo &prjInfo); + void stop(); + void sotp(NodeItem *item); + + QString lastError() const; + +Q_SIGNALS: + void itemStateChanged(NodeItem *item); + +private: + SmartUTManager(QObject *parent = nullptr); + ~SmartUTManager(); + + SmartUTManagerPrivate *const d; +}; + +#endif // SMARTUTMANAGER_H diff --git a/src/plugins/smartut/manager/uttaskpool.cpp b/src/plugins/smartut/manager/uttaskpool.cpp new file mode 100644 index 000000000..72d26e074 --- /dev/null +++ b/src/plugins/smartut/manager/uttaskpool.cpp @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "uttaskpool.h" +#include "smartutmanager.h" +#include "utils/utils.h" + +#include +#include +#include + +constexpr int kMaxModelCount { 10 }; + +using namespace std::placeholders; + +UTTaskPool::UTTaskPool(QObject *parent) + : QObject(parent) +{ +} + +UTTaskPool::~UTTaskPool() +{ + stop(false); + for (auto iter = busyModels.cbegin(); iter != busyModels.cend(); ++iter) { + qDeleteAll(iter.value()); + } + busyModels.clear(); + + for (auto iter = idleModels.cbegin(); iter != idleModels.cend(); ++iter) { + qDeleteAll(iter.value()); + } + idleModels.clear(); +} + +void UTTaskPool::addGenerateTask(const Task &task) +{ + if (!idleModels.contains(task.model)) + createModels(task.model); + + if (idleModels.value(task.model).isEmpty()) { + taskQueue.enqueue(task); + } else { + auto llm = idleModels[task.model].takeFirst(); + busyModels[task.model].append(llm); + llmItemMap.insert(llm, task.item); + auto handler = std::bind(&UTTaskPool::handleReceiveResult, this, task.item, _1, _2); + Q_EMIT started(task.item); + + const QString &tstFW = SmartUTManager::instance()->utSetting()->value(kActiveGroup, kActiveTestFramework).toString(); + if (Utils::isCMakeFile(task.item->itemNode->filePath())) { + const auto &cmakePrompt = Utils::createCMakePrompt(tstFW); + llm->request(Utils::createRequestPrompt(task.item->itemNode->asFileNode(), "", cmakePrompt), handler); + } else { + const auto &userPrompt = SmartUTManager::instance()->userPrompt(tstFW); + llm->request(Utils::createRequestPrompt(task.item->itemNode->asFileNode(), "", userPrompt), handler); + } + } +} + +void UTTaskPool::stop(bool notify) +{ + if (notify) { + for (const auto &task : taskQueue) { + Q_EMIT stoped(task.item); + } + } + taskQueue.clear(); + + for (auto iter = llmItemMap.cbegin(); iter != llmItemMap.cend(); ++iter) { + iter.key()->cancel(); + if (notify) + Q_EMIT stoped(iter.value()); + } +} + +void UTTaskPool::stop(NodeItem *item) +{ + if (item->state == Generating) { + if (auto llm = llmItemMap.key(item)) { + llm->cancel(); + Q_EMIT stoped(item); + } + } else { + auto iter = std::find_if(taskQueue.cbegin(), taskQueue.cend(), + [item](const Task &t) { + return item == t.item; + }); + + if (iter != taskQueue.cend()) { + taskQueue.removeOne(*iter); + Q_EMIT stoped(item); + } + } +} + +void UTTaskPool::handleReceiveResult(NodeItem *item, const QString &data, AbstractLLM::ResponseState state) +{ + switch (state) { + case AbstractLLM::Receiving: { + item->userCache += data; + } break; + case AbstractLLM::Success: { + QFileInfo info(item->itemNode->filePath()); + if (!QFile::exists(info.absolutePath())) + QDir().mkpath(info.absolutePath()); + + QFile file(item->itemNode->filePath()); + if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream out(&file); + const auto &codeList = Utils::queryCodePart(item->userCache, Utils::isCMakeFile(item->itemNode->filePath()) ? "cmake" : "cpp"); + for (const auto &code : codeList) { + out << code; + } + file.close(); + } + } break; + default: + break; + } + + if (state != AbstractLLM::Receiving) { + item->userCache.clear(); + if (state == AbstractLLM::Failed) + item->userCache = data; + Q_EMIT finished(item, state == AbstractLLM::Success ? Completed : Failed); + } +} + +void UTTaskPool::handleModelStateChanged(AbstractLLM *llm) +{ + if (llm->modelState() == AbstractLLM::Idle) { + busyModels[llm->modelName()].removeOne(llm); + idleModels[llm->modelName()].append(llm); + llmItemMap.remove(llm); + if (!taskQueue.isEmpty()) + addGenerateTask(taskQueue.dequeue()); + } +} + +void UTTaskPool::createModels(const QString &model) +{ + QList models; + for (int i = 0; i < kMaxModelCount; ++i) { + auto llm = SmartUTManager::instance()->findModel(model); + if (llm) { + models << llm; + connect(llm, &AbstractLLM::modelStateChanged, this, std::bind(&UTTaskPool::handleModelStateChanged, this, llm)); + } + } + + if (!models.isEmpty()) + idleModels.insert(model, models); +} diff --git a/src/plugins/smartut/manager/uttaskpool.h b/src/plugins/smartut/manager/uttaskpool.h new file mode 100644 index 000000000..c49609af4 --- /dev/null +++ b/src/plugins/smartut/manager/uttaskpool.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef UTTASKPOOL_H +#define UTTASKPOOL_H + +#include "common/itemnode.h" + +#include "base/ai/abstractllm.h" + +#include + +class UTTaskPool : public QObject +{ + Q_OBJECT +public: + struct Task + { + QString model; + NodeItem *item; + + bool operator==(const Task &task) const + { + return model == task.model && item == task.item; + } + }; + + explicit UTTaskPool(QObject *parent = nullptr); + ~UTTaskPool(); + + void addGenerateTask(const Task &task); + void stop(bool notify = true); + void stop(NodeItem *item); + +public Q_SLOTS: + void generateUTFile(const NodeItem *item); + void handleReceiveResult(NodeItem *item, const QString &data, AbstractLLM::ResponseState state); + void handleModelStateChanged(AbstractLLM *llm); + +Q_SIGNALS: + void started(NodeItem *item); + void finished(NodeItem *item, ItemState state); + void stoped(NodeItem *item); + +private: + void createModels(const QString &model); + + QQueue taskQueue; + QMap> idleModels; + QMap> busyModels; + QMap llmItemMap; +}; + +#endif // UTTASKPOOL_H diff --git a/src/plugins/smartut/smartut.cpp b/src/plugins/smartut/smartut.cpp new file mode 100644 index 000000000..428697a0c --- /dev/null +++ b/src/plugins/smartut/smartut.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "smartut.h" +#include "gui/smartutwidget.h" + +#include "base/abstractwidget.h" +#include "services/window/windowservice.h" + +DWIDGET_USE_NAMESPACE + +void SmartUT::initialize() +{ +} + +bool SmartUT::start() +{ + auto windowSrv = dpfGetService(dpfservice::WindowService); + Q_ASSERT(windowSrv); + + auto widget = new SmartUTWidget; + auto widgetImpl = new AbstractWidget(widget); + windowSrv->addWidgetRightspace("SmartUT", widgetImpl, ""); + + settingBtn = new DToolButton(widget); + settingBtn->setIconSize({16,16}); + settingBtn->setIcon(QIcon::fromTheme("uc_settings")); + connect(settingBtn, &DToolButton::clicked, widget, &SmartUTWidget::showSettingDialog); + windowSrv->registerToolBtnToRightspaceWidget(settingBtn, "SmartUT"); + + return true; +} + +dpf::Plugin::ShutdownFlag SmartUT::stop() +{ + return Sync; +} diff --git a/src/plugins/smartut/smartut.h b/src/plugins/smartut/smartut.h new file mode 100644 index 000000000..7e25fa864 --- /dev/null +++ b/src/plugins/smartut/smartut.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef SMARTUT_H +#define SMARTUT_H + +#include + +#include + +class SmartUT : public dpf::Plugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.deepin.plugin.unioncode" FILE "smartut.json") +public: + virtual void initialize() override; + virtual bool start() override; + virtual dpf::Plugin::ShutdownFlag stop() override; + +private: + DTK_WIDGET_NAMESPACE::DToolButton *settingBtn { nullptr }; +}; + +#endif // SMARTUT_H diff --git a/src/plugins/smartut/smartut.json b/src/plugins/smartut/smartut.json new file mode 100644 index 000000000..c456512a2 --- /dev/null +++ b/src/plugins/smartut/smartut.json @@ -0,0 +1,16 @@ +{ + "Name" : "SmartUT", + "Version" : "4.8.2", + "CompatVersion" : "4.8.0", + "Vendor" : "The Uniontech Software Technology Co., Ltd.", + "Copyright" : "Copyright (C) 2020 ~ 2024 Uniontech Software Technology Co., Ltd.", + "License" : [ + "GPL-3.0-or-later" + ], + "Category" : "Core Plugins", + "Description" : "The Unit Test plugin for the unioncode.", + "UrlLink" : "https://www.uniontech.com", + "Depends" : [ + {"Name" : "core"} + ] +} diff --git a/src/plugins/smartut/smartut.qrc b/src/plugins/smartut/smartut.qrc new file mode 100644 index 000000000..336f8b690 --- /dev/null +++ b/src/plugins/smartut/smartut.qrc @@ -0,0 +1,21 @@ + + + + configure/smartut.json + icons/deepin/builtin/texts/uc_settings_16px.svg + icons/deepin/builtin/texts/uc_run_16px.svg + icons/deepin/builtin/texts/uc_report_16px.svg + icons/deepin/builtin/texts/uc_generate_16px.svg + icons/deepin/builtin/dark/icons/uc_configure_248px.svg + icons/deepin/builtin/dark/icons/uc_failure_16px.svg + icons/deepin/builtin/dark/icons/uc_ignore_16px.svg + icons/deepin/builtin/dark/icons/uc_settings-dark.svg + icons/deepin/builtin/dark/icons/uc_success_16px.svg + icons/deepin/builtin/light/icons/uc_configure_248px.svg + icons/deepin/builtin/light/icons/uc_failure_16px.svg + icons/deepin/builtin/light/icons/uc_ignore_16px.svg + icons/deepin/builtin/light/icons/uc_success_16px.svg + icons/deepin/builtin/light/icons/uc_wait_16px.svg + icons/deepin/builtin/dark/icons/uc_wait_16px.svg + + diff --git a/src/plugins/smartut/utils/utils.cpp b/src/plugins/smartut/utils/utils.cpp new file mode 100644 index 000000000..a2041ce9c --- /dev/null +++ b/src/plugins/smartut/utils/utils.cpp @@ -0,0 +1,284 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include + +ProjectNode *Utils::createProjectNode(const dpfservice::ProjectInfo &info) +{ + if (info.isEmpty()) + return nullptr; + + ProjectNode *prjNode = new ProjectNode(info.workspaceFolder()); + std::vector> sourceNodes; + const auto &sources = info.sourceFiles(); + std::transform(sources.cbegin(), sources.cend(), std::back_inserter(sourceNodes), + [](const QString &f) { + return std::make_unique(f); + }); + if (!sourceNodes.empty()) + prjNode->addNestedNodes(std::move(sourceNodes), info.workspaceFolder()); + + return prjNode; +} + +FolderNode *Utils::recursiveFindOrCreateFolderNode(FolderNode *folder, + const QString &directory, + const QString &workspace, + const FolderNode::FolderNodeFactory &factory) +{ + QString path = workspace.isEmpty() ? folder->filePath() : workspace; + QString directoryWithoutPrefix; + bool isRelative = false; + + if (path.isEmpty() || path == "/") { + directoryWithoutPrefix = directory; + isRelative = false; + } else { + if (isChildOf(path, directory) || directory == path) { + isRelative = true; + directoryWithoutPrefix = relativeChildPath(path, directory); + } else { + const QString relativePath = relativeChildPath(path, directory); + if (relativePath.count("../") < 5) { + isRelative = true; + directoryWithoutPrefix = relativePath; + } else { + isRelative = false; + path.clear(); + directoryWithoutPrefix = directory; + } + } + } + QStringList parts = directoryWithoutPrefix.split('/', QString::SkipEmptyParts); + if (!isRelative && !parts.isEmpty()) + parts[0].prepend('/'); + + FolderNode *parent = folder; + for (const QString &part : std::as_const(parts)) { + path += QLatin1Char('/') + part; + // Find folder in subFolders + FolderNode *next = parent->folderNode(path); + if (!next) { + // No FolderNode yet, so create it + auto tmp = factory(path); + tmp->setDisplayName(part); + next = tmp.get(); + parent->addNode(std::move(tmp)); + } + parent = next; + } + return parent; +} + +bool Utils::isChildOf(const QString &path, const QString &subPath) +{ + if (path.isEmpty() || subPath.isEmpty()) + return false; + + if (!subPath.startsWith(path)) + return false; + if (subPath.size() <= path.size()) + return false; + if (path.endsWith(QLatin1Char('/'))) + return true; + return subPath.at(path.size()) == QLatin1Char('/'); +} + +QString Utils::relativeChildPath(const QString &path, const QString &subPath) +{ + QString res; + if (isChildOf(path, subPath)) { + res = subPath.mid(path.size()); + if (res.startsWith('/')) + res = res.mid(1); + } + return res; +} + +bool Utils::isValidPath(const QString &path) +{ + static QRegularExpression regex(R"(^(/[^/ ]*)+/?$)"); + return regex.match(path).hasMatch(); +} + +bool Utils::isCppFile(const QString &filePath) +{ + static QStringList extensions = { "h", "hpp", "c", "cpp", "cc" }; + + QFileInfo fileInfo(filePath); + QString suffix = fileInfo.suffix().toLower(); + return extensions.contains(suffix); +} + +bool Utils::isCMakeFile(const QString &filePath) +{ + QFileInfo fileInfo(filePath); + return fileInfo.fileName().compare("CMakeLists.txt", Qt::CaseInsensitive) == 0; +} + +QStringList Utils::relateFileList(const QString &filePath) +{ + QStringList relatedFileList; + if (isCppFile(filePath)) { + QFileInfo fileInfo(filePath); + QDir dir(fileInfo.absolutePath()); + QStringList filters { "*.h", "*.hpp", "*.c", "*.cpp", "*.cc" }; + QStringList allFiles = dir.entryList(filters, QDir::Files); + + bool containsHeader = false; + QString baseName = fileInfo.baseName(); + for (const auto &file : allFiles) { + QFileInfo info(file); + if (info.baseName() == baseName) { + relatedFileList << dir.absoluteFilePath(file); + if (!containsHeader) + containsHeader = (info.suffix() == "h" || info.suffix() == "hpp"); + } + } + + if (!containsHeader) + return {}; + } else { + relatedFileList << filePath; + } + + return relatedFileList; +} + +QString Utils::createUTFile(const QString &workspace, const QString &filePath, + const QString &target, const QString &nameFormat) +{ + QFileInfo info(filePath); + QString utFile; + if (isCppFile(filePath)) { + utFile = info.absolutePath().replace(workspace, target); + QString targetName = info.baseName(); + QString format = nameFormat; + format.replace("${filename}", targetName); + utFile += QDir::separator() + format; + } else { + utFile = info.absoluteFilePath().replace(workspace, target); + } + + return utFile; +} + +QString Utils::createRequestPrompt(const FileNode *node, const QString &chunkPrompt, const QString &userPrompt) +{ + QStringList prompt; + prompt << "##### Prompt #####" + << "" + << "You are an expert software developer. You give helpful and concise responses.\n" + << ""; + + QStringList fileContents; + for (const auto &file : node->sourceFiles()) { + fileContents << "```" + file; + QFile f(file); + if (f.open(QIODevice::ReadOnly)) + fileContents << f.readAll(); + fileContents << "```\n"; + } + + if (!chunkPrompt.isEmpty()) + prompt << chunkPrompt; + prompt << fileContents; + prompt << node->sourceFiles().join(" "); + prompt << userPrompt; + + return prompt.join('\n'); +} + +QString Utils::createChunkPrompt(const QJsonObject &chunkObj) +{ + QJsonArray chunks = chunkObj["Chunks"].toArray(); + if (chunks.isEmpty()) + return {}; + + QStringList chunkPrompt("Use the above code to answer the following question. " + "You should not reference any files outside of what is shown, " + "unless they are commonly known files, like a .gitignore or " + "package.json. Reference the filenames whenever possible. If " + "there isn't enough information to answer the question, suggest " + "where the user might look to learn more.\n"); + for (auto chunk : chunks) { + chunkPrompt << "```" + chunk.toObject()["fileName"].toString(); + chunkPrompt << chunk.toObject()["content"].toString(); + chunkPrompt << "```\n"; + } + + return chunkPrompt.join('\n'); +} + +QString Utils::createCMakePrompt(const QString &testFramework) +{ + QString prompt("根据上面提供的CMakeList.txt文件内容,帮我为单元测试创建CMakeList.txt文件\n\n" + "关键原则:\n" + "- 测试框架为%1\n" + "- 需要判断提供的CMakeList文件是聚合型CMakeLists还是构建型CMakeLists\n" + "- 只需要生成一个CMakeLists.txt文件,不需要生成多个\n\n" + "根据下面的规则来判断提供的CMakeLists.txt文件是聚合型CMakeLists还是构建型CMakeLists:\n" + "- 如果CMakeLists.txt文件中包含add_subdirectory命令,则为聚合型CMakeLists\n" + "- 如果CMakeLists.txt文件中包含add_executable或add_library命令,则为构建型CMakeLists\n\n" + "为聚合型CMakeLists创建单元测试的CMakeLists.txt文件要求:\n" + "- 只需要获取提供的CMakeLists.txt文件中所有的子目录并添加\n\n" + "为构建型CMakeLists创建单元测试的CMakeLists.txt文件要求:\n" + "- 单元测试源文件采用下面提供的风格来获取::\n" + "```cmake\nFILE(GLOB UT_FILES\n" + " \"${CMAKE_CURRENT_SOURCE_DIR}/*.cpp\"\n" + " \"${CMAKE_CURRENT_SOURCE_DIR}/*/*.cpp\"\n)\n" + "```\n" + "- 根据提供的CMakeLists.txt文件内容,添加必要的依赖"); + return prompt.arg(testFramework); +} + +QStringList Utils::queryCodePart(const QString &content, const QString &type) +{ + static QString regexFormat(R"(```%1\n((.*\n)*?.*)\n```)"); + QRegularExpression regex(regexFormat.arg(type)); + QRegularExpressionMatchIterator it = regex.globalMatch(content); + QStringList matches; + + while (it.hasNext()) { + QRegularExpressionMatch match = it.next(); + matches << match.captured(1); + } + + return matches; +} + +bool Utils::checkAnyState(NodeItem *item, ItemState state) +{ + if (auto node = item->itemNode->asFileNode()) { + return item->state == state; + } else if (item->hasChildren()) { + for (int i = 0; i < item->rowCount(); ++i) { + if (checkAnyState(dynamic_cast(item->child(i)), state)) + return true; + } + } + return false; +} + +bool Utils::checkAllState(NodeItem *item, ItemState state) +{ + if (auto node = item->itemNode->asFileNode()) { + return item->state == state; + } else if (item->hasChildren()) { + for (int i = 0; i < item->rowCount(); ++i) { + if (!checkAllState(dynamic_cast(item->child(i)), state)) + return false; + } + return true; + } + return false; +} diff --git a/src/plugins/smartut/utils/utils.h b/src/plugins/smartut/utils/utils.h new file mode 100644 index 000000000..181b91eb9 --- /dev/null +++ b/src/plugins/smartut/utils/utils.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef UTILS_H +#define UTILS_H + +#include "common/itemnode.h" + +enum ItemRole { + ItemStateRole = Qt::UserRole + 1 +}; + +class Utils +{ +public: + static ProjectNode *createProjectNode(const dpfservice::ProjectInfo &info); + + static FolderNode *recursiveFindOrCreateFolderNode(FolderNode *folder, + const QString &directory, + const QString &workspace, + const FolderNode::FolderNodeFactory &factory); + static bool isChildOf(const QString &path, const QString &subPath); + static QString relativeChildPath(const QString &path, const QString &subPath); + static bool isValidPath(const QString &path); + static bool isCppFile(const QString &filePath); + static bool isCMakeFile(const QString &filePath); + static QStringList relateFileList(const QString &filePath); + static QString createUTFile(const QString &workspace, const QString &filePath, + const QString &target, const QString &nameFormat); + + static QString createRequestPrompt(const FileNode *node, const QString &chunkPrompt, const QString &userPrompt); + static QString createChunkPrompt(const QJsonObject &chunkObj); + static QString createCMakePrompt(const QString &testFramework); + static QStringList queryCodePart(const QString &content, const QString &type); + static bool checkAnyState(NodeItem *item, ItemState state); + static bool checkAllState(NodeItem *item, ItemState state); +}; + +#endif // UTILS_H