/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2013 - Raw Material Software Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ class ProjectTreeItemBase : public JucerTreeViewBase, public ValueTree::Listener { public: ProjectTreeItemBase (const Project::Item& projectItem) : item (projectItem), isFileMissing (false) { item.state.addListener (this); } ~ProjectTreeItemBase() { item.state.removeListener (this); } //============================================================================== virtual bool isRoot() const { return false; } virtual bool acceptsFileDrop (const StringArray& files) const = 0; virtual bool acceptsDragItems (const OwnedArray& selectedNodes) = 0; //============================================================================== virtual String getDisplayName() const { return item.getName(); } virtual String getRenamingName() const { return getDisplayName(); } virtual void setName (const String& newName) { if (item.isMainGroup()) item.project.setTitle (newName); else item.getNameValue() = newName; } virtual bool isMissing() { return isFileMissing; } virtual File getFile() const { return item.getFile(); } virtual void deleteItem() { item.removeItemFromProject(); } virtual void deleteAllSelectedItems() { TreeView* tree = getOwnerView(); const int numSelected = tree->getNumSelectedItems(); OwnedArray filesToTrash; OwnedArray itemsToRemove; for (int i = 0; i < numSelected; ++i) { if (const ProjectTreeItemBase* const p = dynamic_cast (tree->getSelectedItem (i))) { itemsToRemove.add (new Project::Item (p->item)); if (p->getFile().existsAsFile()) filesToTrash.add (new File (p->getFile())); } } if (filesToTrash.size() > 0) { String fileList; const int maxFilesToList = 10; for (int i = jmin (maxFilesToList, filesToTrash.size()); --i >= 0;) fileList << filesToTrash.getUnchecked(i)->getFullPathName() << "\n"; if (filesToTrash.size() > maxFilesToList) fileList << "\n...plus " << (filesToTrash.size() - maxFilesToList) << " more files..."; int r = AlertWindow::showYesNoCancelBox (AlertWindow::NoIcon, "Delete Project Items", "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, "Just remove references", "Also move files to Trash", "Cancel", tree->getTopLevelComponent()); if (r == 0) return; if (r != 2) filesToTrash.clear(); } if (ProjectTreeItemBase* treeRootItem = dynamic_cast (tree->getRootItem())) { OpenDocumentManager& om = IntrojucerApp::getApp().openDocumentManager; for (int i = filesToTrash.size(); --i >= 0;) { const File f (*filesToTrash.getUnchecked(i)); om.closeFile (f, false); if (! f.moveToTrash()) { // xxx } } for (int i = itemsToRemove.size(); --i >= 0;) { if (ProjectTreeItemBase* itemToRemove = treeRootItem->findTreeViewItem (*itemsToRemove.getUnchecked(i))) { om.closeFile (itemToRemove->getFile(), false); itemToRemove->deleteItem(); } } } else { jassertfalse; } } virtual void revealInFinder() const { getFile().revealToUser(); } virtual void browseToAddExistingFiles() { const File location (item.isGroup() ? item.determineGroupFolder() : getFile()); FileChooser fc ("Add Files to Jucer Project", location, String::empty, false); if (fc.browseForMultipleFilesOrDirectories()) { StringArray files; for (int i = 0; i < fc.getResults().size(); ++i) files.add (fc.getResults().getReference(i).getFullPathName()); addFiles (files, 0); } } virtual void checkFileStatus() // (recursive) { const File file (getFile()); const bool nowMissing = file != File::nonexistent && ! file.exists(); if (nowMissing != isFileMissing) { isFileMissing = nowMissing; repaintItem(); } } virtual void addFiles (const StringArray& files, int insertIndex) { if (ProjectTreeItemBase* p = getParentProjectItem()) p->addFiles (files, insertIndex); } virtual void moveSelectedItemsTo (OwnedArray &, int /*insertIndex*/) { jassertfalse; } virtual void showMultiSelectionPopupMenu() { PopupMenu m; m.addItem (1, "Delete"); m.showMenuAsync (PopupMenu::Options(), ModalCallbackFunction::create (treeViewMultiSelectItemChosen, this)); } static void treeViewMultiSelectItemChosen (int resultCode, ProjectTreeItemBase* item) { switch (resultCode) { case 1: item->deleteAllSelectedItems(); break; default: break; } } virtual ProjectTreeItemBase* findTreeViewItem (const Project::Item& itemToFind) { if (item == itemToFind) return this; const bool wasOpen = isOpen(); setOpen (true); for (int i = getNumSubItems(); --i >= 0;) { if (ProjectTreeItemBase* pg = dynamic_cast (getSubItem(i))) if (ProjectTreeItemBase* found = pg->findTreeViewItem (itemToFind)) return found; } setOpen (wasOpen); return nullptr; } //============================================================================== void valueTreePropertyChanged (ValueTree& tree, const Identifier&) override { if (tree == item.state) repaintItem(); } void valueTreeChildAdded (ValueTree& parentTree, ValueTree&) override { treeChildrenChanged (parentTree); } void valueTreeChildRemoved (ValueTree& parentTree, ValueTree&) override { treeChildrenChanged (parentTree); } void valueTreeChildOrderChanged (ValueTree& parentTree) override { treeChildrenChanged (parentTree); } void valueTreeParentChanged (ValueTree&) override {} //============================================================================== bool mightContainSubItems() override { return item.getNumChildren() > 0; } String getUniqueName() const override { jassert (item.getID().isNotEmpty()); return item.getID(); } bool canBeSelected() const override { return true; } String getTooltip() override { return String::empty; } File getDraggableFile() const override { return getFile(); } var getDragSourceDescription() override { cancelDelayedSelectionTimer(); return projectItemDragType; } void addSubItems() override { for (int i = 0; i < item.getNumChildren(); ++i) if (ProjectTreeItemBase* p = createSubItem (item.getChild(i))) addSubItem (p); } void itemOpennessChanged (bool isNowOpen) override { if (isNowOpen) refreshSubItems(); } //============================================================================== bool isInterestedInFileDrag (const StringArray& files) override { return acceptsFileDrop (files); } void filesDropped (const StringArray& files, int insertIndex) override { if (files.size() == 1 && File (files[0]).hasFileExtension (Project::projectFileExtension)) IntrojucerApp::getApp().openFile (files[0]); else addFiles (files, insertIndex); } bool isInterestedInDragSource (const DragAndDropTarget::SourceDetails& dragSourceDetails) override { OwnedArray selectedNodes; getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes); return selectedNodes.size() > 0 && acceptsDragItems (selectedNodes); } void itemDropped (const DragAndDropTarget::SourceDetails& dragSourceDetails, int insertIndex) override { OwnedArray selectedNodes; getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes); if (selectedNodes.size() > 0) { TreeView* tree = getOwnerView(); ScopedPointer oldOpenness (tree->getOpennessState (false)); moveSelectedItemsTo (selectedNodes, insertIndex); if (oldOpenness != nullptr) tree->restoreOpennessState (*oldOpenness, false); } } int getMillisecsAllowedForDragGesture() override { // for images, give the user longer to start dragging before assuming they're // clicking to select it for previewing.. return item.isImageFile() ? 250 : JucerTreeViewBase::getMillisecsAllowedForDragGesture(); } static void getSelectedProjectItemsBeingDragged (const DragAndDropTarget::SourceDetails& dragSourceDetails, OwnedArray& selectedNodes) { if (dragSourceDetails.description == projectItemDragType) { TreeView* tree = dynamic_cast (dragSourceDetails.sourceComponent.get()); if (tree == nullptr) tree = dragSourceDetails.sourceComponent->findParentComponentOfClass(); if (tree != nullptr) { const int numSelected = tree->getNumSelectedItems(); for (int i = 0; i < numSelected; ++i) if (const ProjectTreeItemBase* const p = dynamic_cast (tree->getSelectedItem (i))) selectedNodes.add (new Project::Item (p->item)); } } } ProjectTreeItemBase* getParentProjectItem() const { return dynamic_cast (getParentItem()); } //============================================================================== Project::Item item; protected: bool isFileMissing; virtual ProjectTreeItemBase* createSubItem (const Project::Item& node) = 0; Icon getIcon() const override { return item.getIcon().withContrastingColourTo (getBackgroundColour()); } bool isIconCrossedOut() const override { return item.isIconCrossedOut(); } void treeChildrenChanged (const ValueTree& parentTree) { if (parentTree == item.state) { refreshSubItems(); treeHasChanged(); setOpen (true); } } void triggerAsyncRename (const Project::Item& itemToRename) { class RenameMessage : public CallbackMessage { public: RenameMessage (TreeView* const t, const Project::Item& i) : tree (t), itemToRename (i) {} void messageCallback() override { if (tree != nullptr) if (ProjectTreeItemBase* root = dynamic_cast (tree->getRootItem())) if (ProjectTreeItemBase* found = root->findTreeViewItem (itemToRename)) found->showRenameBox(); } private: Component::SafePointer tree; Project::Item itemToRename; }; (new RenameMessage (getOwnerView(), itemToRename))->post(); } static void moveItems (OwnedArray & selectedNodes, Project::Item destNode, int insertIndex) { for (int i = selectedNodes.size(); --i >= 0;) { Project::Item* const n = selectedNodes.getUnchecked(i); if (destNode == *n || destNode.state.isAChildOf (n->state)) // Check for recursion. return; if (! destNode.canContain (*n)) selectedNodes.remove (i); } // Don't include any nodes that are children of other selected nodes.. for (int i = selectedNodes.size(); --i >= 0;) { Project::Item* const n = selectedNodes.getUnchecked(i); for (int j = selectedNodes.size(); --j >= 0;) { if (j != i && n->state.isAChildOf (selectedNodes.getUnchecked(j)->state)) { selectedNodes.remove (i); break; } } } // Remove and re-insert them one at a time.. for (int i = 0; i < selectedNodes.size(); ++i) { Project::Item* selectedNode = selectedNodes.getUnchecked(i); if (selectedNode->state.getParent() == destNode.state && indexOfNode (destNode.state, selectedNode->state) < insertIndex) --insertIndex; selectedNode->removeItemFromProject(); destNode.addChild (*selectedNode, insertIndex++); } } static int indexOfNode (const ValueTree& parent, const ValueTree& child) { for (int i = parent.getNumChildren(); --i >= 0;) if (parent.getChild (i) == child) return i; return -1; } };