| 1 | // SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. |
| 2 | // |
| 3 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 4 | |
| 5 | #include "cmakeprojectgenerator.h" |
| 6 | #include "cmakeasynparse.h" |
| 7 | #include "cmakeitemkeeper.h" |
| 8 | #include "cmake/project/transceiver/projectcmakereceiver.h" |
| 9 | #include "properties/buildpropertywidget.h" |
| 10 | #include "properties/runpropertywidget.h" |
| 11 | #include "properties/configpropertywidget.h" |
| 12 | #include "properties/configutil.h" |
| 13 | #include "properties/targetsmanager.h" |
| 14 | #include "services/builder/builderservice.h" |
| 15 | #include "services/window/windowservice.h" |
| 16 | #include "common/dialog/propertiesdialog.h" |
| 17 | #include "common/util/eventdefinitions.h" |
| 18 | |
| 19 | #include <QtXml> |
| 20 | #include <QFileIconProvider> |
| 21 | #include <QPushButton> |
| 22 | |
| 23 | class CmakeProjectGeneratorPrivate |
| 24 | { |
| 25 | friend class CmakeProjectGenerator; |
| 26 | |
| 27 | enum CreateItemMode { |
| 28 | NewCreateProject, |
| 29 | RebuildProject, |
| 30 | }; |
| 31 | |
| 32 | QHash<QStandardItem *, QThreadPool *> asynItemThreadPolls; |
| 33 | QList<QStandardItem *> reloadCmakeFileItems; |
| 34 | dpfservice::ProjectInfo configureProjectInfo; |
| 35 | }; |
| 36 | |
| 37 | CmakeProjectGenerator::CmakeProjectGenerator() |
| 38 | : d(new CmakeProjectGeneratorPrivate()) |
| 39 | { |
| 40 | // when execute command end can create root Item |
| 41 | QObject::connect(ProjectCmakeProxy::instance(), |
| 42 | &ProjectCmakeProxy::buildExecuteEnd, |
| 43 | this, &CmakeProjectGenerator::doBuildCmdExecuteEnd); |
| 44 | |
| 45 | // main thread init watcher class |
| 46 | CmakeItemKeeper::instance(); |
| 47 | |
| 48 | // build cmake file changed notify |
| 49 | QObject::connect(CmakeItemKeeper::instance(), |
| 50 | &CmakeItemKeeper::cmakeFileNodeNotify, |
| 51 | this, &CmakeProjectGenerator::doCmakeFileNodeChanged); |
| 52 | |
| 53 | using namespace dpfservice; |
| 54 | auto &ctx = dpfInstance.serviceContext(); |
| 55 | BuilderService *builderService = ctx.service<BuilderService>(BuilderService::name()); |
| 56 | if (!builderService) { |
| 57 | qCritical() << "Failed, not found service : builderService" ; |
| 58 | abort(); |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | CmakeProjectGenerator::~CmakeProjectGenerator() |
| 63 | { |
| 64 | qInfo() << __FUNCTION__; |
| 65 | for (auto val : d->asynItemThreadPolls.keys()) { |
| 66 | auto threadPoll = d->asynItemThreadPolls[val]; |
| 67 | if (threadPoll) { |
| 68 | threadPoll->clear(); |
| 69 | while (threadPoll->activeThreadCount() != 0) {} |
| 70 | delete threadPoll; |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | d->asynItemThreadPolls.clear(); |
| 75 | |
| 76 | if (d) |
| 77 | delete d; |
| 78 | } |
| 79 | |
| 80 | QStringList CmakeProjectGenerator::supportLanguages() |
| 81 | { |
| 82 | return { dpfservice::MWMFA_CXX }; |
| 83 | } |
| 84 | |
| 85 | QStringList CmakeProjectGenerator::supportFileNames() |
| 86 | { |
| 87 | return { "cmakelists.txt" , "CMakeLists.txt" }; |
| 88 | } |
| 89 | |
| 90 | QDialog *CmakeProjectGenerator::configureWidget(const QString &language, |
| 91 | const QString &workspace) |
| 92 | { |
| 93 | ProjectGenerator::configureWidget(language, workspace); |
| 94 | |
| 95 | config::ConfigureParam *param = config::ConfigUtil::instance()->getConfigureParamPointer(); |
| 96 | if (!config::ConfigUtil::instance()->isNeedConfig(workspace, *param)) { |
| 97 | dpfservice::ProjectInfo info; |
| 98 | if (config::ConfigUtil::instance()->updateProjectInfo(info, param)) { |
| 99 | configure(info); |
| 100 | return nullptr; |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | // show build type config pane. |
| 105 | ConfigPropertyWidget *configPropertyWidget = new ConfigPropertyWidget(language, workspace); |
| 106 | QObject::connect(config::ConfigUtil::instance(), &config::ConfigUtil::configureDone, |
| 107 | [this](const dpfservice::ProjectInfo &info) { |
| 108 | configure(info); |
| 109 | }); |
| 110 | |
| 111 | return configPropertyWidget; |
| 112 | } |
| 113 | |
| 114 | bool CmakeProjectGenerator::configure(const dpfservice::ProjectInfo &projInfo) |
| 115 | { |
| 116 | using namespace dpfservice; |
| 117 | auto &ctx = dpfInstance.serviceContext(); |
| 118 | BuilderService *builderService = ctx.service<BuilderService>(BuilderService::name()); |
| 119 | if (builderService) { |
| 120 | |
| 121 | |
| 122 | BuildCommandInfo commandInfo; |
| 123 | commandInfo.kitName = projInfo.kitName(); |
| 124 | commandInfo.program = projInfo.buildProgram(); |
| 125 | commandInfo.arguments = projInfo.configCustomArgs(); |
| 126 | commandInfo.workingDir = projInfo.workspaceFolder(); |
| 127 | |
| 128 | bool isSuccess = builderService->interface.builderCommand({commandInfo}, false); |
| 129 | if (isSuccess) { |
| 130 | ProjectCmakeProxy::instance()->setBuildCommandUuid(commandInfo.uuid); |
| 131 | // display root item before everything is done. |
| 132 | rootItem = ProjectGenerator::createRootItem(projInfo); |
| 133 | setRootItemToView(rootItem); |
| 134 | |
| 135 | dpfservice::ProjectGenerator::configure(projInfo); |
| 136 | |
| 137 | // cache project info, asyn end to use |
| 138 | d->configureProjectInfo = projInfo; |
| 139 | |
| 140 | return true; |
| 141 | } |
| 142 | } |
| 143 | return false; |
| 144 | } |
| 145 | |
| 146 | QStandardItem *CmakeProjectGenerator::createRootItem(const dpfservice::ProjectInfo &info) |
| 147 | { |
| 148 | using namespace dpfservice; |
| 149 | |
| 150 | d->asynItemThreadPolls[rootItem] = new QThreadPool; |
| 151 | |
| 152 | auto parse = new CmakeAsynParse; |
| 153 | |
| 154 | // asyn free parse, that .project file parse |
| 155 | QObject::connect(parse, &CmakeAsynParse::parseProjectEnd, |
| 156 | [=](CmakeAsynParse::ParseInfo<QStandardItem *> parseInfo) { |
| 157 | d->asynItemThreadPolls.remove(parseInfo.result); |
| 158 | // active after everything done. |
| 159 | project.activeProject(info.kitName(), info.language(), info.workspaceFolder()); |
| 160 | delete parse; |
| 161 | }); |
| 162 | |
| 163 | // asyn execute logic, that .project file parse |
| 164 | QtConcurrent::run(d->asynItemThreadPolls[rootItem], |
| 165 | parse, &CmakeAsynParse::parseProject, |
| 166 | rootItem, info); |
| 167 | |
| 168 | return rootItem; |
| 169 | } |
| 170 | |
| 171 | void CmakeProjectGenerator::removeRootItem(QStandardItem *root) |
| 172 | { |
| 173 | // remove watcher from current root item |
| 174 | CmakeItemKeeper::instance()->delCmakeFileNode(root); |
| 175 | |
| 176 | auto threadPoll = d->asynItemThreadPolls[root]; |
| 177 | if (threadPoll) { |
| 178 | threadPoll->clear(); |
| 179 | while(threadPoll->waitForDone()); |
| 180 | delete threadPoll; |
| 181 | d->asynItemThreadPolls.remove(root); |
| 182 | } |
| 183 | |
| 184 | recursionRemoveItem(root); |
| 185 | } |
| 186 | |
| 187 | QMenu *CmakeProjectGenerator::createItemMenu(const QStandardItem *item) |
| 188 | { |
| 189 | QMenu * = nullptr; |
| 190 | |
| 191 | // create parse |
| 192 | CmakeAsynParse *parse = new CmakeAsynParse(); |
| 193 | |
| 194 | // create item from syn |
| 195 | auto targetBuilds = parse->parseActions(item); |
| 196 | |
| 197 | // free parse from syn |
| 198 | delete parse; |
| 199 | |
| 200 | auto root = ProjectGenerator::root(const_cast<QStandardItem *>(item)); |
| 201 | if (!root) |
| 202 | return menu; |
| 203 | |
| 204 | if (!targetBuilds.isEmpty()) { |
| 205 | menu = new QMenu(); |
| 206 | for (auto val : targetBuilds) { |
| 207 | QAction *action = new QAction(); |
| 208 | action->setText(val.buildName); |
| 209 | action->setProperty("workDir" , dpfservice::ProjectInfo::get(root).workspaceFolder()); |
| 210 | action->setProperty(CDT_CPROJECT_KEY::get()->buildCommand.toLatin1(), val.buildCommand); |
| 211 | action->setProperty(CDT_CPROJECT_KEY::get()->buildArguments.toLatin1(), val.buildArguments); |
| 212 | action->setProperty(CDT_CPROJECT_KEY::get()->buildTarget.toLatin1(), val.buildTarget); |
| 213 | action->setProperty(CDT_CPROJECT_KEY::get()->stopOnError.toLatin1(), val.stopOnError); |
| 214 | action->setProperty(CDT_CPROJECT_KEY::get()->useDefaultCommand.toLatin1(), val.useDefaultCommand); |
| 215 | QObject::connect(action, &QAction::triggered, this, &CmakeProjectGenerator::actionTriggered, Qt::UniqueConnection); |
| 216 | menu->addAction(action); |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | if (!menu) { |
| 221 | menu = new QMenu(); |
| 222 | } |
| 223 | |
| 224 | QAction *action = new QAction(tr("Properties" )); |
| 225 | menu->addAction(action); |
| 226 | dpfservice::ProjectInfo info = dpfservice::ProjectInfo::get(item); |
| 227 | QObject::connect(action, &QAction::triggered, [=]() { |
| 228 | actionProperties(info, root); |
| 229 | }); |
| 230 | |
| 231 | return menu; |
| 232 | } |
| 233 | |
| 234 | void CmakeProjectGenerator::actionTriggered() |
| 235 | { |
| 236 | using namespace dpfservice; |
| 237 | QAction *action = qobject_cast<QAction *>(sender()); |
| 238 | if (action) { |
| 239 | QString program = action->property(CDT_CPROJECT_KEY::get()->buildCommand.toLatin1()).toString(); |
| 240 | QStringList args = action->property(CDT_CPROJECT_KEY::get()->buildArguments.toLatin1()).toString().split(" " ); |
| 241 | args << action->property(CDT_CPROJECT_KEY::get()->buildTarget.toLatin1()).toString(); |
| 242 | QString workDir = action->property("workDir" ).toString(); |
| 243 | |
| 244 | // remove extra quotes and empty argument. |
| 245 | QStringList argsFiltered; |
| 246 | for (auto &arg : args) { |
| 247 | if (!arg.isEmpty()) { |
| 248 | argsFiltered << arg.replace("\"" , "" ); |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | using namespace dpfservice; |
| 253 | auto &ctx = dpfInstance.serviceContext(); |
| 254 | BuilderService *builderService = ctx.service<BuilderService>(BuilderService::name()); |
| 255 | if (builderService) { |
| 256 | BuildCommandInfo commandInfo; |
| 257 | commandInfo.kitName = CmakeProjectGenerator::toolKitName(); |
| 258 | commandInfo.program = program; |
| 259 | commandInfo.arguments = args; |
| 260 | commandInfo.workingDir = workDir; |
| 261 | builderService->interface.builderCommand({commandInfo}, false); |
| 262 | } |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | void CmakeProjectGenerator::setRootItemToView(QStandardItem *root) |
| 267 | { |
| 268 | d->asynItemThreadPolls.remove(root); |
| 269 | |
| 270 | using namespace dpfservice; |
| 271 | auto &ctx = dpfInstance.serviceContext(); |
| 272 | ProjectService *projectService = ctx.service<ProjectService>(ProjectService::name()); |
| 273 | if (!projectService) |
| 274 | return; |
| 275 | |
| 276 | WindowService *windowService = ctx.service<WindowService>(WindowService::name()); |
| 277 | if (!windowService) |
| 278 | return; |
| 279 | |
| 280 | if (root) { |
| 281 | // setting item to view |
| 282 | if (projectService->projectView.addRootItem) |
| 283 | projectService->projectView.addRootItem(root); |
| 284 | |
| 285 | // expand view from tree two level |
| 286 | if (projectService->projectView.expandedDepth) |
| 287 | projectService->projectView.expandedDepth(root, 2); |
| 288 | |
| 289 | navigation.doSwitch(MWNA_EDIT); |
| 290 | editor.switchWorkspace(MWCWT_PROJECTS); |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | void CmakeProjectGenerator::doBuildCmdExecuteEnd(const BuildCommandInfo &info, int status) |
| 295 | { |
| 296 | // configure function cached info |
| 297 | if (d->configureProjectInfo.isEmpty()) |
| 298 | return; |
| 299 | |
| 300 | using namespace dpfservice; |
| 301 | auto &ctx = dpfInstance.serviceContext(); |
| 302 | ProjectService *projectService = ctx.service<ProjectService>(ProjectService::name()); |
| 303 | if (!projectService) |
| 304 | return; |
| 305 | |
| 306 | // get reload item from reload cmake file cache |
| 307 | mutex.lock(); |
| 308 | QStandardItem *reloadItem = nullptr; |
| 309 | for (auto val : d->reloadCmakeFileItems) { |
| 310 | if (info.workingDir == d->configureProjectInfo.workspaceFolder()) { |
| 311 | reloadItem = val; |
| 312 | break; |
| 313 | } |
| 314 | } |
| 315 | mutex.unlock(); |
| 316 | |
| 317 | if (reloadItem) { |
| 318 | d->reloadCmakeFileItems.removeOne(reloadItem); //clean cache |
| 319 | if (status == 0) { |
| 320 | projectService->projectView.removeRootItem(reloadItem); |
| 321 | createRootItem(d->configureProjectInfo); |
| 322 | } else { |
| 323 | qCritical() << "Failed execute cmd : " |
| 324 | << info.program |
| 325 | << info.arguments.join(" " ) |
| 326 | << "status : " << status; |
| 327 | } |
| 328 | } else { |
| 329 | createRootItem(d->configureProjectInfo); |
| 330 | } |
| 331 | emit projectService->projectConfigureDone(QString()); |
| 332 | } |
| 333 | |
| 334 | void CmakeProjectGenerator::doCmakeFileNodeChanged(QStandardItem *root, const QPair<QString, QStringList> &files) |
| 335 | { |
| 336 | Q_UNUSED(files) |
| 337 | |
| 338 | if (d->reloadCmakeFileItems.contains(root)) |
| 339 | return; |
| 340 | |
| 341 | qInfo() << __FUNCTION__; |
| 342 | using namespace dpfservice; |
| 343 | |
| 344 | // get current project info |
| 345 | auto proInfo = ProjectInfo::get(root); |
| 346 | |
| 347 | // cache the reload item |
| 348 | d->reloadCmakeFileItems.append(root); |
| 349 | |
| 350 | // reconfigure project info |
| 351 | configure(proInfo); |
| 352 | } |
| 353 | |
| 354 | void CmakeProjectGenerator::actionProperties(const dpfservice::ProjectInfo &info, QStandardItem *item) |
| 355 | { |
| 356 | PropertiesDialog dlg; |
| 357 | |
| 358 | BuildPropertyWidget *buildWidget = new BuildPropertyWidget(info); |
| 359 | RunPropertyWidget *runWidget = new RunPropertyWidget(info, item); |
| 360 | |
| 361 | dlg.insertPropertyPanel(tr("Build" ), buildWidget); |
| 362 | dlg.insertPropertyPanel(tr("Run" ), runWidget); |
| 363 | |
| 364 | dlg.exec(); |
| 365 | } |
| 366 | |
| 367 | void CmakeProjectGenerator::recursionRemoveItem(QStandardItem *item) |
| 368 | { |
| 369 | if (!item) |
| 370 | return; |
| 371 | |
| 372 | for (int row = 0; row < item->rowCount(); row++) { |
| 373 | auto child = item->takeChild(row); |
| 374 | if (!child->hasChildren()) { |
| 375 | delete child; |
| 376 | } else { |
| 377 | recursionRemoveItem(child); |
| 378 | } |
| 379 | } |
| 380 | |
| 381 | delete item; |
| 382 | return; |
| 383 | } |
| 384 | |