From f4d8cf70d156dd48ea8a7055ab7ae240e6b939b8 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 20 Jun 2017 14:57:04 +0100 Subject: [PATCH] Projucer: Added a search field to the file panel to filter project files and added options to expand/collapse all groups in the file tree popup menu --- .../Project/jucer_ProjectContentComponent.cpp | 11 +- .../Source/Project/jucer_ProjectTab.h | 167 +++++++++++++++++- .../Source/Project/jucer_ProjectTree_Group.h | 100 ++++++++--- 3 files changed, 244 insertions(+), 34 deletions(-) diff --git a/extras/Projucer/Source/Project/jucer_ProjectContentComponent.cpp b/extras/Projucer/Source/Project/jucer_ProjectContentComponent.cpp index b84064dda0..20461b7b21 100644 --- a/extras/Projucer/Source/Project/jucer_ProjectContentComponent.cpp +++ b/extras/Projucer/Source/Project/jucer_ProjectContentComponent.cpp @@ -982,7 +982,7 @@ void ProjectContentComponent::getCommandInfo (const CommandID commandID, Applica "Shows the main project options page", CommandCategories::general, 0); result.setActive (project != nullptr); - result.defaultKeypresses.add (KeyPress ('p', ModifierKeys::commandModifier | ModifierKeys::shiftModifier, 0)); + result.defaultKeypresses.add (KeyPress ('s', ModifierKeys::commandModifier | ModifierKeys::ctrlModifier, 0)); break; case CommandIDs::showProjectTab: @@ -990,7 +990,7 @@ void ProjectContentComponent::getCommandInfo (const CommandID commandID, Applica "Shows the tab containing the project information", CommandCategories::general, 0); result.setActive (project != nullptr); - result.defaultKeypresses.add (KeyPress ('p', ModifierKeys::commandModifier, 0)); + result.defaultKeypresses.add (KeyPress ('p', ModifierKeys::commandModifier | ModifierKeys::ctrlModifier, 0)); break; case CommandIDs::showBuildTab: @@ -998,6 +998,7 @@ void ProjectContentComponent::getCommandInfo (const CommandID commandID, Applica "Shows the tab containing the build panel", CommandCategories::general, 0); result.setActive (project != nullptr); + result.defaultKeypresses.add (KeyPress ('b', ModifierKeys::commandModifier | ModifierKeys::ctrlModifier, 0)); break; case CommandIDs::showFileExplorerPanel: @@ -1005,7 +1006,7 @@ void ProjectContentComponent::getCommandInfo (const CommandID commandID, Applica "Shows the panel containing the tree of files for this project", CommandCategories::general, 0); result.setActive (project != nullptr); - result.defaultKeypresses.add (KeyPress ('f', ModifierKeys::commandModifier, 0)); + result.defaultKeypresses.add (KeyPress ('f', ModifierKeys::commandModifier | ModifierKeys::ctrlModifier, 0)); break; case CommandIDs::showModulesPanel: @@ -1013,7 +1014,7 @@ void ProjectContentComponent::getCommandInfo (const CommandID commandID, Applica "Shows the panel containing the project's list of modules", CommandCategories::general, 0); result.setActive (project != nullptr); - result.defaultKeypresses.add (KeyPress ('m', ModifierKeys::commandModifier, 0)); + result.defaultKeypresses.add (KeyPress ('m', ModifierKeys::commandModifier | ModifierKeys::ctrlModifier, 0)); break; case CommandIDs::showExportersPanel: @@ -1021,7 +1022,7 @@ void ProjectContentComponent::getCommandInfo (const CommandID commandID, Applica "Shows the panel containing the project's list of exporters", CommandCategories::general, 0); result.setActive (project != nullptr); - result.defaultKeypresses.add (KeyPress ('e', ModifierKeys::commandModifier, 0)); + result.defaultKeypresses.add (KeyPress ('e', ModifierKeys::commandModifier | ModifierKeys::ctrlModifier, 0)); break; case CommandIDs::showExporterSettings: diff --git a/extras/Projucer/Source/Project/jucer_ProjectTab.h b/extras/Projucer/Source/Project/jucer_ProjectTab.h index f3e1b1578e..161c594788 100644 --- a/extras/Projucer/Source/Project/jucer_ProjectTab.h +++ b/extras/Projucer/Source/Project/jucer_ProjectTab.h @@ -182,6 +182,12 @@ struct FileTreePanel : public TreePanelBase if (auto* p = dynamic_cast (rootItem.get())) p->checkFileStatus(); } + + void setSearchFilter (const String& filter) + { + if (auto* p = dynamic_cast (rootItem.get())) + p->setSearchFilter (filter); + } }; struct ModuleTreePanel : public TreePanelBase @@ -216,12 +222,12 @@ struct ExportersTreePanel : public TreePanelBase //============================================================================== class ConcertinaTreeComponent : public Component, - private Button::Listener + private Button::Listener, + private ChangeListener { public: - ConcertinaTreeComponent (TreePanelBase* tree, bool showSettings = false) - : treeToDisplay (tree), - showSettingsButton (showSettings) + ConcertinaTreeComponent (TreePanelBase* tree, bool showSettingsButton = false, bool showFindPanel = false) + : treeToDisplay (tree) { addAndMakeVisible (popupMenuButton = new IconButton ("Add", &getIcons().plus)); popupMenuButton->addListener (this); @@ -232,6 +238,12 @@ public: settingsButton->addListener (this); } + if (showFindPanel) + { + addAndMakeVisible (findPanel = new FindPanel()); + findPanel->addChangeListener (this); + } + addAndMakeVisible (treeToDisplay); } @@ -249,18 +261,26 @@ public: bottomSlice.removeFromRight (5); popupMenuButton->setBounds (bottomSlice.removeFromRight (25).reduced (2)); - if (showSettingsButton) - settingsButton->setBounds (bottomSlice.removeFromRight(25).reduced (2)); + if (settingsButton != nullptr) + settingsButton->setBounds (bottomSlice.removeFromRight (25).reduced (2)); + + if (findPanel != nullptr) + findPanel->setBounds (bottomSlice.reduced (2)); treeToDisplay->setBounds (bounds); } TreePanelBase* getTree() const noexcept { return treeToDisplay.get(); } + void grabFindFocus() + { + if (findPanel != nullptr) + findPanel->grabKeyboardFocus(); + } + private: ScopedPointer treeToDisplay; ScopedPointer popupMenuButton, settingsButton; - bool showSettingsButton; void buttonClicked (Button* b) override { @@ -296,11 +316,93 @@ private: } } + void changeListenerCallback (ChangeBroadcaster* source) override + { + if (source == findPanel) + if (auto* fileTree = dynamic_cast (treeToDisplay.get())) + fileTree->setSearchFilter (findPanel->editor.getText()); + } + + class FindPanel : public Component, + public ChangeBroadcaster, + private TextEditor::Listener, + private Timer, + private FocusChangeListener + { + public: + FindPanel() + { + addAndMakeVisible (editor); + editor.addListener (this); + + Desktop::getInstance().addFocusChangeListener (this); + + lookAndFeelChanged(); + } + + void paintOverChildren (Graphics& g) override + { + if (! isFocused) + return; + + g.setColour (findColour (defaultHighlightColourId)); + + Path p; + p.addRoundedRectangle (getLocalBounds().reduced (2), 3.0f); + g.strokePath (p, PathStrokeType (2.0f)); + } + + + void resized() override + { + editor.setBounds (getLocalBounds().reduced (2)); + } + + TextEditor editor; + + private: + + void lookAndFeelChanged() override + { + editor.setTextToShowWhenEmpty ("Filter...", findColour (widgetTextColourId).withAlpha (0.3f)); + } + + void textEditorTextChanged (TextEditor&) override + { + startTimer (250); + } + + void textEditorFocusLost (TextEditor&) override + { + isFocused = false; + repaint(); + } + + void globalFocusChanged (Component* focusedComponent) override + { + if (focusedComponent == &editor) + { + isFocused = true; + repaint(); + } + } + + void timerCallback() override + { + stopTimer(); + sendChangeMessage(); + } + + bool isFocused = false; + }; + ScopedPointer findPanel; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConcertinaTreeComponent) }; //============================================================================== class ProjectTab : public Component, + public ApplicationCommandTarget, private ChangeListener { public: @@ -389,6 +491,47 @@ public: return ((float) (concertinaPanel.getPanel (panelIndex)->getHeight()) / (concertinaPanel.getHeight() - 90)); } + //============================================================================== + ApplicationCommandTarget* getNextCommandTarget() override { return nullptr; } + + void getAllCommands (Array & commands) override + { + const CommandID ids[] = { CommandIDs::showFindPanel }; + + commands.addArray (ids, numElementsInArray (ids)); + } + + void getCommandInfo (CommandID commandID, ApplicationCommandInfo& result) override + { + switch (commandID) + { + case CommandIDs::showFindPanel: + result.setInfo (TRANS ("Find"), TRANS ("Searches for ."), "Editing", 0); + result.defaultKeypresses.add (KeyPress ('f', ModifierKeys::commandModifier, 0)); + break; + + default: + break; + } + } + + bool perform (const InvocationInfo& info) override + { + switch (info.commandID) + { + case CommandIDs::showFindPanel: + { + find(); + return true; + } + + default: + return false; + } + + return true; + } + private: ConcertinaPanel concertinaPanel; @@ -414,7 +557,7 @@ private: if (project != nullptr) { - concertinaPanel.addPanel (0, new ConcertinaTreeComponent (new FileTreePanel (*project)), true); + concertinaPanel.addPanel (0, new ConcertinaTreeComponent (new FileTreePanel (*project), false, true), true); concertinaPanel.addPanel (1, new ConcertinaTreeComponent (new ModuleTreePanel (*project), true), true); concertinaPanel.addPanel (2, new ConcertinaTreeComponent (new ExportersTreePanel (*project)), true); } @@ -456,5 +599,13 @@ private: } } + void find() + { + showPanel (0); + + if (auto* treeComponent = dynamic_cast (concertinaPanel.getPanel (0))) + treeComponent->grabFindFocus(); + } + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectTab) }; diff --git a/extras/Projucer/Source/Project/jucer_ProjectTree_Group.h b/extras/Projucer/Source/Project/jucer_ProjectTree_Group.h index 18beb92356..bcde2ee5f8 100644 --- a/extras/Projucer/Source/Project/jucer_ProjectTree_Group.h +++ b/extras/Projucer/Source/Project/jucer_ProjectTree_Group.h @@ -27,8 +27,9 @@ class GroupItem : public ProjectTreeItemBase { public: - GroupItem (const Project::Item& projectItem) - : ProjectTreeItemBase (projectItem) + GroupItem (const Project::Item& projectItem, const String& filter = String()) + : ProjectTreeItemBase (projectItem), + searchFilter (filter) { } @@ -79,10 +80,40 @@ public: p->checkFileStatus(); } + bool isGroupEmpty (const Project::Item& group) // recursive + { + bool isEmpty = true; + + for (auto i = 0; i < group.getNumChildren(); ++i) + { + auto child = group.getChild (i); + + if (child.isGroup()) + isEmpty = isGroupEmpty (child); + else if (child.isFile() && child.getName().containsIgnoreCase (searchFilter)) + isEmpty = false; + } + + return isEmpty; + } + ProjectTreeItemBase* createSubItem (const Project::Item& child) override { - if (child.isGroup()) return new GroupItem (child); - if (child.isFile()) return new SourceFileItem (child); + if (child.isGroup()) + { + if (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; @@ -94,12 +125,26 @@ public: pcc->setEditorComponent (new GroupInformationComponent (item), nullptr); } + static void openAllGroups (TreeViewItem* root) + { + for (auto i = 0; i < root->getNumSubItems(); ++i) + if (auto* sub = root->getSubItem (i)) + openOrCloseAllSubGroups (*sub, true); + } + + static void closeAllGroups (TreeViewItem* root) + { + for (auto i = 0; i < root->getNumSubItems(); ++i) + if (auto* sub = root->getSubItem (i)) + openOrCloseAllSubGroups (*sub, false); + } + static void openOrCloseAllSubGroups (TreeViewItem& item, bool shouldOpen) { item.setOpen (shouldOpen); for (int i = item.getNumSubItems(); --i >= 0;) - if (TreeViewItem* sub = item.getSubItem(i)) + if (auto* sub = item.getSubItem (i)) openOrCloseAllSubGroups (*sub, shouldOpen); } @@ -119,27 +164,30 @@ public: m.addSeparator(); + m.addItem (1, "Collapse all Groups"); + m.addItem (2, "Expand all Groups"); + if (! isRoot()) { if (isOpen()) - m.addItem (1, "Collapse all Sub-groups"); + m.addItem (3, "Collapse all Sub-groups"); else - m.addItem (2, "Expand all Sub-groups"); + m.addItem (4, "Expand all Sub-groups"); } m.addSeparator(); - m.addItem (3, "Enable compiling of all enclosed files"); - m.addItem (4, "Disable compiling of all enclosed files"); + m.addItem (5, "Enable compiling of all enclosed files"); + m.addItem (6, "Disable compiling of all enclosed files"); m.addSeparator(); - m.addItem (5, "Sort Items Alphabetically"); - m.addItem (6, "Sort Items Alphabetically (Groups first)"); + m.addItem (7, "Sort Items Alphabetically"); + m.addItem (8, "Sort Items Alphabetically (Groups first)"); m.addSeparator(); if (! isRoot()) { - m.addItem (7, "Rename..."); - m.addItem (8, "Delete"); + m.addItem (9, "Rename..."); + m.addItem (10, "Delete"); } launchPopupMenu (m); @@ -157,14 +205,16 @@ public: { switch (resultCode) { - case 1: openOrCloseAllSubGroups (*this, false); break; - case 2: openOrCloseAllSubGroups (*this, true); break; - case 3: setFilesToCompile (item, true); break; - case 4: setFilesToCompile (item, false); break; - case 5: item.sortAlphabetically (false, false); break; - case 6: item.sortAlphabetically (true, false); break; - case 7: triggerAsyncRename (item); break; - case 8: deleteAllSelectedItems(); break; + 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; } } @@ -200,4 +250,12 @@ public: return nullptr; } + + void setSearchFilter (const String& filter) + { + searchFilter = filter; + refreshSubItems(); + } + + String searchFilter; };