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.

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