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.

428 lines
15KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  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 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. class ProjectTreeItemBase : public JucerTreeViewBase,
  20. public ValueTree::Listener
  21. {
  22. public:
  23. ProjectTreeItemBase (const Project::Item& projectItem)
  24. : item (projectItem), isFileMissing (false)
  25. {
  26. item.state.addListener (this);
  27. }
  28. ~ProjectTreeItemBase()
  29. {
  30. item.state.removeListener (this);
  31. }
  32. //==============================================================================
  33. virtual bool acceptsFileDrop (const StringArray& files) const = 0;
  34. virtual bool acceptsDragItems (const OwnedArray<Project::Item>& selectedNodes) = 0;
  35. //==============================================================================
  36. String getDisplayName() const override { return item.getName(); }
  37. String getRenamingName() const override { return getDisplayName(); }
  38. void setName (const String& newName) override { item.getNameValue() = newName; }
  39. bool isMissing() const override { return isFileMissing; }
  40. virtual File getFile() const { return item.getFile(); }
  41. void deleteItem() override { item.removeItemFromProject(); }
  42. virtual void deleteAllSelectedItems() override
  43. {
  44. TreeView* tree = getOwnerView();
  45. const int numSelected = tree->getNumSelectedItems();
  46. OwnedArray<File> filesToTrash;
  47. OwnedArray<Project::Item> itemsToRemove;
  48. for (int i = 0; i < numSelected; ++i)
  49. {
  50. if (auto* p = dynamic_cast<ProjectTreeItemBase*> (tree->getSelectedItem (i)))
  51. {
  52. itemsToRemove.add (new Project::Item (p->item));
  53. if (p->getFile().existsAsFile())
  54. filesToTrash.add (new File (p->getFile()));
  55. }
  56. }
  57. if (filesToTrash.size() > 0)
  58. {
  59. String fileList;
  60. const int maxFilesToList = 10;
  61. for (int i = jmin (maxFilesToList, filesToTrash.size()); --i >= 0;)
  62. fileList << filesToTrash.getUnchecked(i)->getFullPathName() << "\n";
  63. if (filesToTrash.size() > maxFilesToList)
  64. fileList << "\n...plus " << (filesToTrash.size() - maxFilesToList) << " more files...";
  65. int r = AlertWindow::showYesNoCancelBox (AlertWindow::NoIcon, "Delete Project Items",
  66. "As well as removing the selected item(s) from the project, do you also want to move their files to the trash:\n\n"
  67. + fileList,
  68. "Just remove references",
  69. "Also move files to Trash",
  70. "Cancel",
  71. tree->getTopLevelComponent());
  72. if (r == 0)
  73. return;
  74. if (r != 2)
  75. filesToTrash.clear();
  76. }
  77. if (ProjectTreeItemBase* treeRootItem = dynamic_cast<ProjectTreeItemBase*> (tree->getRootItem()))
  78. {
  79. OpenDocumentManager& om = ProjucerApplication::getApp().openDocumentManager;
  80. for (int i = filesToTrash.size(); --i >= 0;)
  81. {
  82. const File f (*filesToTrash.getUnchecked(i));
  83. om.closeFile (f, false);
  84. if (! f.moveToTrash())
  85. {
  86. // xxx
  87. }
  88. }
  89. for (int i = itemsToRemove.size(); --i >= 0;)
  90. {
  91. if (ProjectTreeItemBase* itemToRemove = treeRootItem->findTreeViewItem (*itemsToRemove.getUnchecked(i)))
  92. {
  93. om.closeFile (itemToRemove->getFile(), false);
  94. itemToRemove->deleteItem();
  95. }
  96. }
  97. }
  98. else
  99. {
  100. jassertfalse;
  101. }
  102. }
  103. virtual void revealInFinder() const
  104. {
  105. getFile().revealToUser();
  106. }
  107. virtual void browseToAddExistingFiles()
  108. {
  109. const File location (item.isGroup() ? item.determineGroupFolder() : getFile());
  110. FileChooser fc ("Add Files to Jucer Project", location, String());
  111. if (fc.browseForMultipleFilesOrDirectories())
  112. {
  113. StringArray files;
  114. for (int i = 0; i < fc.getResults().size(); ++i)
  115. files.add (fc.getResults().getReference(i).getFullPathName());
  116. addFilesRetainingSortOrder (files);
  117. }
  118. }
  119. virtual void checkFileStatus() // (recursive)
  120. {
  121. const File file (getFile());
  122. const bool nowMissing = file != File() && ! file.exists();
  123. if (nowMissing != isFileMissing)
  124. {
  125. isFileMissing = nowMissing;
  126. repaintItem();
  127. }
  128. }
  129. virtual void addFilesAtIndex (const StringArray& files, int insertIndex)
  130. {
  131. if (ProjectTreeItemBase* p = getParentProjectItem())
  132. p->addFilesAtIndex (files, insertIndex);
  133. }
  134. virtual void addFilesRetainingSortOrder (const StringArray& files)
  135. {
  136. if (ProjectTreeItemBase* p = getParentProjectItem())
  137. p->addFilesRetainingSortOrder (files);
  138. }
  139. virtual void moveSelectedItemsTo (OwnedArray <Project::Item>&, int /*insertIndex*/)
  140. {
  141. jassertfalse;
  142. }
  143. void showMultiSelectionPopupMenu() override
  144. {
  145. PopupMenu m;
  146. m.addItem (1, "Delete");
  147. m.showMenuAsync (PopupMenu::Options(),
  148. ModalCallbackFunction::create (treeViewMultiSelectItemChosen, this));
  149. }
  150. static void treeViewMultiSelectItemChosen (int resultCode, ProjectTreeItemBase* item)
  151. {
  152. switch (resultCode)
  153. {
  154. case 1: item->deleteAllSelectedItems(); break;
  155. default: break;
  156. }
  157. }
  158. virtual ProjectTreeItemBase* findTreeViewItem (const Project::Item& itemToFind)
  159. {
  160. if (item == itemToFind)
  161. return this;
  162. const bool wasOpen = isOpen();
  163. setOpen (true);
  164. for (int i = getNumSubItems(); --i >= 0;)
  165. {
  166. if (ProjectTreeItemBase* pg = dynamic_cast<ProjectTreeItemBase*> (getSubItem(i)))
  167. if (ProjectTreeItemBase* found = pg->findTreeViewItem (itemToFind))
  168. return found;
  169. }
  170. setOpen (wasOpen);
  171. return nullptr;
  172. }
  173. //==============================================================================
  174. void valueTreePropertyChanged (ValueTree& tree, const Identifier&) override
  175. {
  176. if (tree == item.state)
  177. repaintItem();
  178. }
  179. void valueTreeChildAdded (ValueTree& parentTree, ValueTree&) override { treeChildrenChanged (parentTree); }
  180. void valueTreeChildRemoved (ValueTree& parentTree, ValueTree&, int) override { treeChildrenChanged (parentTree); }
  181. void valueTreeChildOrderChanged (ValueTree& parentTree, int, int) override { treeChildrenChanged (parentTree); }
  182. void valueTreeParentChanged (ValueTree&) override {}
  183. //==============================================================================
  184. bool mightContainSubItems() override { return item.getNumChildren() > 0; }
  185. String getUniqueName() const override { jassert (item.getID().isNotEmpty()); return item.getID(); }
  186. bool canBeSelected() const override { return true; }
  187. String getTooltip() override { return {}; }
  188. File getDraggableFile() const override { return getFile(); }
  189. var getDragSourceDescription() override
  190. {
  191. cancelDelayedSelectionTimer();
  192. return projectItemDragType;
  193. }
  194. void addSubItems() override
  195. {
  196. for (int i = 0; i < item.getNumChildren(); ++i)
  197. if (ProjectTreeItemBase* p = createSubItem (item.getChild(i)))
  198. addSubItem (p);
  199. }
  200. void itemOpennessChanged (bool isNowOpen) override
  201. {
  202. if (isNowOpen)
  203. refreshSubItems();
  204. }
  205. //==============================================================================
  206. bool isInterestedInFileDrag (const StringArray& files) override
  207. {
  208. return acceptsFileDrop (files);
  209. }
  210. void filesDropped (const StringArray& files, int insertIndex) override
  211. {
  212. if (files.size() == 1 && File (files[0]).hasFileExtension (Project::projectFileExtension))
  213. ProjucerApplication::getApp().openFile (files[0]);
  214. else
  215. addFilesAtIndex (files, insertIndex);
  216. }
  217. bool isInterestedInDragSource (const DragAndDropTarget::SourceDetails& dragSourceDetails) override
  218. {
  219. OwnedArray<Project::Item> selectedNodes;
  220. getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes);
  221. return selectedNodes.size() > 0 && acceptsDragItems (selectedNodes);
  222. }
  223. void itemDropped (const DragAndDropTarget::SourceDetails& dragSourceDetails, int insertIndex) override
  224. {
  225. OwnedArray<Project::Item> selectedNodes;
  226. getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes);
  227. if (selectedNodes.size() > 0)
  228. {
  229. TreeView* tree = getOwnerView();
  230. ScopedPointer<XmlElement> oldOpenness (tree->getOpennessState (false));
  231. moveSelectedItemsTo (selectedNodes, insertIndex);
  232. if (oldOpenness != nullptr)
  233. tree->restoreOpennessState (*oldOpenness, false);
  234. }
  235. }
  236. int getMillisecsAllowedForDragGesture() override
  237. {
  238. // for images, give the user longer to start dragging before assuming they're
  239. // clicking to select it for previewing..
  240. return item.isImageFile() ? 250 : JucerTreeViewBase::getMillisecsAllowedForDragGesture();
  241. }
  242. static void getSelectedProjectItemsBeingDragged (const DragAndDropTarget::SourceDetails& dragSourceDetails,
  243. OwnedArray<Project::Item>& selectedNodes)
  244. {
  245. if (dragSourceDetails.description == projectItemDragType)
  246. {
  247. TreeView* tree = dynamic_cast<TreeView*> (dragSourceDetails.sourceComponent.get());
  248. if (tree == nullptr)
  249. tree = dragSourceDetails.sourceComponent->findParentComponentOfClass<TreeView>();
  250. if (tree != nullptr)
  251. {
  252. const int numSelected = tree->getNumSelectedItems();
  253. for (int i = 0; i < numSelected; ++i)
  254. if (const ProjectTreeItemBase* const p = dynamic_cast<ProjectTreeItemBase*> (tree->getSelectedItem (i)))
  255. selectedNodes.add (new Project::Item (p->item));
  256. }
  257. }
  258. }
  259. ProjectTreeItemBase* getParentProjectItem() const
  260. {
  261. return dynamic_cast<ProjectTreeItemBase*> (getParentItem());
  262. }
  263. //==============================================================================
  264. Project::Item item;
  265. protected:
  266. bool isFileMissing;
  267. virtual ProjectTreeItemBase* createSubItem (const Project::Item& node) = 0;
  268. Icon getIcon() const override
  269. {
  270. auto colour = getOwnerView()->findColour (isSelected() ? defaultHighlightedTextColourId
  271. : treeIconColourId);
  272. return item.getIcon (isOpen()).withColour (colour);
  273. }
  274. bool isIconCrossedOut() const override { return item.isIconCrossedOut(); }
  275. void treeChildrenChanged (const ValueTree& parentTree)
  276. {
  277. if (parentTree == item.state)
  278. {
  279. refreshSubItems();
  280. treeHasChanged();
  281. setOpen (true);
  282. }
  283. }
  284. void triggerAsyncRename (const Project::Item& itemToRename)
  285. {
  286. struct RenameMessage : public CallbackMessage
  287. {
  288. RenameMessage (TreeView* const t, const Project::Item& i)
  289. : tree (t), itemToRename (i) {}
  290. void messageCallback() override
  291. {
  292. if (tree != nullptr)
  293. if (ProjectTreeItemBase* root = dynamic_cast<ProjectTreeItemBase*> (tree->getRootItem()))
  294. if (ProjectTreeItemBase* found = root->findTreeViewItem (itemToRename))
  295. found->showRenameBox();
  296. }
  297. private:
  298. Component::SafePointer<TreeView> tree;
  299. Project::Item itemToRename;
  300. };
  301. (new RenameMessage (getOwnerView(), itemToRename))->post();
  302. }
  303. static void moveItems (OwnedArray<Project::Item>& selectedNodes, Project::Item destNode, int insertIndex)
  304. {
  305. for (int i = selectedNodes.size(); --i >= 0;)
  306. {
  307. Project::Item* const n = selectedNodes.getUnchecked(i);
  308. if (destNode == *n || destNode.state.isAChildOf (n->state)) // Check for recursion.
  309. return;
  310. if (! destNode.canContain (*n))
  311. selectedNodes.remove (i);
  312. }
  313. // Don't include any nodes that are children of other selected nodes..
  314. for (int i = selectedNodes.size(); --i >= 0;)
  315. {
  316. Project::Item* const n = selectedNodes.getUnchecked(i);
  317. for (int j = selectedNodes.size(); --j >= 0;)
  318. {
  319. if (j != i && n->state.isAChildOf (selectedNodes.getUnchecked(j)->state))
  320. {
  321. selectedNodes.remove (i);
  322. break;
  323. }
  324. }
  325. }
  326. // Remove and re-insert them one at a time..
  327. for (int i = 0; i < selectedNodes.size(); ++i)
  328. {
  329. Project::Item* selectedNode = selectedNodes.getUnchecked(i);
  330. if (selectedNode->state.getParent() == destNode.state
  331. && indexOfNode (destNode.state, selectedNode->state) < insertIndex)
  332. --insertIndex;
  333. selectedNode->removeItemFromProject();
  334. destNode.addChild (*selectedNode, insertIndex++);
  335. }
  336. }
  337. static int indexOfNode (const ValueTree& parent, const ValueTree& child)
  338. {
  339. for (int i = parent.getNumChildren(); --i >= 0;)
  340. if (parent.getChild (i) == child)
  341. return i;
  342. return -1;
  343. }
  344. };