The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

885 lines
31KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. #pragma once
  19. //==============================================================================
  20. class FileTreeItemBase : public JucerTreeViewBase,
  21. private ValueTree::Listener
  22. {
  23. public:
  24. FileTreeItemBase (const Project::Item& projectItem)
  25. : item (projectItem), isFileMissing (false)
  26. {
  27. item.state.addListener (this);
  28. }
  29. ~FileTreeItemBase() override
  30. {
  31. item.state.removeListener (this);
  32. }
  33. //==============================================================================
  34. virtual bool acceptsFileDrop (const StringArray& files) const = 0;
  35. virtual bool acceptsDragItems (const OwnedArray<Project::Item>& selectedNodes) = 0;
  36. //==============================================================================
  37. String getDisplayName() const override { return item.getName(); }
  38. String getRenamingName() const override { return getDisplayName(); }
  39. void setName (const String& newName) override { item.getNameValue() = newName; }
  40. bool isMissing() const override { return isFileMissing; }
  41. virtual File getFile() const { return item.getFile(); }
  42. void deleteItem() override { item.removeItemFromProject(); }
  43. void deleteAllSelectedItems() override
  44. {
  45. auto* tree = getOwnerView();
  46. Array<File> filesToTrash;
  47. Array<Project::Item> itemsToRemove;
  48. for (int i = 0; i < tree->getNumSelectedItems(); ++i)
  49. {
  50. if (auto* p = dynamic_cast<FileTreeItemBase*> (tree->getSelectedItem (i)))
  51. {
  52. itemsToRemove.add (p->item);
  53. if (p->item.isGroup())
  54. {
  55. for (int j = 0; j < p->item.getNumChildren(); ++j)
  56. {
  57. auto associatedFile = p->item.getChild (j).getFile();
  58. if (associatedFile.existsAsFile())
  59. filesToTrash.addIfNotAlreadyThere (associatedFile);
  60. }
  61. }
  62. else if (p->getFile().existsAsFile())
  63. {
  64. filesToTrash.addIfNotAlreadyThere (p->getFile());
  65. }
  66. }
  67. }
  68. WeakReference<FileTreeItemBase> treeRootItem { dynamic_cast<FileTreeItemBase*> (tree->getRootItem()) };
  69. if (treeRootItem == nullptr)
  70. {
  71. jassertfalse;
  72. return;
  73. }
  74. auto doDelete = [treeRootItem, itemsToRemove] (const Array<File>& fsToTrash)
  75. {
  76. if (treeRootItem == nullptr)
  77. return;
  78. auto& om = ProjucerApplication::getApp().openDocumentManager;
  79. for (auto i = fsToTrash.size(); --i >= 0;)
  80. {
  81. auto f = fsToTrash.getUnchecked (i);
  82. om.closeFileWithoutSaving (f);
  83. if (! f.moveToTrash())
  84. {
  85. // xxx
  86. }
  87. }
  88. for (auto i = itemsToRemove.size(); --i >= 0;)
  89. {
  90. if (auto itemToRemove = treeRootItem->findTreeViewItem (itemsToRemove.getUnchecked (i)))
  91. {
  92. if (auto* pcc = treeRootItem->getProjectContentComponent())
  93. {
  94. if (auto* fileInfoComp = dynamic_cast<FileGroupInformationComponent*> (pcc->getEditorComponent()))
  95. if (fileInfoComp->getGroupPath() == itemToRemove->getFile().getFullPathName())
  96. pcc->hideEditor();
  97. }
  98. om.closeFileWithoutSaving (itemToRemove->getFile());
  99. itemToRemove->deleteItem();
  100. }
  101. }
  102. };
  103. if (! filesToTrash.isEmpty())
  104. {
  105. String fileList;
  106. auto maxFilesToList = 10;
  107. for (auto i = jmin (maxFilesToList, filesToTrash.size()); --i >= 0;)
  108. fileList << filesToTrash.getUnchecked (i).getFullPathName() << "\n";
  109. if (filesToTrash.size() > maxFilesToList)
  110. fileList << "\n...plus " << (filesToTrash.size() - maxFilesToList) << " more files...";
  111. auto options = MessageBoxOptions::makeOptionsYesNoCancel (MessageBoxIconType::NoIcon,
  112. "Delete Project Items",
  113. "As well as removing the selected item(s) from the project, do you also want to move their files to the trash:\n\n" + fileList,
  114. "Just remove references",
  115. "Also move files to Trash",
  116. "Cancel",
  117. tree->getTopLevelComponent());
  118. messageBox = AlertWindow::showScopedAsync (options, [treeRootItem, filesToTrash, doDelete] (int r) mutable
  119. {
  120. if (treeRootItem == nullptr)
  121. return;
  122. if (r == 0)
  123. return;
  124. if (r != 2)
  125. filesToTrash.clear();
  126. doDelete (filesToTrash);
  127. });
  128. return;
  129. }
  130. doDelete (filesToTrash);
  131. }
  132. virtual void revealInFinder() const
  133. {
  134. getFile().revealToUser();
  135. }
  136. virtual void browseToAddExistingFiles()
  137. {
  138. auto location = item.isGroup() ? item.determineGroupFolder() : getFile();
  139. chooser = std::make_unique<FileChooser> ("Add Files to Jucer Project", location, "");
  140. auto flags = FileBrowserComponent::openMode
  141. | FileBrowserComponent::canSelectFiles
  142. | FileBrowserComponent::canSelectDirectories
  143. | FileBrowserComponent::canSelectMultipleItems;
  144. chooser->launchAsync (flags, [this] (const FileChooser& fc)
  145. {
  146. if (fc.getResults().isEmpty())
  147. return;
  148. StringArray files;
  149. for (int i = 0; i < fc.getResults().size(); ++i)
  150. files.add (fc.getResults().getReference (i).getFullPathName());
  151. addFilesRetainingSortOrder (files);
  152. });
  153. }
  154. virtual void checkFileStatus() // (recursive)
  155. {
  156. auto file = getFile();
  157. auto nowMissing = (file != File() && ! file.exists());
  158. if (nowMissing != isFileMissing)
  159. {
  160. isFileMissing = nowMissing;
  161. repaintItem();
  162. }
  163. }
  164. virtual void addFilesAtIndex (const StringArray& files, int insertIndex)
  165. {
  166. if (auto* p = getParentProjectItem())
  167. p->addFilesAtIndex (files, insertIndex);
  168. }
  169. virtual void addFilesRetainingSortOrder (const StringArray& files)
  170. {
  171. if (auto* p = getParentProjectItem())
  172. p->addFilesRetainingSortOrder (files);
  173. }
  174. virtual void moveSelectedItemsTo (OwnedArray<Project::Item>&, int /*insertIndex*/)
  175. {
  176. jassertfalse;
  177. }
  178. void showMultiSelectionPopupMenu (Point<int> p) override
  179. {
  180. PopupMenu m;
  181. m.addItem (1, "Delete");
  182. m.showMenuAsync (PopupMenu::Options().withTargetScreenArea ({ p.x, p.y, 1, 1 }),
  183. ModalCallbackFunction::create (treeViewMultiSelectItemChosen, this));
  184. }
  185. static void treeViewMultiSelectItemChosen (int resultCode, FileTreeItemBase* item)
  186. {
  187. switch (resultCode)
  188. {
  189. case 1: item->deleteAllSelectedItems(); break;
  190. default: break;
  191. }
  192. }
  193. virtual FileTreeItemBase* findTreeViewItem (const Project::Item& itemToFind)
  194. {
  195. if (item == itemToFind)
  196. return this;
  197. auto wasOpen = isOpen();
  198. setOpen (true);
  199. for (auto i = getNumSubItems(); --i >= 0;)
  200. {
  201. if (auto* pg = dynamic_cast<FileTreeItemBase*> (getSubItem (i)))
  202. if (auto* found = pg->findTreeViewItem (itemToFind))
  203. return found;
  204. }
  205. setOpen (wasOpen);
  206. return nullptr;
  207. }
  208. //==============================================================================
  209. bool mightContainSubItems() override { return item.getNumChildren() > 0; }
  210. String getUniqueName() const override { jassert (item.getID().isNotEmpty()); return item.getID(); }
  211. bool canBeSelected() const override { return true; }
  212. String getTooltip() override { return {}; }
  213. File getDraggableFile() const override { return getFile(); }
  214. var getDragSourceDescription() override
  215. {
  216. cancelDelayedSelectionTimer();
  217. return projectItemDragType;
  218. }
  219. void addSubItems() override
  220. {
  221. for (int i = 0; i < item.getNumChildren(); ++i)
  222. if (auto* p = createSubItem (item.getChild (i)))
  223. addSubItem (p);
  224. }
  225. void itemOpennessChanged (bool isNowOpen) override
  226. {
  227. if (isNowOpen)
  228. refreshSubItems();
  229. }
  230. //==============================================================================
  231. bool isInterestedInFileDrag (const StringArray& files) override
  232. {
  233. return acceptsFileDrop (files);
  234. }
  235. void filesDropped (const StringArray& files, int insertIndex) override
  236. {
  237. if (files.size() == 1 && File (files[0]).hasFileExtension (Project::projectFileExtension))
  238. ProjucerApplication::getApp().openFile (files[0], [] (bool) {});
  239. else
  240. addFilesAtIndex (files, insertIndex);
  241. }
  242. bool isInterestedInDragSource (const DragAndDropTarget::SourceDetails& dragSourceDetails) override
  243. {
  244. OwnedArray<Project::Item> selectedNodes;
  245. getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes);
  246. return selectedNodes.size() > 0 && acceptsDragItems (selectedNodes);
  247. }
  248. void itemDropped (const DragAndDropTarget::SourceDetails& dragSourceDetails, int insertIndex) override
  249. {
  250. OwnedArray<Project::Item> selectedNodes;
  251. getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes);
  252. if (selectedNodes.size() > 0)
  253. {
  254. auto* tree = getOwnerView();
  255. std::unique_ptr<XmlElement> oldOpenness (tree->getOpennessState (false));
  256. moveSelectedItemsTo (selectedNodes, insertIndex);
  257. if (oldOpenness != nullptr)
  258. tree->restoreOpennessState (*oldOpenness, false);
  259. }
  260. }
  261. int getMillisecsAllowedForDragGesture() override
  262. {
  263. // for images, give the user longer to start dragging before assuming they're
  264. // clicking to select it for previewing..
  265. return item.isImageFile() ? 250 : JucerTreeViewBase::getMillisecsAllowedForDragGesture();
  266. }
  267. static void getSelectedProjectItemsBeingDragged (const DragAndDropTarget::SourceDetails& dragSourceDetails,
  268. OwnedArray<Project::Item>& selectedNodes)
  269. {
  270. if (dragSourceDetails.description == projectItemDragType)
  271. {
  272. auto* tree = dynamic_cast<TreeView*> (dragSourceDetails.sourceComponent.get());
  273. if (tree == nullptr)
  274. tree = dragSourceDetails.sourceComponent->findParentComponentOfClass<TreeView>();
  275. if (tree != nullptr)
  276. {
  277. auto numSelected = tree->getNumSelectedItems();
  278. for (int i = 0; i < numSelected; ++i)
  279. if (auto* p = dynamic_cast<FileTreeItemBase*> (tree->getSelectedItem (i)))
  280. selectedNodes.add (new Project::Item (p->item));
  281. }
  282. }
  283. }
  284. FileTreeItemBase* getParentProjectItem() const
  285. {
  286. return dynamic_cast<FileTreeItemBase*> (getParentItem());
  287. }
  288. //==============================================================================
  289. Project::Item item;
  290. protected:
  291. //==============================================================================
  292. void valueTreePropertyChanged (ValueTree& tree, const Identifier&) override
  293. {
  294. if (tree == item.state)
  295. repaintItem();
  296. }
  297. void valueTreeChildAdded (ValueTree& parentTree, ValueTree&) override { treeChildrenChanged (parentTree); }
  298. void valueTreeChildRemoved (ValueTree& parentTree, ValueTree&, int) override { treeChildrenChanged (parentTree); }
  299. void valueTreeChildOrderChanged (ValueTree& parentTree, int, int) override { treeChildrenChanged (parentTree); }
  300. bool isFileMissing;
  301. virtual FileTreeItemBase* createSubItem (const Project::Item& node) = 0;
  302. Icon getIcon() const override
  303. {
  304. auto colour = getOwnerView()->findColour (isSelected() ? defaultHighlightedTextColourId
  305. : treeIconColourId);
  306. return item.getIcon (isOpen()).withColour (colour);
  307. }
  308. bool isIconCrossedOut() const override { return item.isIconCrossedOut(); }
  309. void treeChildrenChanged (const ValueTree& parentTree)
  310. {
  311. if (parentTree == item.state)
  312. {
  313. refreshSubItems();
  314. treeHasChanged();
  315. setOpen (true);
  316. }
  317. }
  318. void triggerAsyncRename (const Project::Item& itemToRename)
  319. {
  320. struct RenameMessage final : public CallbackMessage
  321. {
  322. RenameMessage (TreeView* const t, const Project::Item& i)
  323. : tree (t), itemToRename (i) {}
  324. void messageCallback() override
  325. {
  326. if (tree != nullptr)
  327. if (auto* root = dynamic_cast<FileTreeItemBase*> (tree->getRootItem()))
  328. if (auto* found = root->findTreeViewItem (itemToRename))
  329. found->showRenameBox();
  330. }
  331. private:
  332. Component::SafePointer<TreeView> tree;
  333. Project::Item itemToRename;
  334. };
  335. (new RenameMessage (getOwnerView(), itemToRename))->post();
  336. }
  337. static void moveItems (OwnedArray<Project::Item>& selectedNodes, Project::Item destNode, int insertIndex)
  338. {
  339. for (auto i = selectedNodes.size(); --i >= 0;)
  340. {
  341. auto* n = selectedNodes.getUnchecked (i);
  342. if (destNode == *n || destNode.state.isAChildOf (n->state)) // Check for recursion.
  343. return;
  344. if (! destNode.canContain (*n))
  345. selectedNodes.remove (i);
  346. }
  347. // Don't include any nodes that are children of other selected nodes..
  348. for (auto i = selectedNodes.size(); --i >= 0;)
  349. {
  350. auto* n = selectedNodes.getUnchecked (i);
  351. for (auto j = selectedNodes.size(); --j >= 0;)
  352. {
  353. if (j != i && n->state.isAChildOf (selectedNodes.getUnchecked (j)->state))
  354. {
  355. selectedNodes.remove (i);
  356. break;
  357. }
  358. }
  359. }
  360. // Remove and re-insert them one at a time..
  361. for (int i = 0; i < selectedNodes.size(); ++i)
  362. {
  363. auto* selectedNode = selectedNodes.getUnchecked (i);
  364. if (selectedNode->state.getParent() == destNode.state
  365. && indexOfNode (destNode.state, selectedNode->state) < insertIndex)
  366. --insertIndex;
  367. selectedNode->removeItemFromProject();
  368. destNode.addChild (*selectedNode, insertIndex++);
  369. }
  370. }
  371. static int indexOfNode (const ValueTree& parent, const ValueTree& child)
  372. {
  373. for (auto i = parent.getNumChildren(); --i >= 0;)
  374. if (parent.getChild (i) == child)
  375. return i;
  376. return -1;
  377. }
  378. ScopedMessageBox messageBox;
  379. private:
  380. std::unique_ptr<FileChooser> chooser;
  381. JUCE_DECLARE_WEAK_REFERENCEABLE (FileTreeItemBase)
  382. };
  383. //==============================================================================
  384. class SourceFileItem final : public FileTreeItemBase
  385. {
  386. public:
  387. SourceFileItem (const Project::Item& projectItem)
  388. : FileTreeItemBase (projectItem)
  389. {
  390. }
  391. bool acceptsFileDrop (const StringArray&) const override { return false; }
  392. bool acceptsDragItems (const OwnedArray<Project::Item>&) override { return false; }
  393. String getDisplayName() const override
  394. {
  395. return getFile().getFileName();
  396. }
  397. void paintItem (Graphics& g, int width, int height) override
  398. {
  399. JucerTreeViewBase::paintItem (g, width, height);
  400. if (item.needsSaving())
  401. {
  402. auto bounds = g.getClipBounds().withY (0).withHeight (height);
  403. g.setFont (getFont());
  404. g.setColour (getContentColour (false));
  405. g.drawFittedText ("*", bounds.removeFromLeft (height), Justification::centred, 1);
  406. }
  407. }
  408. static File findCorrespondingHeaderOrCpp (const File& f)
  409. {
  410. if (f.hasFileExtension (sourceFileExtensions)) return f.withFileExtension (".h");
  411. if (f.hasFileExtension (headerFileExtensions)) return f.withFileExtension (".cpp");
  412. return {};
  413. }
  414. void setName (const String& newName) override
  415. {
  416. if (newName != File::createLegalFileName (newName))
  417. {
  418. auto options = MessageBoxOptions::makeOptionsOk (MessageBoxIconType::WarningIcon,
  419. "File Rename",
  420. "That filename contained some illegal characters!");
  421. messageBox = AlertWindow::showScopedAsync (options, [this, item = item] (int)
  422. {
  423. triggerAsyncRename (item);
  424. });
  425. return;
  426. }
  427. auto oldFile = getFile();
  428. auto newFile = oldFile.getSiblingFile (newName);
  429. auto correspondingFile = findCorrespondingHeaderOrCpp (oldFile);
  430. if (correspondingFile.exists() && newFile.hasFileExtension (oldFile.getFileExtension()))
  431. {
  432. auto correspondingItem = item.project.getMainGroup().findItemForFile (correspondingFile);
  433. if (correspondingItem.isValid())
  434. {
  435. auto options = MessageBoxOptions::makeOptionsOkCancel (MessageBoxIconType::NoIcon,
  436. "File Rename",
  437. "Do you also want to rename the corresponding file \"" + correspondingFile.getFileName() + "\" to match?");
  438. messageBox = AlertWindow::showScopedAsync (options, [parent = WeakReference { this }, oldFile, newFile, correspondingFile, correspondingItem] (int result) mutable
  439. {
  440. if (parent == nullptr || result == 0)
  441. return;
  442. if (! parent->item.renameFile (newFile))
  443. {
  444. auto opts = MessageBoxOptions::makeOptionsOk (MessageBoxIconType::WarningIcon,
  445. "File Rename",
  446. "Failed to rename \"" + oldFile.getFullPathName() + "\"!\n\nCheck your file permissions!");
  447. parent->messageBox = AlertWindow::showScopedAsync (opts, nullptr);
  448. return;
  449. }
  450. if (! correspondingItem.renameFile (newFile.withFileExtension (correspondingFile.getFileExtension())))
  451. {
  452. auto opts = MessageBoxOptions::makeOptionsOk (MessageBoxIconType::WarningIcon,
  453. "File Rename",
  454. "Failed to rename \"" + correspondingFile.getFullPathName() + "\"!\n\nCheck your file permissions!");
  455. parent->messageBox = AlertWindow::showScopedAsync (opts, nullptr);
  456. }
  457. });
  458. }
  459. }
  460. if (! item.renameFile (newFile))
  461. {
  462. auto options = MessageBoxOptions::makeOptionsOk (MessageBoxIconType::WarningIcon,
  463. "File Rename",
  464. "Failed to rename the file!\n\nCheck your file permissions!");
  465. messageBox = AlertWindow::showScopedAsync (options, nullptr);
  466. }
  467. }
  468. FileTreeItemBase* createSubItem (const Project::Item&) override
  469. {
  470. jassertfalse;
  471. return nullptr;
  472. }
  473. void showDocument() override
  474. {
  475. auto f = getFile();
  476. if (f.exists())
  477. if (auto* pcc = getProjectContentComponent())
  478. pcc->showEditorForFile (f, false);
  479. }
  480. void showPopupMenu (Point<int> p) override
  481. {
  482. PopupMenu m;
  483. m.addItem (1, "Open in external editor");
  484. m.addItem (2,
  485. #if JUCE_MAC
  486. "Reveal in Finder");
  487. #else
  488. "Reveal in Explorer");
  489. #endif
  490. m.addItem (4, "Rename File...");
  491. m.addSeparator();
  492. m.addItem (5, "Binary Resource", true, item.shouldBeAddedToBinaryResources());
  493. m.addItem (6, "Xcode Resource", true, item.shouldBeAddedToXcodeResources());
  494. m.addItem (7, "Compile", item.isSourceFile(), item.shouldBeCompiled());
  495. m.addItem (8, "Skip PCH", item.shouldBeCompiled(), item.shouldSkipPCH());
  496. m.addSeparator();
  497. m.addItem (3, "Delete");
  498. launchPopupMenu (m, p);
  499. }
  500. void showAddMenu (Point<int> p) override
  501. {
  502. if (auto* group = dynamic_cast<GroupItem*> (getParentItem()))
  503. group->showAddMenu (p);
  504. }
  505. void handlePopupMenuResult (int resultCode) override
  506. {
  507. switch (resultCode)
  508. {
  509. case 1: getFile().startAsProcess(); break;
  510. case 2: revealInFinder(); break;
  511. case 3: deleteAllSelectedItems(); break;
  512. case 4: triggerAsyncRename (item); break;
  513. case 5: item.getShouldAddToBinaryResourcesValue().setValue (! item.shouldBeAddedToBinaryResources()); break;
  514. case 6: item.getShouldAddToXcodeResourcesValue().setValue (! item.shouldBeAddedToXcodeResources()); break;
  515. case 7: item.getShouldCompileValue().setValue (! item.shouldBeCompiled()); break;
  516. case 8: item.getShouldSkipPCHValue().setValue (! item.shouldSkipPCH()); break;
  517. default:
  518. if (auto* parentGroup = dynamic_cast<GroupItem*> (getParentProjectItem()))
  519. parentGroup->processCreateFileMenuItem (resultCode);
  520. break;
  521. }
  522. }
  523. JUCE_DECLARE_WEAK_REFERENCEABLE (SourceFileItem)
  524. };
  525. //==============================================================================
  526. class GroupItem final : public FileTreeItemBase
  527. {
  528. public:
  529. GroupItem (const Project::Item& projectItem, const String& filter = {})
  530. : FileTreeItemBase (projectItem),
  531. searchFilter (filter)
  532. {
  533. }
  534. bool isRoot() const override { return item.isMainGroup(); }
  535. bool acceptsFileDrop (const StringArray&) const override { return true; }
  536. void addNewGroup()
  537. {
  538. auto newGroup = item.addNewSubGroup ("New Group", 0);
  539. triggerAsyncRename (newGroup);
  540. }
  541. bool acceptsDragItems (const OwnedArray<Project::Item>& selectedNodes) override
  542. {
  543. for (auto i = selectedNodes.size(); --i >= 0;)
  544. if (item.canContain (*selectedNodes.getUnchecked (i)))
  545. return true;
  546. return false;
  547. }
  548. void addFilesAtIndex (const StringArray& files, int insertIndex) override
  549. {
  550. for (auto f : files)
  551. {
  552. if (item.addFileAtIndex (f, insertIndex, true))
  553. ++insertIndex;
  554. }
  555. }
  556. void addFilesRetainingSortOrder (const StringArray& files) override
  557. {
  558. for (auto i = files.size(); --i >= 0;)
  559. item.addFileRetainingSortOrder (files[i], true);
  560. }
  561. void moveSelectedItemsTo (OwnedArray<Project::Item>& selectedNodes, int insertIndex) override
  562. {
  563. moveItems (selectedNodes, item, insertIndex);
  564. }
  565. void checkFileStatus() override
  566. {
  567. for (int i = 0; i < getNumSubItems(); ++i)
  568. if (auto* p = dynamic_cast<FileTreeItemBase*> (getSubItem (i)))
  569. p->checkFileStatus();
  570. }
  571. bool isGroupEmpty (const Project::Item& group) // recursive
  572. {
  573. for (int i = 0; i < group.getNumChildren(); ++i)
  574. {
  575. auto child = group.getChild (i);
  576. if ((child.isGroup() && ! isGroupEmpty (child))
  577. || (child.isFile() && child.getName().containsIgnoreCase (searchFilter)))
  578. return false;
  579. }
  580. return true;
  581. }
  582. FileTreeItemBase* createSubItem (const Project::Item& child) override
  583. {
  584. if (child.isGroup())
  585. {
  586. if (searchFilter.isNotEmpty() && isGroupEmpty (child))
  587. return nullptr;
  588. return new GroupItem (child, searchFilter);
  589. }
  590. if (child.isFile())
  591. {
  592. if (child.getName().containsIgnoreCase (searchFilter))
  593. return new SourceFileItem (child);
  594. return nullptr;
  595. }
  596. jassertfalse;
  597. return nullptr;
  598. }
  599. void showDocument() override
  600. {
  601. if (auto* pcc = getProjectContentComponent())
  602. pcc->setScrollableEditorComponent (std::make_unique<FileGroupInformationComponent> (item));
  603. }
  604. static void openAllGroups (TreeViewItem* root)
  605. {
  606. for (int i = 0; i < root->getNumSubItems(); ++i)
  607. if (auto* sub = root->getSubItem (i))
  608. openOrCloseAllSubGroups (*sub, true);
  609. }
  610. static void closeAllGroups (TreeViewItem* root)
  611. {
  612. for (int i = 0; i < root->getNumSubItems(); ++i)
  613. if (auto* sub = root->getSubItem (i))
  614. openOrCloseAllSubGroups (*sub, false);
  615. }
  616. static void openOrCloseAllSubGroups (TreeViewItem& treeItem, bool shouldOpen)
  617. {
  618. treeItem.setOpen (shouldOpen);
  619. for (auto i = treeItem.getNumSubItems(); --i >= 0;)
  620. if (auto* sub = treeItem.getSubItem (i))
  621. openOrCloseAllSubGroups (*sub, shouldOpen);
  622. }
  623. static void setFilesToCompile (Project::Item projectItem, const bool shouldCompile)
  624. {
  625. if (projectItem.isFile() && (projectItem.getFile().hasFileExtension (fileTypesToCompileByDefault)))
  626. projectItem.getShouldCompileValue() = shouldCompile;
  627. for (auto i = projectItem.getNumChildren(); --i >= 0;)
  628. setFilesToCompile (projectItem.getChild (i), shouldCompile);
  629. }
  630. void showPopupMenu (Point<int> p) override
  631. {
  632. PopupMenu m;
  633. addCreateFileMenuItems (m);
  634. m.addSeparator();
  635. m.addItem (1, "Collapse all Groups");
  636. m.addItem (2, "Expand all Groups");
  637. if (! isRoot())
  638. {
  639. if (isOpen())
  640. m.addItem (3, "Collapse all Sub-groups");
  641. else
  642. m.addItem (4, "Expand all Sub-groups");
  643. }
  644. m.addSeparator();
  645. m.addItem (5, "Enable compiling of all enclosed files");
  646. m.addItem (6, "Disable compiling of all enclosed files");
  647. m.addSeparator();
  648. m.addItem (7, "Sort Items Alphabetically");
  649. m.addItem (8, "Sort Items Alphabetically (Groups first)");
  650. m.addSeparator();
  651. if (! isRoot())
  652. {
  653. m.addItem (9, "Rename...");
  654. m.addItem (10, "Delete");
  655. }
  656. launchPopupMenu (m, p);
  657. }
  658. void showAddMenu (Point<int> p) override
  659. {
  660. PopupMenu m;
  661. addCreateFileMenuItems (m);
  662. launchPopupMenu (m, p);
  663. }
  664. void handlePopupMenuResult (int resultCode) override
  665. {
  666. switch (resultCode)
  667. {
  668. case 1: closeAllGroups (getOwnerView()->getRootItem()); break;
  669. case 2: openAllGroups (getOwnerView()->getRootItem()); break;
  670. case 3: openOrCloseAllSubGroups (*this, false); break;
  671. case 4: openOrCloseAllSubGroups (*this, true); break;
  672. case 5: setFilesToCompile (item, true); break;
  673. case 6: setFilesToCompile (item, false); break;
  674. case 7: item.sortAlphabetically (false, false); break;
  675. case 8: item.sortAlphabetically (true, false); break;
  676. case 9: triggerAsyncRename (item); break;
  677. case 10: deleteAllSelectedItems(); break;
  678. default: processCreateFileMenuItem (resultCode); break;
  679. }
  680. }
  681. void addCreateFileMenuItems (PopupMenu& m)
  682. {
  683. m.addItem (1001, "Add New Group");
  684. m.addItem (1002, "Add Existing Files...");
  685. m.addSeparator();
  686. wizard.addWizardsToMenu (m);
  687. }
  688. void processCreateFileMenuItem (int menuID)
  689. {
  690. switch (menuID)
  691. {
  692. case 1001: addNewGroup(); break;
  693. case 1002: browseToAddExistingFiles(); break;
  694. default:
  695. jassert (getProject() != nullptr);
  696. wizard.runWizardFromMenu (menuID, *getProject(), item);
  697. break;
  698. }
  699. }
  700. Project* getProject()
  701. {
  702. if (auto* tv = getOwnerView())
  703. if (auto* pcc = tv->findParentComponentOfClass<ProjectContentComponent>())
  704. return pcc->getProject();
  705. return nullptr;
  706. }
  707. void setSearchFilter (const String& filter) override
  708. {
  709. searchFilter = filter;
  710. refreshSubItems();
  711. }
  712. String searchFilter;
  713. NewFileWizard wizard;
  714. };