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.

430 lines
15KB

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