1 | // SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. |
2 | // |
3 | // SPDX-License-Identifier: GPL-3.0-or-later |
4 | |
5 | #include "projecttree.h" |
6 | #include "projectinfodialog.h" |
7 | #include "projectselectionmodel.h" |
8 | #include "projectdelegate.h" |
9 | #include "projectmodel.h" |
10 | #include "transceiver/sendevents.h" |
11 | |
12 | #include "services/project/projectservice.h" |
13 | |
14 | #include "common/common.h" |
15 | |
16 | #include <QDebug> |
17 | #include <QHeaderView> |
18 | #include <QContextMenuEvent> |
19 | #include <QPushButton> |
20 | #include <QDrag> |
21 | #include <QApplication> |
22 | #include <QUrl> |
23 | |
24 | const QString DELETE_MESSAGE_TEXT {QTreeView::tr("The delete operation will be removed from" |
25 | "the disk and will not be recoverable " |
26 | "after this operation.\nDelete anyway?" )}; |
27 | |
28 | using namespace dpfservice; |
29 | |
30 | class ProjectTreePrivate |
31 | { |
32 | friend class ProjectTree; |
33 | ProjectModel *itemModel {nullptr}; |
34 | ProjectSelectionModel *sectionModel {nullptr}; |
35 | ProjectDelegate *delegate {nullptr}; |
36 | QPoint startPos; |
37 | int itemDepth(const QStandardItem *item) |
38 | { |
39 | int depth = 0; |
40 | const QStandardItem *current = item; |
41 | while (current->parent()) { |
42 | current = current->parent(); |
43 | depth ++; |
44 | } |
45 | return depth; |
46 | } |
47 | }; |
48 | |
49 | ProjectTree::ProjectTree(QWidget *parent) |
50 | : QTreeView (parent) |
51 | , d(new ProjectTreePrivate) |
52 | { |
53 | setEditTriggers(QTreeView::NoEditTriggers); //节点不能编辑 |
54 | setSelectionBehavior(QTreeView::SelectRows); //一次选中整行 |
55 | setSelectionMode(QTreeView::SingleSelection); //单选,配合上面的整行就是一次选单行 |
56 | setFocusPolicy(Qt::NoFocus); //去掉鼠标移到节点上时的虚线框 |
57 | this->header()->hide(); |
58 | |
59 | d->itemModel = new ProjectModel(this); |
60 | setModel(d->itemModel); |
61 | |
62 | // 右键菜单创建 |
63 | QObject::connect(this, &ProjectTree::itemMenuRequest, |
64 | this, &ProjectTree::doItemMenuRequest); |
65 | |
66 | |
67 | // 双击操作 |
68 | QObject::connect(this, &ProjectTree::doubleClicked, |
69 | this, &ProjectTree::doDoubleClieked); |
70 | |
71 | d->sectionModel = new ProjectSelectionModel(d->itemModel); |
72 | setSelectionModel(d->sectionModel); |
73 | |
74 | d->delegate = new ProjectDelegate(this); |
75 | setItemDelegate(d->delegate); |
76 | this->setDragEnabled(true); |
77 | } |
78 | |
79 | ProjectTree::~ProjectTree() |
80 | { |
81 | if (d) { |
82 | delete d; |
83 | } |
84 | } |
85 | |
86 | void ProjectTree::activeProjectInfo(const ProjectInfo &info) |
87 | { |
88 | int rowCount = d->itemModel->rowCount(); |
89 | for (int currRow = 0; currRow < rowCount; currRow ++) { |
90 | auto currItem = d->itemModel->item(currRow, 0); |
91 | if (currItem) { |
92 | auto currInfo = ProjectInfo::get(ProjectGenerator::root(currItem)); |
93 | if (currInfo.language() == info.language() |
94 | && currInfo.workspaceFolder() == info.workspaceFolder() |
95 | && currInfo.kitName() == info.kitName()) { |
96 | doActiveProject(currItem); |
97 | } |
98 | } |
99 | } |
100 | } |
101 | |
102 | void ProjectTree::activeProjectInfo(const QString &kitName, |
103 | const QString &language, |
104 | const QString &workspace) |
105 | { |
106 | int rowCount = d->itemModel->rowCount(); |
107 | for (int currRow = 0; currRow < rowCount; currRow ++) { |
108 | auto currItem = d->itemModel->item(currRow, 0); |
109 | if (currItem) { |
110 | auto currInfo = ProjectInfo::get(ProjectGenerator::root(currItem)); |
111 | if (currInfo.language() == language |
112 | && currInfo.workspaceFolder() == workspace |
113 | && currInfo.kitName() == kitName) { |
114 | doActiveProject(currItem); |
115 | } |
116 | } |
117 | } |
118 | } |
119 | |
120 | void ProjectTree::appendRootItem(QStandardItem *root) |
121 | { |
122 | if (!root) |
123 | return; |
124 | |
125 | // 发送工程创建信号 |
126 | using namespace dpfservice; |
127 | auto info = ProjectInfo::get(ProjectGenerator::root(root)); |
128 | |
129 | // 添加工程节点 |
130 | QStandardItemModel *model = static_cast<QStandardItemModel*>(QTreeView::model()); |
131 | if (model) |
132 | model->appendRow(root); |
133 | |
134 | // 发送工程节点已创建信号 |
135 | SendEvents::projectCreated(info); |
136 | |
137 | // 激活当前工程节点 |
138 | doActiveProject(root); |
139 | } |
140 | |
141 | void ProjectTree::removeRootItem(QStandardItem *root) |
142 | { |
143 | this->viewport()->setUpdatesEnabled(false); |
144 | |
145 | ProjectInfo info = ProjectInfo::get(ProjectGenerator::root(root)); |
146 | |
147 | // 从Model中移除 |
148 | this->takeRootItem(root); |
149 | |
150 | // 从生成器中删除 |
151 | using namespace dpfservice; |
152 | auto &ctx = dpfInstance.serviceContext(); |
153 | ProjectService *projectService = ctx.service<ProjectService>(ProjectService::name()); |
154 | if (!projectService) |
155 | return; |
156 | |
157 | auto generator = projectService->createGenerator<ProjectGenerator>(info.kitName()); |
158 | if (generator) |
159 | generator->removeRootItem(root); |
160 | |
161 | // 发送工程删除信号 |
162 | SendEvents::projectDeleted(info); |
163 | |
164 | // 始终保持首选项 |
165 | int rowCount = d->itemModel->rowCount(); |
166 | if ( 0 < rowCount) { // 存在其他工程时 |
167 | auto index = d->itemModel->index(0, 0); |
168 | doActiveProject(d->itemModel->itemFromIndex(index)); |
169 | } |
170 | |
171 | this->viewport()->setUpdatesEnabled(true); |
172 | } |
173 | |
174 | void ProjectTree::takeRootItem(QStandardItem *root) |
175 | { |
176 | // 从展示的模型中删除 |
177 | QModelIndex index = d->itemModel->indexFromItem(root); |
178 | d->itemModel->takeRow(index.row()); |
179 | } |
180 | |
181 | void ProjectTree::doItemMenuRequest(QStandardItem *item, QContextMenuEvent *event) |
182 | { |
183 | auto rootItem = ProjectGenerator::root(item); |
184 | QMenu * = nullptr; |
185 | |
186 | if (rootItem == item) { |
187 | menu = rootMenu(rootItem); |
188 | } else { |
189 | menu = childMenu(rootItem, item); |
190 | } |
191 | |
192 | if (menu) { |
193 | menu->move(event->globalPos()); |
194 | menu->exec(); |
195 | delete menu; |
196 | } |
197 | } |
198 | |
199 | void ProjectTree::expandedProjectDepth(const QStandardItem *root, int depth) |
200 | { |
201 | if (!root) |
202 | return; |
203 | |
204 | if (d->itemDepth(root) < depth) { //满足深度 |
205 | expand(d->itemModel->indexFromItem(root)); |
206 | for(int i = 0; i < root->rowCount(); i++) { |
207 | QStandardItem * childitem = root->child(i); |
208 | if (root->hasChildren()) { |
209 | expandedProjectDepth(childitem, depth); |
210 | } |
211 | } |
212 | } |
213 | } |
214 | |
215 | void ProjectTree::expandedProjectAll(const QStandardItem *root) |
216 | { |
217 | if (!root) |
218 | return; |
219 | |
220 | expand(d->itemModel->indexFromItem(root)); |
221 | if (root->hasChildren()) { |
222 | for(int i = 0; i < root->rowCount(); i++) { |
223 | QStandardItem * childitem = root->child(i); |
224 | expandedProjectAll(childitem); |
225 | } |
226 | } |
227 | } |
228 | |
229 | QList<dpfservice::ProjectInfo> ProjectTree::getAllProjectInfo() |
230 | { |
231 | using namespace dpfservice; |
232 | QList<ProjectInfo> result; |
233 | for (int row = 0; row < d->itemModel->rowCount(); row++) { |
234 | result << ProjectInfo::get(d->itemModel->index(row, 0)); |
235 | } |
236 | return result; |
237 | } |
238 | |
239 | ProjectInfo ProjectTree::getProjectInfo(const QString &kitName, const QString &workspace) |
240 | { |
241 | ProjectInfo projectInfo; |
242 | for (int row = 0; row < d->itemModel->rowCount(); row++) { |
243 | ProjectInfo info = ProjectInfo::get(d->itemModel->index(row, 0)); |
244 | if (kitName == info.kitName() && workspace == info.workspaceFolder()) { |
245 | projectInfo = info; |
246 | break; |
247 | } |
248 | } |
249 | return projectInfo; |
250 | } |
251 | |
252 | ProjectInfo ProjectTree::getActiveProjectInfo() const |
253 | { |
254 | ProjectInfo projectInfo; |
255 | auto activeProject = d->delegate->getActiveProject(); |
256 | if(activeProject.isValid()) { |
257 | projectInfo = ProjectInfo::get(activeProject); |
258 | } |
259 | return projectInfo; |
260 | } |
261 | |
262 | void ProjectTree::(QContextMenuEvent *event) |
263 | { |
264 | QTreeView::contextMenuEvent(event); |
265 | QModelIndex index = indexAt(event->pos()); |
266 | selectionModel()->select(index, QItemSelectionModel::SelectCurrent); |
267 | indexMenuRequest(index, event); |
268 | itemMenuRequest(d->itemModel->itemFromIndex(index), event); |
269 | } |
270 | |
271 | void ProjectTree::mousePressEvent(QMouseEvent *event) |
272 | { |
273 | if (event->button() == Qt::LeftButton) |
274 | d->startPos = event->pos(); |
275 | QTreeView::mousePressEvent(event); |
276 | } |
277 | |
278 | void ProjectTree::mouseMoveEvent(QMouseEvent *event) |
279 | { |
280 | if (event->buttons() & Qt::LeftButton) { |
281 | int distance = (event->pos() - d->startPos).manhattanLength(); |
282 | if (distance >= QApplication::startDragDistance()) |
283 | performDrag(); |
284 | } |
285 | QTreeView::mouseMoveEvent(event); |
286 | } |
287 | |
288 | QMenu *ProjectTree::childMenu(const QStandardItem *root, const QStandardItem *child) |
289 | { |
290 | QMenu * = nullptr; |
291 | QString toolKitName = ProjectInfo::get(root).kitName(); |
292 | // 获取支持右键菜单生成器 |
293 | auto &ctx = dpfInstance.serviceContext(); |
294 | ProjectService *projectService = ctx.service<ProjectService>(ProjectService::name()); |
295 | if (projectService->supportGeneratorName<ProjectGenerator>().contains(toolKitName)) { |
296 | menu = projectService->createGenerator<ProjectGenerator>(toolKitName)->createItemMenu(child); |
297 | } |
298 | if (!menu) |
299 | menu = new QMenu(); |
300 | |
301 | |
302 | QAction *newDocAction = new QAction(tr("New Document" )); |
303 | menu->addAction(newDocAction); |
304 | QObject::connect(newDocAction, &QAction::triggered, this, [=](){ |
305 | actionNewDocument(child); |
306 | }); |
307 | |
308 | bool isDir = false; |
309 | QModelIndex index = d->itemModel->indexFromItem(child); |
310 | QFileInfo info(index.data(Qt::ToolTipRole).toString()); |
311 | if (info.isDir()) { |
312 | isDir = true; |
313 | } |
314 | |
315 | QAction *deleteDocAction = new QAction(tr("Delete Document" )); |
316 | menu->addAction(deleteDocAction); |
317 | QObject::connect(deleteDocAction, &QAction::triggered, this, [=](){ |
318 | actionDeleteDocument(child); |
319 | }); |
320 | if (isDir) { |
321 | deleteDocAction->setEnabled(false); |
322 | } |
323 | return menu; |
324 | } |
325 | |
326 | QMenu *ProjectTree::rootMenu(QStandardItem *root) |
327 | { |
328 | QMenu * = nullptr; |
329 | QString toolKitName = ProjectInfo::get(root).kitName(); |
330 | // 获取支持右键菜单生成器 |
331 | auto &ctx = dpfInstance.serviceContext(); |
332 | ProjectService *projectService = ctx.service<ProjectService>(ProjectService::name()); |
333 | if (projectService->supportGeneratorName<ProjectGenerator>().contains(toolKitName)) { |
334 | menu = projectService->createGenerator<ProjectGenerator>(toolKitName)->createItemMenu(root); |
335 | } |
336 | if (!menu) |
337 | menu = new QMenu(); |
338 | |
339 | QAction* activeProjectAction = new QAction(QAction::tr("Project Active" ), menu); |
340 | QAction* closeAction = new QAction(QAction::tr("Project Close" ), menu); |
341 | QAction* propertyAction = new QAction(QAction::tr("Project Info" ), menu); |
342 | QObject::connect(activeProjectAction, &QAction::triggered, activeProjectAction, [=](){doActiveProject(root);}); |
343 | QObject::connect(closeAction, &QAction::triggered, closeAction, [=](){doCloseProject(root);}); |
344 | QObject::connect(propertyAction, &QAction::triggered, propertyAction, [=](){doShowProjectInfo(root);}); |
345 | menu->insertAction(nullptr, activeProjectAction); |
346 | menu->insertAction(nullptr, closeAction); |
347 | menu->insertAction(nullptr, propertyAction); |
348 | return menu; |
349 | } |
350 | |
351 | void ProjectTree::performDrag() |
352 | { |
353 | QModelIndex index = currentIndex(); |
354 | QStandardItem *item = d->itemModel->itemFromIndex(index); |
355 | if (item) { |
356 | QMimeData *mimeData = new QMimeData; |
357 | QList<QUrl> urls; |
358 | QString filePath = "file:" + index.data(Qt::ToolTipRole).toString(); |
359 | urls.append(QUrl(filePath)); |
360 | mimeData->setUrls(urls); |
361 | |
362 | QDrag *drag = new QDrag(this); |
363 | drag->setMimeData(mimeData); |
364 | drag->exec(); |
365 | } |
366 | } |
367 | |
368 | void ProjectTree::itemModified(QStandardItem *item, const QList<QStandardItem *> &childs) |
369 | { |
370 | setUpdatesEnabled(false); |
371 | auto parentIndex = d->itemModel->indexFromItem(item); |
372 | int childCount = d->itemModel->rowCount(parentIndex); |
373 | d->itemModel->removeRows(0, childCount, parentIndex); |
374 | item->appendRows(childs); |
375 | setUpdatesEnabled(true); |
376 | } |
377 | |
378 | void ProjectTree::doDoubleClieked(const QModelIndex &index) |
379 | { |
380 | QFileInfo info(index.data(Qt::ToolTipRole).toString()); |
381 | if (info.exists() && info.isFile()) { |
382 | QString workspaceFolder, language; |
383 | QModelIndex rootIndex = ProjectGenerator::root(index); |
384 | if (rootIndex.isValid()) { |
385 | auto info = ProjectInfo::get(rootIndex); |
386 | workspaceFolder = info.workspaceFolder(); |
387 | language = info.language(); |
388 | } |
389 | editor.openFileWithKey(workspaceFolder, language, info.filePath()); |
390 | } |
391 | } |
392 | |
393 | void ProjectTree::doCloseProject(QStandardItem *root) |
394 | { |
395 | if (!root && root != ProjectGenerator::root(root)) |
396 | return; |
397 | auto info = ProjectInfo::get(root); |
398 | this->removeRootItem(root); |
399 | } |
400 | |
401 | void ProjectTree::doActiveProject(QStandardItem *root) |
402 | { |
403 | if (!root && root != ProjectGenerator::root(root)) |
404 | return; |
405 | d->delegate->setActiveProject(d->itemModel->indexFromItem(root)); |
406 | SendEvents::projectActived(ProjectInfo::get(root)); |
407 | } |
408 | |
409 | |
410 | void ProjectTree::actionNewDocument(const QStandardItem *item) |
411 | { |
412 | QDialog *dlg = new QDialog; |
413 | QLineEdit *edit = new QLineEdit; |
414 | |
415 | edit->setAlignment(Qt::AlignLeft); |
416 | dlg->setAttribute(Qt::WA_DeleteOnClose); |
417 | dlg->setWindowTitle(tr("New Document" )); |
418 | dlg->resize(400, 100); |
419 | |
420 | QVBoxLayout *vLayout = new QVBoxLayout(dlg); |
421 | QPushButton *pbtOk = new QPushButton(tr("ok" )); |
422 | pbtOk->setFixedSize(40, 20); |
423 | vLayout->addWidget(edit); |
424 | vLayout->addWidget(pbtOk, 0, Qt::AlignCenter); |
425 | |
426 | QObject::connect(pbtOk, &QPushButton::clicked, dlg, [=](){ |
427 | creatNewDocument(item, edit->text()); |
428 | dlg->close(); |
429 | }); |
430 | |
431 | dlg->exec(); |
432 | } |
433 | |
434 | void ProjectTree::actionDeleteDocument(const QStandardItem *item) |
435 | { |
436 | QModelIndex index = d->itemModel->indexFromItem(item); |
437 | QFileInfo info(index.data(Qt::ToolTipRole).toString()); |
438 | if (!info.isFile()) |
439 | return; |
440 | |
441 | bool doDelete = false; |
442 | auto okCallBack = [&](bool checked) { |
443 | Q_UNUSED(checked); |
444 | doDelete = true; |
445 | }; |
446 | |
447 | QString mess = DELETE_MESSAGE_TEXT + "\n" + info.filePath(); |
448 | ContextDialog::okCancel(mess, |
449 | DELETE_MESSAGE_TEXT, |
450 | QMessageBox::Warning, |
451 | okCallBack, |
452 | nullptr); |
453 | |
454 | if (!doDelete) |
455 | return; |
456 | QFile(info.filePath()).remove(); |
457 | } |
458 | |
459 | void ProjectTree::creatNewDocument(const QStandardItem *item, const QString &fileName) |
460 | { |
461 | QModelIndex index = d->itemModel->indexFromItem(item); |
462 | QFileInfo info(index.data(Qt::ToolTipRole).toString()); |
463 | QString workspace, language; |
464 | QModelIndex rootIndex = ProjectGenerator::root(index); |
465 | if (rootIndex.isValid()) { |
466 | auto rootInfo = ProjectInfo::get(rootIndex); |
467 | workspace = rootInfo.workspaceFolder(); |
468 | language = rootInfo.language(); |
469 | } |
470 | |
471 | QString filePath; |
472 | if (info.isDir()) { |
473 | filePath = info.filePath() + QDir::separator() + fileName; |
474 | } else if (info.isFile()) { |
475 | filePath = info.path() + QDir::separator() + fileName; |
476 | } |
477 | |
478 | if (QFile::exists(filePath)) { |
479 | bool doOverWrite = false; |
480 | auto okCallBack = [&](bool checked) { |
481 | Q_UNUSED(checked); |
482 | doOverWrite = true; |
483 | }; |
484 | |
485 | QString mess = "A file with name " + fileName + " already exists. Would you like to overwrite it?" ; |
486 | ContextDialog::okCancel(mess, |
487 | DELETE_MESSAGE_TEXT, |
488 | QMessageBox::Warning, |
489 | okCallBack, |
490 | nullptr); |
491 | if (doOverWrite) { |
492 | QFile::remove(filePath); |
493 | } |
494 | } |
495 | |
496 | QFile file(filePath); |
497 | if (file.open(QFile::OpenModeFlag::NewOnly)) { |
498 | file.close(); |
499 | } |
500 | editor.openFileWithKey(workspace, language, filePath); |
501 | } |
502 | |
503 | void ProjectTree::doShowProjectInfo(QStandardItem *root) |
504 | { |
505 | if (!root && root != ProjectGenerator::root(root)) |
506 | return; |
507 | |
508 | ProjectInfoDialog dialog; |
509 | QString propertyText = "Language: " + ProjectInfo::get(root).language() + "\n" |
510 | + "KitName: " + ProjectInfo::get(root).kitName() + "\n" |
511 | + "BuildFolder: " + ProjectInfo::get(root).buildFolder() + "\n" |
512 | + "WorkspaceFolder: " + ProjectInfo::get(root).workspaceFolder() + "\n" |
513 | + "BuildType: " + ProjectInfo::get(root).buildType() + "\n" |
514 | + "BuildProgram: " + "\n " + ProjectInfo::get(root).buildProgram() + "\n" |
515 | + "ConfigCustomArgs: " + "\n " + ProjectInfo::get(root).configCustomArgs().join(" " ) + "\n" |
516 | + "BuildCustomArgs: " + "\n " + ProjectInfo::get(root).buildCustomArgs().join(" " ) + "\n" |
517 | + "CleanCustomArgs: " + "\n " + ProjectInfo::get(root).cleanCustomArgs().join(" " ); |
518 | dialog.setPropertyText(propertyText); |
519 | dialog.exec(); |
520 | } |
521 | |