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/openai/openaicompatiblellm.cpp b/src/plugins/aimanager/openai/openaicompatiblellm.cpp index fe0a8a85d..79b322aec 100644 --- a/src/plugins/aimanager/openai/openaicompatiblellm.cpp +++ b/src/plugins/aimanager/openai/openaicompatiblellm.cpp @@ -54,7 +54,7 @@ class OpenAiCompatibleLLMPrivate 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 {}; @@ -66,7 +66,7 @@ class OpenAiCompatibleLLMPrivate }; OpenAiCompatibleLLMPrivate::OpenAiCompatibleLLMPrivate(OpenAiCompatibleLLM *qq) - : q(qq) + : q(qq) { manager = new QNetworkAccessManager(qq); currentConversation = new OpenAiCompatibleConversation(); @@ -86,7 +86,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,7 +101,7 @@ 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); } @@ -165,13 +165,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(); @@ -203,7 +203,7 @@ void OpenAiCompatibleLLM::request(const QJsonObject &data) QNetworkReply *reply = d->postMessage(modelPath() + "/v1/chat/completions", d->apiKey, body); connect(this, &OpenAiCompatibleLLM::requstCancel, reply, &QNetworkReply::abort); - connect(reply, &QNetworkReply::finished, this, [=](){ + connect(reply, &QNetworkReply::finished, this, [=]() { d->waitingResponse = false; if (!d->httpResult.isEmpty()) d->currentConversation->update(d->httpResult); @@ -235,7 +235,7 @@ void OpenAiCompatibleLLM::request(const QString &prompt) QNetworkReply *reply = d->postMessage(modelPath() + "/v1/completions", d->apiKey, QJsonDocument(dataObject).toJson()); connect(this, &OpenAiCompatibleLLM::requstCancel, reply, &QNetworkReply::abort); - connect(reply, &QNetworkReply::finished, this, [=](){ + connect(reply, &QNetworkReply::finished, this, [=]() { d->waitingResponse = false; if (reply->error()) { qWarning() << "NetWork Error: " << reply->errorString(); @@ -266,14 +266,15 @@ 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, [=](){ + connect(reply, &QNetworkReply::finished, this, [=]() { d->waitingResponse = false; 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); + reply->deleteLater(); }); processResponse(reply); 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..f972bb59e --- /dev/null +++ b/src/plugins/smartut/common/itemnode.cpp @@ -0,0 +1,199 @@ +// 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 &overrideBaseDir, + const FolderNodeFactory &factory) +{ + using DirWithNodes = std::pair>>; + std::vector fileNodesPerDir; + for (auto &f : files) { + 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, + overrideBaseDir, 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..d1819f111 --- /dev/null +++ b/src/plugins/smartut/common/itemnode.h @@ -0,0 +1,144 @@ +// 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 &overrideBaseDir = "", + 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 }; +}; + +#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..9e58805cc --- /dev/null +++ b/src/plugins/smartut/common/projectitemdelegate.cpp @@ -0,0 +1,406 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "projectitemdelegate.h" +#include "utils/utils.h" +#include "common/itemnode.h" + +#include +#include +#ifdef DTKWIDGET_CLASS_DPaletteHelper +# include +#endif + +#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; +} + +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 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..5d0ca5d13 --- /dev/null +++ b/src/plugins/smartut/common/projectitemdelegate.h @@ -0,0 +1,74 @@ +// 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; + +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/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..dd417f8cb --- /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..45cd1e915 --- /dev/null +++ b/src/plugins/smartut/gui/smartutwidget.cpp @@ -0,0 +1,186 @@ +// 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 +#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); +} + +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); +} diff --git a/src/plugins/smartut/gui/smartutwidget.h b/src/plugins/smartut/gui/smartutwidget.h new file mode 100644 index 000000000..e3ebf071b --- /dev/null +++ b/src/plugins/smartut/gui/smartutwidget.h @@ -0,0 +1,46 @@ +// 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); + +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..e6416eb0d --- /dev/null +++ b/src/plugins/smartut/gui/widget/promptsettingwidget.cpp @@ -0,0 +1,173 @@ +// 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.setOnButtonClickedClose(false); + dlg.setFocusProxy(edit); + + connect(edit, &DLineEdit::textChanged, this, + [edit] { + if (edit->isAlert()) + edit->setAlert(false); + }); + 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..d95d225d6 --- /dev/null +++ b/src/plugins/smartut/manager/smartutmanager.cpp @@ -0,0 +1,176 @@ +// 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); + }); +} + +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) +{ + if (!item->hasChildren() && item->itemNode->isFileNodeType() && item->state != Ignored) { + item->state = Waiting; + 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: +} + +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..78337577a --- /dev/null +++ b/src/plugins/smartut/manager/smartutmanager.h @@ -0,0 +1,54 @@ +// 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); + + 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..f0e1dede8 --- /dev/null +++ b/src/plugins/smartut/manager/uttaskpool.cpp @@ -0,0 +1,115 @@ +// 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 kMaxThreadCount { 10 }; + +Task::Task(const QString &model, NodeItem *item) + : model(model), + item(item) +{ +} + +void Task::run() +{ + Q_EMIT started(item); + QEventLoop loop; + auto llm = SmartUTManager::instance()->findModel(model); + if (!llm) { + Q_EMIT finished(item, Failed); + return; + } + + llm->setStream(true); + connect(llm, &AbstractLLM::dataReceived, this, + [&loop, this](const QString &data, AbstractLLM::ResponseState state) { + handleReceiveResult(item, data, state); + if (state != AbstractLLM::Receiving) { + Q_EMIT finished(item, state == AbstractLLM::Success ? Completed : Failed); + loop.quit(); + } + }); + const QString &tstFW = SmartUTManager::instance()->utSetting()->value(kActiveGroup, kActiveTestFramework).toString(); + if (Utils::isCMakeFile(item->itemNode->filePath())) { + const auto &cmakePrompt = Utils::createCMakePrompt(tstFW); + llm->request(Utils::createRequestPrompt(item->itemNode->asFileNode(), "", cmakePrompt)); + } else { + const auto &userPrompt = SmartUTManager::instance()->userPrompt(tstFW); + llm->request(Utils::createRequestPrompt(item->itemNode->asFileNode(), "", userPrompt)); + } + + loop.exec(); + llm->deleteLater(); +} + +void Task::handleReceiveResult(NodeItem *item, const QString &data, AbstractLLM::ResponseState state) +{ + switch (state) { + case AbstractLLM::Receiving: + result += 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(result, Utils::isCMakeFile(item->itemNode->filePath()) ? "cmake" : "cpp"); + for (const auto &code : codeList) { + out << code; + } + file.close(); + } + } break; + default: + break; + } +} + +UTTaskPool::UTTaskPool(QObject *parent) + : QObject(parent), + threadPool(new QThreadPool(this)) +{ + threadPool->setMaxThreadCount(kMaxThreadCount); +} + +UTTaskPool::~UTTaskPool() +{ + stop(); + threadPool->waitForDone(); +} + +void UTTaskPool::addGenerateTask(const QString &model, NodeItem *item) +{ + QMutexLocker locker(&mutex); + if (!isRunning) + return; + + Task *task = new Task(model, item); + connect(task, &Task::finished, this, &UTTaskPool::finished); + connect(task, &Task::started, this, &UTTaskPool::started); + task->setAutoDelete(true); + threadPool->start(task); +} + +void UTTaskPool::waitForDone() +{ + threadPool->waitForDone(); +} + +void UTTaskPool::stop() +{ + QMutexLocker locker(&mutex); + isRunning = false; + threadPool->clear(); +} diff --git a/src/plugins/smartut/manager/uttaskpool.h b/src/plugins/smartut/manager/uttaskpool.h new file mode 100644 index 000000000..4c5efec5d --- /dev/null +++ b/src/plugins/smartut/manager/uttaskpool.h @@ -0,0 +1,61 @@ +// 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 +#include + +class Task : public QObject, public QRunnable +{ + Q_OBJECT +public: + Task() = default; + explicit Task(const QString &model, NodeItem *item); + + void run() override; + + void handleReceiveResult(NodeItem *item, const QString &data, AbstractLLM::ResponseState state); + +Q_SIGNALS: + void started(NodeItem *item); + void finished(NodeItem *item, ItemState state); + +private: + QString model; + NodeItem *item { nullptr }; + QString result; +}; + +class UTTaskPool : public QObject +{ + Q_OBJECT +public: + explicit UTTaskPool(QObject *parent = nullptr); + ~UTTaskPool(); + + void addGenerateTask(const QString &model, NodeItem *item); + + void waitForDone(); + void stop(); + +public Q_SLOTS: + void generateUTFile(const NodeItem *item); + +Q_SIGNALS: + void started(NodeItem *item); + void finished(NodeItem *item, ItemState state); + +private: + QThreadPool *threadPool { nullptr }; + QMutex mutex; + bool isRunning { true }; +}; + +#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..6095625ff --- /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 &overrideBaseDir, + const FolderNode::FolderNodeFactory &factory) +{ + QString path = overrideBaseDir.isEmpty() ? folder->filePath() : overrideBaseDir; + 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..4d4ee6bfa --- /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 &overrideBaseDir, + 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