/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 5 End-User License Agreement and JUCE 5 Privacy Policy (both updated and effective as of the 27th April 2017). End User License Agreement: www.juce.com/juce-5-licence Privacy Policy: www.juce.com/juce-5-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #pragma once //============================================================================== class FileTreeItemBase : public JucerTreeViewBase, public ValueTree::Listener { public: FileTreeItemBase (const Project::Item& projectItem) : item (projectItem), isFileMissing (false) { item.state.addListener (this); } ~FileTreeItemBase() override { item.state.removeListener (this); } //============================================================================== virtual bool acceptsFileDrop (const StringArray& files) const = 0; virtual bool acceptsDragItems (const OwnedArray& selectedNodes) = 0; //============================================================================== String getDisplayName() const override { return item.getName(); } String getRenamingName() const override { return getDisplayName(); } void setName (const String& newName) override { item.getNameValue() = newName; } bool isMissing() const override { return isFileMissing; } virtual File getFile() const { return item.getFile(); } void deleteItem() override { item.removeItemFromProject(); } void deleteAllSelectedItems() override { auto* tree = getOwnerView(); Array filesToTrash; Array itemsToRemove; for (int i = 0; i < tree->getNumSelectedItems(); ++i) { if (auto* p = dynamic_cast (tree->getSelectedItem (i))) { itemsToRemove.add (p->item); if (p->item.isGroup()) { for (int j = 0; j < p->item.getNumChildren(); ++j) { auto associatedFile = p->item.getChild (j).getFile(); if (associatedFile.existsAsFile()) filesToTrash.addIfNotAlreadyThere (associatedFile); } } else if (p->getFile().existsAsFile()) { filesToTrash.addIfNotAlreadyThere (p->getFile()); } } } if (filesToTrash.size() > 0) { String fileList; auto maxFilesToList = 10; for (auto 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..."; auto 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 (auto* treeRootItem = dynamic_cast (tree->getRootItem())) { auto& om = ProjucerApplication::getApp().openDocumentManager; for (auto i = filesToTrash.size(); --i >= 0;) { auto f = filesToTrash.getUnchecked(i); om.closeFile (f, false); if (! f.moveToTrash()) { // xxx } } for (auto i = itemsToRemove.size(); --i >= 0;) { if (auto itemToRemove = treeRootItem->findTreeViewItem (itemsToRemove.getUnchecked (i))) { if (auto* pcc = treeRootItem->getProjectContentComponent()) { if (auto* fileInfoComp = dynamic_cast (pcc->getEditorComponentContent())) if (fileInfoComp->getGroupPath() == itemToRemove->getFile().getFullPathName()) pcc->hideEditor(); } om.closeFile (itemToRemove->getFile(), false); itemToRemove->deleteItem(); } } } else { jassertfalse; } } virtual void revealInFinder() const { getFile().revealToUser(); } virtual void browseToAddExistingFiles() { auto location = item.isGroup() ? item.determineGroupFolder() : getFile(); FileChooser fc ("Add Files to Jucer Project", location, {}); if (fc.browseForMultipleFilesOrDirectories()) { StringArray files; for (int i = 0; i < fc.getResults().size(); ++i) files.add (fc.getResults().getReference(i).getFullPathName()); addFilesRetainingSortOrder (files); } } virtual void checkFileStatus() // (recursive) { auto file = getFile(); auto nowMissing = (file != File() && ! file.exists()); if (nowMissing != isFileMissing) { isFileMissing = nowMissing; repaintItem(); } } virtual void addFilesAtIndex (const StringArray& files, int insertIndex) { if (auto* p = getParentProjectItem()) p->addFilesAtIndex (files, insertIndex); } virtual void addFilesRetainingSortOrder (const StringArray& files) { if (auto* p = getParentProjectItem()) p->addFilesRetainingSortOrder (files); } virtual void moveSelectedItemsTo (OwnedArray &, int /*insertIndex*/) { jassertfalse; } void showMultiSelectionPopupMenu() override { PopupMenu m; m.addItem (1, "Delete"); m.showMenuAsync (PopupMenu::Options(), ModalCallbackFunction::create (treeViewMultiSelectItemChosen, this)); } static void treeViewMultiSelectItemChosen (int resultCode, FileTreeItemBase* item) { switch (resultCode) { case 1: item->deleteAllSelectedItems(); break; default: break; } } virtual FileTreeItemBase* findTreeViewItem (const Project::Item& itemToFind) { if (item == itemToFind) return this; auto wasOpen = isOpen(); setOpen (true); for (auto i = getNumSubItems(); --i >= 0;) { if (auto* pg = dynamic_cast (getSubItem(i))) if (auto* 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&, int) override { treeChildrenChanged (parentTree); } void valueTreeChildOrderChanged (ValueTree& parentTree, int, int) override { treeChildrenChanged (parentTree); } //============================================================================== 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 {}; } File getDraggableFile() const override { return getFile(); } var getDragSourceDescription() override { cancelDelayedSelectionTimer(); return projectItemDragType; } void addSubItems() override { for (int i = 0; i < item.getNumChildren(); ++i) if (auto* 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)) ProjucerApplication::getApp().openFile (files[0]); else addFilesAtIndex (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) { auto* tree = getOwnerView(); std::unique_ptr 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) { auto* tree = dynamic_cast (dragSourceDetails.sourceComponent.get()); if (tree == nullptr) tree = dragSourceDetails.sourceComponent->findParentComponentOfClass(); if (tree != nullptr) { auto numSelected = tree->getNumSelectedItems(); for (int i = 0; i < numSelected; ++i) if (auto* p = dynamic_cast (tree->getSelectedItem (i))) selectedNodes.add (new Project::Item (p->item)); } } } FileTreeItemBase* getParentProjectItem() const { return dynamic_cast (getParentItem()); } //============================================================================== Project::Item item; protected: bool isFileMissing; virtual FileTreeItemBase* createSubItem (const Project::Item& node) = 0; Icon getIcon() const override { auto colour = getOwnerView()->findColour (isSelected() ? defaultHighlightedTextColourId : treeIconColourId); return item.getIcon (isOpen()).withColour (colour); } 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) { struct RenameMessage : public CallbackMessage { RenameMessage (TreeView* const t, const Project::Item& i) : tree (t), itemToRename (i) {} void messageCallback() override { if (tree != nullptr) if (auto* root = dynamic_cast (tree->getRootItem())) if (auto* 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 (auto i = selectedNodes.size(); --i >= 0;) { auto* 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 (auto i = selectedNodes.size(); --i >= 0;) { auto* n = selectedNodes.getUnchecked(i); for (auto 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) { auto* 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 (auto i = parent.getNumChildren(); --i >= 0;) if (parent.getChild (i) == child) return i; return -1; } }; //============================================================================== class SourceFileItem : public FileTreeItemBase { public: SourceFileItem (const Project::Item& projectItem) : FileTreeItemBase (projectItem) { } bool acceptsFileDrop (const StringArray&) const override { return false; } bool acceptsDragItems (const OwnedArray &) override { return false; } String getDisplayName() const override { return getFile().getFileName(); } void paintItem (Graphics& g, int width, int height) override { JucerTreeViewBase::paintItem (g, width, height); if (item.needsSaving()) { auto bounds = g.getClipBounds().withY (0).withHeight (height); g.setFont (getFont()); g.setColour (getContentColour (false)); g.drawFittedText ("*", bounds.removeFromLeft (height), Justification::centred, 1); } } static File findCorrespondingHeaderOrCpp (const File& f) { if (f.hasFileExtension (sourceFileExtensions)) return f.withFileExtension (".h"); if (f.hasFileExtension (headerFileExtensions)) return f.withFileExtension (".cpp"); return {}; } void setName (const String& newName) override { if (newName != File::createLegalFileName (newName)) { AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename", "That filename contained some illegal characters!"); triggerAsyncRename (item); return; } auto oldFile = getFile(); auto newFile = oldFile.getSiblingFile (newName); auto correspondingFile = findCorrespondingHeaderOrCpp (oldFile); if (correspondingFile.exists() && newFile.hasFileExtension (oldFile.getFileExtension())) { auto correspondingItem = item.project.getMainGroup().findItemForFile (correspondingFile); if (correspondingItem.isValid()) { if (AlertWindow::showOkCancelBox (AlertWindow::NoIcon, "File Rename", "Do you also want to rename the corresponding file \"" + correspondingFile.getFileName() + "\" to match?")) { if (! item.renameFile (newFile)) { AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename", "Failed to rename \"" + oldFile.getFullPathName() + "\"!\n\nCheck your file permissions!"); return; } if (! correspondingItem.renameFile (newFile.withFileExtension (correspondingFile.getFileExtension()))) { AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename", "Failed to rename \"" + correspondingFile.getFullPathName() + "\"!\n\nCheck your file permissions!"); } } } } if (! item.renameFile (newFile)) { AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename", "Failed to rename the file!\n\nCheck your file permissions!"); } } FileTreeItemBase* createSubItem (const Project::Item&) override { jassertfalse; return nullptr; } void showDocument() override { auto f = getFile(); if (f.exists()) if (auto* pcc = getProjectContentComponent()) pcc->showEditorForFile (f, false); } void showPopupMenu() override { PopupMenu m; m.addItem (1, "Open in external editor"); m.addItem (2, #if JUCE_MAC "Reveal in Finder"); #else "Reveal in Explorer"); #endif m.addItem (4, "Rename File..."); m.addSeparator(); if (auto* group = dynamic_cast (getParentItem())) { if (group->isRoot()) { m.addItem (5, "Binary Resource", true, item.shouldBeAddedToBinaryResources()); m.addItem (6, "Xcode Resource", true, item.shouldBeAddedToXcodeResources()); m.addItem (7, "Compile", true, item.shouldBeCompiled()); m.addSeparator(); } } m.addItem (3, "Delete"); launchPopupMenu (m); } void showAddMenu() override { if (auto* group = dynamic_cast (getParentItem())) group->showAddMenu(); } void handlePopupMenuResult (int resultCode) override { switch (resultCode) { case 1: getFile().startAsProcess(); break; case 2: revealInFinder(); break; case 3: deleteAllSelectedItems(); break; case 4: triggerAsyncRename (item); break; case 5: item.getShouldAddToBinaryResourcesValue().setValue (! item.shouldBeAddedToBinaryResources()); break; case 6: item.getShouldAddToXcodeResourcesValue().setValue (! item.shouldBeAddedToXcodeResources()); break; case 7: item.getShouldCompileValue().setValue (! item.shouldBeCompiled()); break; default: if (auto* parentGroup = dynamic_cast (getParentProjectItem())) parentGroup->processCreateFileMenuItem (resultCode); break; } } }; //============================================================================== class GroupItem : public FileTreeItemBase { public: GroupItem (const Project::Item& projectItem, const String& filter = {}) : FileTreeItemBase (projectItem), searchFilter (filter) { } bool isRoot() const override { return item.isMainGroup(); } bool acceptsFileDrop (const StringArray&) const override { return true; } void addNewGroup() { auto newGroup = item.addNewSubGroup ("New Group", 0); triggerAsyncRename (newGroup); } bool acceptsDragItems (const OwnedArray& selectedNodes) override { for (auto i = selectedNodes.size(); --i >= 0;) if (item.canContain (*selectedNodes.getUnchecked(i))) return true; return false; } void addFilesAtIndex (const StringArray& files, int insertIndex) override { for (auto f : files) { if (item.addFileAtIndex (f, insertIndex, true)) ++insertIndex; } } void addFilesRetainingSortOrder (const StringArray& files) override { for (auto i = files.size(); --i >= 0;) item.addFileRetainingSortOrder (files[i], true); } void moveSelectedItemsTo (OwnedArray& selectedNodes, int insertIndex) override { moveItems (selectedNodes, item, insertIndex); } void checkFileStatus() override { for (int i = 0; i < getNumSubItems(); ++i) if (auto* p = dynamic_cast (getSubItem(i))) p->checkFileStatus(); } bool isGroupEmpty (const Project::Item& group) // recursive { for (int i = 0; i < group.getNumChildren(); ++i) { auto child = group.getChild (i); if ((child.isGroup() && ! isGroupEmpty (child)) || (child.isFile() && child.getName().containsIgnoreCase (searchFilter))) return false; } return true; } FileTreeItemBase* createSubItem (const Project::Item& child) override { if (child.isGroup()) { if (searchFilter.isNotEmpty() && isGroupEmpty (child)) return nullptr; return new GroupItem (child, searchFilter); } if (child.isFile()) { if (child.getName().containsIgnoreCase (searchFilter)) return new SourceFileItem (child); return nullptr; } jassertfalse; return nullptr; } void showDocument() override { if (auto* pcc = getProjectContentComponent()) pcc->setEditorComponent (new FileGroupInformationComponent (item), nullptr); } static void openAllGroups (TreeViewItem* root) { for (int i = 0; i < root->getNumSubItems(); ++i) if (auto* sub = root->getSubItem (i)) openOrCloseAllSubGroups (*sub, true); } static void closeAllGroups (TreeViewItem* root) { for (int i = 0; i < root->getNumSubItems(); ++i) if (auto* sub = root->getSubItem (i)) openOrCloseAllSubGroups (*sub, false); } static void openOrCloseAllSubGroups (TreeViewItem& treeItem, bool shouldOpen) { treeItem.setOpen (shouldOpen); for (auto i = treeItem.getNumSubItems(); --i >= 0;) if (auto* sub = treeItem.getSubItem (i)) openOrCloseAllSubGroups (*sub, shouldOpen); } static void setFilesToCompile (Project::Item projectItem, const bool shouldCompile) { if (projectItem.isFile() && (projectItem.getFile().hasFileExtension (fileTypesToCompileByDefault))) projectItem.getShouldCompileValue() = shouldCompile; for (auto i = projectItem.getNumChildren(); --i >= 0;) setFilesToCompile (projectItem.getChild (i), shouldCompile); } void showPopupMenu() override { PopupMenu m; addCreateFileMenuItems (m); m.addSeparator(); m.addItem (1, "Collapse all Groups"); m.addItem (2, "Expand all Groups"); if (! isRoot()) { if (isOpen()) m.addItem (3, "Collapse all Sub-groups"); else m.addItem (4, "Expand all Sub-groups"); } m.addSeparator(); m.addItem (5, "Enable compiling of all enclosed files"); m.addItem (6, "Disable compiling of all enclosed files"); m.addSeparator(); m.addItem (7, "Sort Items Alphabetically"); m.addItem (8, "Sort Items Alphabetically (Groups first)"); m.addSeparator(); if (! isRoot()) { m.addItem (9, "Rename..."); m.addItem (10, "Delete"); } launchPopupMenu (m); } void showAddMenu() override { PopupMenu m; addCreateFileMenuItems (m); launchPopupMenu (m); } void handlePopupMenuResult (int resultCode) override { switch (resultCode) { case 1: closeAllGroups (getOwnerView()->getRootItem()); break; case 2: openAllGroups (getOwnerView()->getRootItem()); break; case 3: openOrCloseAllSubGroups (*this, false); break; case 4: openOrCloseAllSubGroups (*this, true); break; case 5: setFilesToCompile (item, true); break; case 6: setFilesToCompile (item, false); break; case 7: item.sortAlphabetically (false, false); break; case 8: item.sortAlphabetically (true, false); break; case 9: triggerAsyncRename (item); break; case 10: deleteAllSelectedItems(); break; default: processCreateFileMenuItem (resultCode); break; } } void addCreateFileMenuItems (PopupMenu& m) { m.addItem (1001, "Add New Group"); m.addItem (1002, "Add Existing Files..."); m.addSeparator(); NewFileWizard().addWizardsToMenu (m); } void processCreateFileMenuItem (int menuID) { switch (menuID) { case 1001: addNewGroup(); break; case 1002: browseToAddExistingFiles(); break; default: jassert (getProject() != nullptr); NewFileWizard().runWizardFromMenu (menuID, *getProject(), item); break; } } Project* getProject() { if (auto* tv = getOwnerView()) if (auto* pcc = tv->findParentComponentOfClass()) return pcc->getProject(); return nullptr; } void setSearchFilter (const String& filter) override { searchFilter = filter; refreshSubItems(); } String searchFilter; };