/* ============================================================================== This file is part of the JUCE 7 technical preview. Copyright (c) 2022 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For the technical preview this file cannot be licensed commercially. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { struct PropertyPanel::SectionComponent : public Component { SectionComponent (const String& sectionTitle, const Array& newProperties, bool sectionIsOpen, int extraPadding) : Component (sectionTitle), isOpen (sectionIsOpen), padding (extraPadding) { lookAndFeelChanged(); propertyComps.addArray (newProperties); for (auto* propertyComponent : propertyComps) { addAndMakeVisible (propertyComponent); propertyComponent->refresh(); } } ~SectionComponent() override { propertyComps.clear(); } void paint (Graphics& g) override { if (titleHeight > 0) getLookAndFeel().drawPropertyPanelSectionHeader (g, getName(), isOpen, getWidth(), titleHeight); } void resized() override { auto y = titleHeight; for (auto* propertyComponent : propertyComps) { propertyComponent->setBounds (1, y, getWidth() - 2, propertyComponent->getPreferredHeight()); y = propertyComponent->getBottom() + padding; } } void lookAndFeelChanged() override { titleHeight = getLookAndFeel().getPropertyPanelSectionHeaderHeight (getName()); resized(); repaint(); } int getPreferredHeight() const { auto y = titleHeight; auto numComponents = propertyComps.size(); if (numComponents > 0 && isOpen) { for (auto* propertyComponent : propertyComps) y += propertyComponent->getPreferredHeight(); y += (numComponents - 1) * padding; } return y; } void setOpen (bool open) { if (isOpen != open) { isOpen = open; for (auto* propertyComponent : propertyComps) propertyComponent->setVisible (open); if (auto* propertyPanel = findParentComponentOfClass()) propertyPanel->resized(); } } void refreshAll() const { for (auto* propertyComponent : propertyComps) propertyComponent->refresh(); } void mouseUp (const MouseEvent& e) override { if (e.getMouseDownX() < titleHeight && e.x < titleHeight && e.getNumberOfClicks() != 2) mouseDoubleClick (e); } void mouseDoubleClick (const MouseEvent& e) override { if (e.y < titleHeight) setOpen (! isOpen); } OwnedArray propertyComps; int titleHeight; bool isOpen; int padding; JUCE_DECLARE_NON_COPYABLE (SectionComponent) }; //============================================================================== struct PropertyPanel::PropertyHolderComponent : public Component { PropertyHolderComponent() {} void paint (Graphics&) override {} void updateLayout (int width) { auto y = 0; for (auto* section : sections) { section->setBounds (0, y, width, section->getPreferredHeight()); y = section->getBottom(); } setSize (width, y); repaint(); } void refreshAll() const { for (auto* section : sections) section->refreshAll(); } void insertSection (int indexToInsertAt, SectionComponent* newSection) { sections.insert (indexToInsertAt, newSection); addAndMakeVisible (newSection, 0); } SectionComponent* getSectionWithNonEmptyName (int targetIndex) const noexcept { auto index = 0; for (auto* section : sections) { if (section->getName().isNotEmpty()) if (index++ == targetIndex) return section; } return nullptr; } OwnedArray sections; JUCE_DECLARE_NON_COPYABLE (PropertyHolderComponent) }; //============================================================================== PropertyPanel::PropertyPanel() { init(); } PropertyPanel::PropertyPanel (const String& name) : Component (name) { init(); } void PropertyPanel::init() { messageWhenEmpty = TRANS("(nothing selected)"); addAndMakeVisible (viewport); viewport.setViewedComponent (propertyHolderComponent = new PropertyHolderComponent()); viewport.setFocusContainerType (FocusContainerType::keyboardFocusContainer); } PropertyPanel::~PropertyPanel() { clear(); } //============================================================================== void PropertyPanel::paint (Graphics& g) { if (isEmpty()) { g.setColour (Colours::black.withAlpha (0.5f)); g.setFont (14.0f); g.drawText (messageWhenEmpty, getLocalBounds().withHeight (30), Justification::centred, true); } } void PropertyPanel::resized() { viewport.setBounds (getLocalBounds()); updatePropHolderLayout(); } //============================================================================== void PropertyPanel::clear() { if (! isEmpty()) { propertyHolderComponent->sections.clear(); updatePropHolderLayout(); } } bool PropertyPanel::isEmpty() const { return propertyHolderComponent->sections.size() == 0; } int PropertyPanel::getTotalContentHeight() const { return propertyHolderComponent->getHeight(); } void PropertyPanel::addProperties (const Array& newProperties, int extraPaddingBetweenComponents) { if (isEmpty()) repaint(); propertyHolderComponent->insertSection (-1, new SectionComponent ({}, newProperties, true, extraPaddingBetweenComponents)); updatePropHolderLayout(); } void PropertyPanel::addSection (const String& sectionTitle, const Array& newProperties, bool shouldBeOpen, int indexToInsertAt, int extraPaddingBetweenComponents) { jassert (sectionTitle.isNotEmpty()); if (isEmpty()) repaint(); propertyHolderComponent->insertSection (indexToInsertAt, new SectionComponent (sectionTitle, newProperties, shouldBeOpen, extraPaddingBetweenComponents)); updatePropHolderLayout(); } void PropertyPanel::updatePropHolderLayout() const { auto maxWidth = viewport.getMaximumVisibleWidth(); propertyHolderComponent->updateLayout (maxWidth); auto newMaxWidth = viewport.getMaximumVisibleWidth(); if (maxWidth != newMaxWidth) { // need to do this twice because of scrollbars changing the size, etc. propertyHolderComponent->updateLayout (newMaxWidth); } } void PropertyPanel::refreshAll() const { propertyHolderComponent->refreshAll(); } //============================================================================== StringArray PropertyPanel::getSectionNames() const { StringArray s; for (auto* section : propertyHolderComponent->sections) { if (section->getName().isNotEmpty()) s.add (section->getName()); } return s; } bool PropertyPanel::isSectionOpen (int sectionIndex) const { if (auto* s = propertyHolderComponent->getSectionWithNonEmptyName (sectionIndex)) return s->isOpen; return false; } void PropertyPanel::setSectionOpen (int sectionIndex, bool shouldBeOpen) { if (auto* s = propertyHolderComponent->getSectionWithNonEmptyName (sectionIndex)) s->setOpen (shouldBeOpen); } void PropertyPanel::setSectionEnabled (int sectionIndex, bool shouldBeEnabled) { if (auto* s = propertyHolderComponent->getSectionWithNonEmptyName (sectionIndex)) s->setEnabled (shouldBeEnabled); } void PropertyPanel::removeSection (int sectionIndex) { if (auto* s = propertyHolderComponent->getSectionWithNonEmptyName (sectionIndex)) { propertyHolderComponent->sections.removeObject (s); updatePropHolderLayout(); } } //============================================================================== std::unique_ptr PropertyPanel::getOpennessState() const { auto xml = std::make_unique ("PROPERTYPANELSTATE"); xml->setAttribute ("scrollPos", viewport.getViewPositionY()); auto sections = getSectionNames(); for (auto s : sections) { if (s.isNotEmpty()) { auto* e = xml->createNewChildElement ("SECTION"); e->setAttribute ("name", s); e->setAttribute ("open", isSectionOpen (sections.indexOf (s)) ? 1 : 0); } } return xml; } void PropertyPanel::restoreOpennessState (const XmlElement& xml) { if (xml.hasTagName ("PROPERTYPANELSTATE")) { auto sections = getSectionNames(); for (auto* e : xml.getChildWithTagNameIterator ("SECTION")) { setSectionOpen (sections.indexOf (e->getStringAttribute ("name")), e->getBoolAttribute ("open")); } viewport.setViewPosition (viewport.getViewPositionX(), xml.getIntAttribute ("scrollPos", viewport.getViewPositionY())); } } //============================================================================== void PropertyPanel::setMessageWhenEmpty (const String& newMessage) { if (messageWhenEmpty != newMessage) { messageWhenEmpty = newMessage; repaint(); } } const String& PropertyPanel::getMessageWhenEmpty() const noexcept { return messageWhenEmpty; } } // namespace juce