/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited 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 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-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 ConcertinaHeader final : public Component, public ChangeBroadcaster { public: ConcertinaHeader (String n, Path p) : Component (n), name (n), iconPath (p) { setTitle (getName()); panelIcon = Icon (iconPath, Colours::white); nameLabel.setText (name, dontSendNotification); nameLabel.setJustificationType (Justification::centredLeft); nameLabel.setInterceptsMouseClicks (false, false); nameLabel.setAccessible (false); nameLabel.setColour (Label::textColourId, Colours::white); addAndMakeVisible (nameLabel); } void resized() override { auto b = getLocalBounds().toFloat(); iconBounds = b.removeFromLeft (b.getHeight()).reduced (7, 7); arrowBounds = b.removeFromRight (b.getHeight()); nameLabel.setBounds (b.toNearestInt()); } void paint (Graphics& g) override { g.setColour (findColour (defaultButtonBackgroundColourId)); g.fillRoundedRectangle (getLocalBounds().reduced (2, 3).toFloat(), 2.0f); g.setColour (Colours::white); g.fillPath (ProjucerLookAndFeel::getArrowPath (arrowBounds, getParentComponent()->getBoundsInParent().getY() == yPosition ? 2 : 0, true, Justification::centred)); panelIcon.draw (g, iconBounds.toFloat(), false); } void mouseUp (const MouseEvent& e) override { if (! e.mouseWasDraggedSinceMouseDown()) sendChangeMessage(); } std::unique_ptr createAccessibilityHandler() override { return std::make_unique (*this, AccessibilityRole::button, AccessibilityActions().addAction (AccessibilityActionType::press, [this] { sendChangeMessage(); })); } int direction = 0; int yPosition = 0; private: String name; Label nameLabel; Path iconPath; Icon panelIcon; Rectangle arrowBounds, iconBounds; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConcertinaHeader) }; //============================================================================== class FindPanel final : public Component, private Timer, private FocusChangeListener { public: FindPanel (std::function cb) : callback (cb) { addAndMakeVisible (editor); editor.onTextChange = [this] { startTimer (250); }; editor.onFocusLost = [this] { isFocused = false; repaint(); }; Desktop::getInstance().addFocusChangeListener (this); lookAndFeelChanged(); } ~FindPanel() override { Desktop::getInstance().removeFocusChangeListener (this); } 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)); } private: TextEditor editor; bool isFocused = false; std::function callback; //============================================================================== void lookAndFeelChanged() override { editor.setTextToShowWhenEmpty ("Filter...", findColour (widgetTextColourId).withAlpha (0.3f)); } void globalFocusChanged (Component* focusedComponent) override { if (focusedComponent == &editor) { isFocused = true; repaint(); } } void timerCallback() override { stopTimer(); callback (editor.getText()); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FindPanel) }; //============================================================================== class ConcertinaTreeComponent final : public Component { public: class AdditionalComponents { public: enum Type { addButton = (1 << 0), settingsButton = (1 << 1), findPanel = (1 << 2) }; [[nodiscard]] AdditionalComponents with (Type t) { auto copy = *this; copy.componentTypes |= t; return copy; } bool has (Type t) const noexcept { return (componentTypes & t) != 0; } private: int componentTypes = 0; }; ConcertinaTreeComponent (const String& name, TreePanelBase* tree, AdditionalComponents additionalComponents) : Component (name), treeToDisplay (tree) { setTitle (getName()); setFocusContainerType (FocusContainerType::focusContainer); if (additionalComponents.has (AdditionalComponents::addButton)) { addButton = std::make_unique ("Add", getIcons().plus); addAndMakeVisible (addButton.get()); addButton->onClick = [this] { showAddMenu(); }; } if (additionalComponents.has (AdditionalComponents::settingsButton)) { settingsButton = std::make_unique ("Settings", getIcons().settings); addAndMakeVisible (settingsButton.get()); settingsButton->onClick = [this] { showSettings(); }; } if (additionalComponents.has (AdditionalComponents::findPanel)) { findPanel = std::make_unique ([this] (const String& filter) { treeToDisplay->rootItem->setSearchFilter (filter); }); addAndMakeVisible (findPanel.get()); } addAndMakeVisible (treeToDisplay.get()); } void resized() override { auto bounds = getLocalBounds(); if (addButton != nullptr || settingsButton != nullptr || findPanel != nullptr) { auto bottomSlice = bounds.removeFromBottom (25); bottomSlice.removeFromRight (3); if (addButton != nullptr) addButton->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(); } private: std::unique_ptr treeToDisplay; std::unique_ptr addButton, settingsButton; std::unique_ptr findPanel; void showAddMenu() { auto numSelected = treeToDisplay->tree.getNumSelectedItems(); if (numSelected > 1) return; if (numSelected == 0) { if (auto* root = dynamic_cast (treeToDisplay->tree.getRootItem())) root->showPopupMenu (addButton->getScreenBounds().getCentre()); } else { if (auto* item = dynamic_cast (treeToDisplay->tree.getSelectedItem (0))) item->showAddMenu (addButton->getScreenBounds().getCentre()); } } void showSettings() { if (auto* root = dynamic_cast (treeToDisplay->tree.getRootItem())) { treeToDisplay->tree.clearSelectedItems(); root->showDocument(); } } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConcertinaTreeComponent) }; //============================================================================== struct ProjectSettingsComponent final : public Component, private ChangeListener { ProjectSettingsComponent (Project& p) : project (p), group (project.getProjectFilenameRootString(), Icon (getIcons().settings, Colours::transparentBlack)) { setTitle ("Project Settings"); setFocusContainerType (FocusContainerType::focusContainer); addAndMakeVisible (group); updatePropertyList(); project.addChangeListener (this); } ~ProjectSettingsComponent() override { project.removeChangeListener (this); } void resized() override { group.updateSize (12, 0, getWidth() - 24); group.setBounds (getLocalBounds().reduced (12, 0)); } void updatePropertyList() { PropertyListBuilder props; project.createPropertyEditors (props); group.setProperties (props); group.setName ("Project Settings"); lastProjectType = project.getProjectTypeString(); parentSizeChanged(); } void changeListenerCallback (ChangeBroadcaster*) override { if (lastProjectType != project.getProjectTypeString()) updatePropertyList(); } void parentSizeChanged() override { auto width = jmax (550, getParentWidth()); auto y = group.updateSize (12, 0, width - 12); y = jmax (getParentHeight(), y); setSize (width, y); } Project& project; var lastProjectType; PropertyGroupComponent group; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectSettingsComponent) }; //============================================================================== struct FileTreePanel final : public TreePanelBase { FileTreePanel (Project& p) : TreePanelBase (&p, "fileTreeState") { tree.setMultiSelectEnabled (true); setRoot (std::make_unique (p.getMainGroup())); tree.setRootItemVisible (false); } void updateMissingFileStatuses() { if (auto* p = dynamic_cast (rootItem.get())) p->checkFileStatus(); } }; struct ModuleTreePanel final : public TreePanelBase { ModuleTreePanel (Project& p) : TreePanelBase (&p, "moduleTreeState") { tree.setMultiSelectEnabled (false); setRoot (std::make_unique (p)); tree.setRootItemVisible (false); } }; struct ExportersTreePanel final : public TreePanelBase { ExportersTreePanel (Project& p) : TreePanelBase (&p, "exportersTreeState") { tree.setMultiSelectEnabled (false); setRoot (std::make_unique (p)); tree.setRootItemVisible (false); } }; //============================================================================== class Sidebar final : public Component, private ChangeListener { public: Sidebar (Project* p) : project (p) { setFocusContainerType (FocusContainerType::focusContainer); if (project != nullptr) buildConcertina(); } ~Sidebar() override { TreePanelBase* panels[] = { getFileTreePanel(), getModuleTreePanel(), getExportersTreePanel() }; for (auto* panel : panels) if (panel != nullptr) panel->saveOpenness(); } void paint (Graphics& g) override { g.fillAll (findColour (secondaryBackgroundColourId)); } void resized() override { concertinaPanel.setBounds (getLocalBounds().withTrimmedBottom (3)); } TreePanelBase* getTreeWithSelectedItems() { for (auto i = concertinaPanel.getNumPanels() - 1; i >= 0; --i) { if (auto* treeComponent = dynamic_cast (concertinaPanel.getPanel (i))) { if (auto* base = treeComponent->getTree()) if (base->tree.getNumSelectedItems() != 0) return base; } } return nullptr; } FileTreePanel* getFileTreePanel() { return getPanel (0); } ModuleTreePanel* getModuleTreePanel() { return getPanel (1); } ExportersTreePanel* getExportersTreePanel() { return getPanel (2); } void showPanel (int panelIndex) { jassert (isPositiveAndBelow (panelIndex, concertinaPanel.getNumPanels())); concertinaPanel.expandPanelFully (concertinaPanel.getPanel (panelIndex), true); } private: //============================================================================== template PanelType* getPanel (int panelIndex) { if (auto* panel = dynamic_cast (concertinaPanel.getPanel (panelIndex))) return dynamic_cast (panel->getTree()); return nullptr; } void changeListenerCallback (ChangeBroadcaster* source) override { const auto pointerMatches = [source] (const std::unique_ptr& header) { return header.get() == source; }; const auto it = std::find_if (headers.begin(), headers.end(), pointerMatches); const auto index = (int) std::distance (headers.begin(), it); if (index != (int) headers.size()) concertinaPanel.expandPanelFully (concertinaPanel.getPanel (index), true); } void buildConcertina() { for (auto i = concertinaPanel.getNumPanels() - 1; i >= 0 ; --i) concertinaPanel.removePanel (concertinaPanel.getPanel (i)); headers.clear(); auto addPanel = [this] (const String& name, TreePanelBase* tree, ConcertinaTreeComponent::AdditionalComponents components, const Path& icon) { if (project != nullptr) concertinaPanel.addPanel (-1, new ConcertinaTreeComponent (name, tree, components), true); headers.push_back (std::make_unique (name, icon)); }; using AdditionalComponents = ConcertinaTreeComponent::AdditionalComponents; addPanel ("File Explorer", new FileTreePanel (*project), AdditionalComponents{} .with (AdditionalComponents::addButton) .with (AdditionalComponents::findPanel), getIcons().fileExplorer); addPanel ("Modules", new ModuleTreePanel (*project), AdditionalComponents{} .with (AdditionalComponents::addButton) .with (AdditionalComponents::settingsButton), getIcons().modules); addPanel ("Exporters", new ExportersTreePanel (*project), AdditionalComponents{}.with (AdditionalComponents::addButton), getIcons().exporter); for (int i = 0; i < concertinaPanel.getNumPanels(); ++i) { auto* p = concertinaPanel.getPanel (i); auto* h = headers[(size_t) i].get(); p->addMouseListener (this, true); h->addChangeListener (this); h->yPosition = i * 30; concertinaPanel.setCustomPanelHeader (p, h, false); concertinaPanel.setPanelHeaderSize (p, 30); } addAndMakeVisible (concertinaPanel); } void mouseDown (const MouseEvent& e) override { for (auto i = concertinaPanel.getNumPanels() - 1; i >= 0; --i) { if (auto* p = concertinaPanel.getPanel (i)) { if (! (p->isParentOf (e.eventComponent))) { auto* base = dynamic_cast (p); if (base == nullptr) if (auto* concertina = dynamic_cast (p)) base = concertina->getTree(); if (base != nullptr) base->tree.clearSelectedItems(); } } } } //============================================================================== ConcertinaPanel concertinaPanel; std::vector> headers; Project* project = nullptr; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Sidebar) };