From 2462a7978ef0d1e773b742f0bed2f194dbef6329 Mon Sep 17 00:00:00 2001 From: James Ball Date: Tue, 11 Jul 2023 18:48:45 +0100 Subject: [PATCH] Add variable-size listboxes for echo effect --- Source/EffectsComponent.cpp | 1 - Source/components/ComponentList.cpp | 15 + Source/components/ComponentList.h | 38 + Source/components/DraggableListBox.h | 5 +- Source/components/EffectComponent.cpp | 12 +- Source/components/EffectComponent.h | 2 + Source/components/EffectsListComponent.cpp | 69 +- Source/components/EffectsListComponent.h | 26 +- Source/components/VListBox.cpp | 1069 ++++++++++++++++++++ Source/components/VListBox.h | 622 ++++++++++++ osci-render.jucer | 5 + 11 files changed, 1817 insertions(+), 47 deletions(-) create mode 100644 Source/components/ComponentList.cpp create mode 100644 Source/components/ComponentList.h create mode 100644 Source/components/VListBox.cpp create mode 100644 Source/components/VListBox.h diff --git a/Source/EffectsComponent.cpp b/Source/EffectsComponent.cpp index c19ff95..99e3a56 100644 --- a/Source/EffectsComponent.cpp +++ b/Source/EffectsComponent.cpp @@ -37,7 +37,6 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p) : audioProcessor addAndMakeVisible(addBtn);*/ listBox.setModel(&listBoxModel); - listBox.setRowHeight(30); addAndMakeVisible(listBox); } diff --git a/Source/components/ComponentList.cpp b/Source/components/ComponentList.cpp new file mode 100644 index 0000000..456f684 --- /dev/null +++ b/Source/components/ComponentList.cpp @@ -0,0 +1,15 @@ +#include "ComponentList.h" + +int ComponentListModel::getNumRows() { + return components.size(); +} + +void ComponentListModel::paintListBoxItem(int rowNumber, juce::Graphics& g, int width, int height, bool rowIsSelected) {} + +juce::Component* ComponentListModel::refreshComponentForRow(int rowNum, bool isRowSelected, juce::Component *existingComponentToUpdate) { + std::unique_ptr item(dynamic_cast(existingComponentToUpdate)); + if (juce::isPositiveAndBelow(rowNum, getNumRows())) { + item = std::make_unique(components[rowNum]); + } + return item.release(); +} diff --git a/Source/components/ComponentList.h b/Source/components/ComponentList.h new file mode 100644 index 0000000..8f23a6d --- /dev/null +++ b/Source/components/ComponentList.h @@ -0,0 +1,38 @@ +#pragma once +#include + +// This class is a wrapper for a component that allows it to be used in a ListBox +// Why is this needed?!?!?!?! +class ComponentWrapper : public juce::Component { + public: + ComponentWrapper(std::shared_ptr component) : component(component) { + addAndMakeVisible(component.get()); + } + + ~ComponentWrapper() override {} + + void resized() override { + component->setBounds(getLocalBounds()); + } + +private: + std::shared_ptr component; +}; + +class ComponentListModel : public juce::ListBoxModel +{ +public: + ComponentListModel() {} + ~ComponentListModel() override {} + + int getNumRows() override; + void paintListBoxItem(int rowNumber, juce::Graphics& g, int width, int height, bool rowIsSelected) override; + juce::Component* refreshComponentForRow(int sliderNum, bool isRowSelected, juce::Component *existingComponentToUpdate) override; + + void addComponent(std::shared_ptr component) { + components.push_back(component); + } + +private: + std::vector> components; +}; diff --git a/Source/components/DraggableListBox.h b/Source/components/DraggableListBox.h index 17f907f..f21dff9 100644 --- a/Source/components/DraggableListBox.h +++ b/Source/components/DraggableListBox.h @@ -1,5 +1,6 @@ #pragma once #include "JuceHeader.h" +#include "VListBox.h" // Your item-data container must inherit from this, and override at least the first // four member functions. @@ -19,7 +20,7 @@ struct DraggableListBoxItemData // DraggableListBox is basically just a ListBox, that inherits from DragAndDropContainer. // Declare your list box using this type. -class DraggableListBox : public juce::ListBox, public juce::DragAndDropContainer +class DraggableListBox : public juce::jc::ListBox, public juce::DragAndDropContainer { }; @@ -58,7 +59,7 @@ protected: bool insertBefore = false; }; -class DraggableListBoxModel : public juce::ListBoxModel +class DraggableListBoxModel : public juce::jc::ListBoxModel { public: DraggableListBoxModel(DraggableListBox& lb, DraggableListBoxItemData& md) diff --git a/Source/components/EffectComponent.cpp b/Source/components/EffectComponent.cpp index a9b7519..10ee897 100644 --- a/Source/components/EffectComponent.cpp +++ b/Source/components/EffectComponent.cpp @@ -6,16 +6,22 @@ EffectComponent::EffectComponent(double min, double max, double step, double val slider.setValue(value, juce::dontSendNotification); } -EffectComponent::EffectComponent(double min, double max, double step, Effect& effect) : name(effect.getName()), id(effect.getId()) { +EffectComponent::EffectComponent(double min, double max, double step, EffectDetails details) : name(details.name), id(details.id) { componentSetup(); slider.setRange(min, max, step); - slider.setValue(effect.getValue(), juce::dontSendNotification); + slider.setValue(details.value, juce::dontSendNotification); } -EffectComponent::EffectComponent(double min, double max, double step, Effect& effect, bool checkboxVisible) : EffectComponent(min, max, step, effect) { +EffectComponent::EffectComponent(double min, double max, double step, EffectDetails details, bool checkboxVisible) : EffectComponent(min, max, step, details) { setCheckboxVisible(checkboxVisible); } +EffectComponent::EffectComponent(double min, double max, double step, Effect& effect) : EffectComponent(min, max, step, effect.getDetails()[0]) {} + +EffectComponent::EffectComponent(double min, double max, double step, Effect& effect, bool checkboxVisible) : EffectComponent(min, max, step, effect) { + setCheckboxVisible(checkboxVisible); +} + void EffectComponent::componentSetup() { addAndMakeVisible(slider); addAndMakeVisible(selected); diff --git a/Source/components/EffectComponent.h b/Source/components/EffectComponent.h index 015dc2e..5186e94 100644 --- a/Source/components/EffectComponent.h +++ b/Source/components/EffectComponent.h @@ -7,6 +7,8 @@ class EffectComponent : public juce::Component { public: EffectComponent(double min, double max, double step, double value, juce::String name, juce::String id); + EffectComponent(double min, double max, double step, EffectDetails details); + EffectComponent(double min, double max, double step, EffectDetails details, bool checkboxVisible); EffectComponent(double min, double max, double step, Effect& effect); EffectComponent(double min, double max, double step, Effect& effect, bool checkboxVisible); ~EffectComponent(); diff --git a/Source/components/EffectsListComponent.cpp b/Source/components/EffectsListComponent.cpp index de279fc..791b2c6 100644 --- a/Source/components/EffectsListComponent.cpp +++ b/Source/components/EffectsListComponent.cpp @@ -1,31 +1,42 @@ #include "EffectsListComponent.h" -EffectsListComponent::EffectsListComponent(DraggableListBox& lb, AudioEffectListBoxItemData& data, int rn, std::shared_ptr effectComponent) : DraggableListBoxItem(lb, data, rn), effectComponent(effectComponent) { - addAndMakeVisible(*effectComponent); +EffectsListComponent::EffectsListComponent(DraggableListBox& lb, AudioEffectListBoxItemData& data, int rn, std::shared_ptr effect) : DraggableListBoxItem(lb, data, rn), effect(effect) { + auto details = effect->getDetails(); + for (int i = 0; i < details.size(); i++) { + std::shared_ptr effectComponent = std::make_shared(0, 1, 0.01, details[i], i == 0); + effectComponent->slider.setValue(details[i].value, juce::dontSendNotification); + effectComponent->slider.onValueChange = [this, i, effectComponent] { + this->effect->setValue(i, effectComponent->slider.getValue()); + }; - effectComponent->slider.setValue(data.getValue(rn), juce::dontSendNotification); - effectComponent->slider.onValueChange = [this] { - ((AudioEffectListBoxItemData&)modelData).setValue(rowNum, this->effectComponent->slider.getValue()); - }; + if (i == 0) { + bool isSelected = false; - bool isSelected = false; + { + juce::SpinLock::ScopedLockType lock(data.audioProcessor.effectsLock); + // check if effect is in audioProcessor enabled effects + for (auto processorEffect : data.audioProcessor.enabledEffects) { + if (processorEffect->getId() == effect->getId()) { + isSelected = true; + break; + } + } + } + effectComponent->selected.setToggleState(isSelected, juce::dontSendNotification); + effectComponent->selected.onClick = [this, effectComponent] { + auto data = (AudioEffectListBoxItemData&)modelData; + juce::SpinLock::ScopedLockType lock(data.audioProcessor.effectsLock); + data.setSelected(rowNum, effectComponent->selected.getToggleState()); + }; + } - { - juce::SpinLock::ScopedLockType lock(data.audioProcessor.effectsLock); - // check if effect is in audioProcessor enabled effects - for (auto effect : data.audioProcessor.enabledEffects) { - if (effect->getId() == data.getId(rn)) { - isSelected = true; - break; - } - } + listModel.addComponent(effectComponent); } - effectComponent->selected.setToggleState(isSelected, juce::dontSendNotification); - effectComponent->selected.onClick = [this] { - auto data = (AudioEffectListBoxItemData&)modelData; - juce::SpinLock::ScopedLockType lock(data.audioProcessor.effectsLock); - ((AudioEffectListBoxItemData&)modelData).setSelected(rowNum, this->effectComponent->selected.getToggleState()); - }; + + list.setModel(&listModel); + list.setRowHeight(30); + list.updateContent(); + addAndMakeVisible(list); } EffectsListComponent::~EffectsListComponent() {} @@ -51,15 +62,23 @@ void EffectsListComponent::paint(juce::Graphics& g) { void EffectsListComponent::resized() { auto area = getLocalBounds(); area.removeFromLeft(20); - effectComponent->setBounds(area); + list.setBounds(area); +} + +int EffectsListBoxModel::getRowHeight(int row) { + auto data = (AudioEffectListBoxItemData&)modelData; + return data.getEffect(row)->getDetails().size() * 30; +} + +bool EffectsListBoxModel::hasVariableHeightRows() const { + return true; } juce::Component* EffectsListBoxModel::refreshComponentForRow(int rowNumber, bool isRowSelected, juce::Component *existingComponentToUpdate) { std::unique_ptr item(dynamic_cast(existingComponentToUpdate)); if (juce::isPositiveAndBelow(rowNumber, modelData.getNumItems())) { auto data = (AudioEffectListBoxItemData&)modelData; - std::shared_ptr effectComponent = std::make_shared(0, 1, 0.01, 0, data.getText(rowNumber), data.getId(rowNumber)); - item = std::make_unique(listBox, (AudioEffectListBoxItemData&)modelData, rowNumber, effectComponent); + item = std::make_unique(listBox, (AudioEffectListBoxItemData&)modelData, rowNumber, data.getEffect(rowNumber)); } return item.release(); } diff --git a/Source/components/EffectsListComponent.h b/Source/components/EffectsListComponent.h index de29540..8ce8735 100644 --- a/Source/components/EffectsListComponent.h +++ b/Source/components/EffectsListComponent.h @@ -4,6 +4,7 @@ #include "../PluginProcessor.h" #include "../audio/Effect.h" #include "EffectComponent.h" +#include "ComponentList.h" // Application-specific data container struct AudioEffectListBoxItemData : public DraggableListBoxItemData @@ -71,10 +72,6 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData } } - void setValue(int itemIndex, double value) { - data[itemIndex]->setValue(value); - } - void setSelected(int itemIndex, bool selected) { if (selected) { audioProcessor.enableEffect(data[itemIndex]); @@ -83,16 +80,8 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData } } - juce::String getText(int itemIndex) { - return data[itemIndex]->getName(); - } - - double getValue(int itemIndex) { - return data[itemIndex]->getValue(); - } - - juce::String getId(int itemIndex) { - return data[itemIndex]->getId(); + std::shared_ptr getEffect(int itemIndex) { + return data[itemIndex]; } }; @@ -100,14 +89,16 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData class EffectsListComponent : public DraggableListBoxItem { public: - EffectsListComponent(DraggableListBox& lb, AudioEffectListBoxItemData& data, int rn, std::shared_ptr effectComponent); + EffectsListComponent(DraggableListBox& lb, AudioEffectListBoxItemData& data, int rn, std::shared_ptr effect); ~EffectsListComponent(); void paint(juce::Graphics& g) override; void resized() override; protected: - std::shared_ptr effectComponent; + std::shared_ptr effect; + ComponentListModel listModel; + juce::ListBox list; private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectsListComponent) }; @@ -124,5 +115,8 @@ public: audioProcessor.updateEffectPrecedence(); } + int getRowHeight(int row) override; + bool hasVariableHeightRows() const override; + juce::Component* refreshComponentForRow(int, bool, juce::Component*) override; }; diff --git a/Source/components/VListBox.cpp b/Source/components/VListBox.cpp new file mode 100644 index 0000000..855bee5 --- /dev/null +++ b/Source/components/VListBox.cpp @@ -0,0 +1,1069 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-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. + + ============================================================================== +*/ + +#include "VListBox.h" + +namespace juce +{ +namespace jc +{ +class ListBox::RowComponent : public Component, public TooltipClient +{ +public: + RowComponent (ListBox& lb) : owner (lb) {} + + void paint (Graphics& g) override + { + if (auto* m = owner.getModel()) + m->paintListBoxItem (row, g, getWidth(), getHeight(), selected); + } + + void update (const int newRow, const bool nowSelected) + { + if (row != newRow || selected != nowSelected) + { + repaint(); + row = newRow; + selected = nowSelected; + } + + if (auto* m = owner.getModel()) + { + setMouseCursor (m->getMouseCursorForRow (row)); + + customComponent.reset (m->refreshComponentForRow (newRow, nowSelected, customComponent.release())); + + if (customComponent != nullptr) + { + addAndMakeVisible (customComponent.get()); + customComponent->setBounds (getLocalBounds()); + } + } + } + + void performSelection (const MouseEvent& e, bool isMouseUp) + { + owner.selectRowsBasedOnModifierKeys (row, e.mods, isMouseUp); + + if (auto* m = owner.getModel()) + m->listBoxItemClicked (row, e); + } + + bool isInDragToScrollViewport() const noexcept + { + if (auto* vp = owner.getViewport()) + return vp->isScrollOnDragEnabled() && (vp->canScrollVertically() || vp->canScrollHorizontally()); + + return false; + } + + void mouseDown (const MouseEvent& e) override + { + isDragging = false; + isDraggingToScroll = false; + selectRowOnMouseUp = false; + + if (isEnabled()) + { + if (owner.selectOnMouseDown && ! (selected || isInDragToScrollViewport())) + performSelection (e, false); + else + selectRowOnMouseUp = true; + } + } + + void mouseUp (const MouseEvent& e) override + { + if (isEnabled() && selectRowOnMouseUp && ! (isDragging || isDraggingToScroll)) + performSelection (e, true); + } + + void mouseDoubleClick (const MouseEvent& e) override + { + if (isEnabled()) + if (auto* m = owner.getModel()) + m->listBoxItemDoubleClicked (row, e); + } + + void mouseDrag (const MouseEvent& e) override + { + if (auto* m = owner.getModel()) + { + if (isEnabled() && e.mouseWasDraggedSinceMouseDown() && ! isDragging) + { + SparseSet rowsToDrag; + + if (owner.selectOnMouseDown || owner.isRowSelected (row)) + rowsToDrag = owner.getSelectedRows(); + else + rowsToDrag.addRange (Range::withStartAndLength (row, 1)); + + if (rowsToDrag.size() > 0) + { + auto dragDescription = m->getDragSourceDescription (rowsToDrag); + + if (! (dragDescription.isVoid() || (dragDescription.isString() && dragDescription.toString().isEmpty()))) + { + isDragging = true; + owner.startDragAndDrop (e, rowsToDrag, dragDescription, true); + } + } + } + } + + if (! isDraggingToScroll) + if (auto* vp = owner.getViewport()) + isDraggingToScroll = vp->isCurrentlyScrollingOnDrag(); + } + + void resized() override + { + if (customComponent != nullptr) + customComponent->setBounds (getLocalBounds()); + } + + String getTooltip() override + { + if (auto* m = owner.getModel()) + return m->getTooltipForRow (row); + + return {}; + } + + ListBox& owner; + std::unique_ptr customComponent; + int row = -1; + bool selected = false, isDragging = false, isDraggingToScroll = false, selectRowOnMouseUp = false; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RowComponent) +}; + +//============================================================================== +class ListBox::ListViewport : public Viewport +{ +public: + ListViewport (ListBox& lb) : owner (lb) + { + setWantsKeyboardFocus (false); + + auto content = new Component(); + setViewedComponent (content); + content->setWantsKeyboardFocus (false); + } + + RowComponent* getComponentForRow (const int row) const noexcept { return rows[row % jmax (1, rows.size())]; } + + RowComponent* getComponentForRowIfOnscreen (const int row) const noexcept + { + return (row >= firstIndex && row < firstIndex + rows.size()) ? getComponentForRow (row) : nullptr; + } + + int getRowNumberOfComponent (Component* const rowComponent) const noexcept + { + const int index = getViewedComponent()->getIndexOfChildComponent (rowComponent); + const int num = rows.size(); + + for (int i = num; --i >= 0;) + if (((firstIndex + i) % jmax (1, num)) == index) + return firstIndex + i; + + return -1; + } + + void visibleAreaChanged (const Rectangle&) override + { + updateVisibleArea (true); + + if (auto* m = owner.getModel()) + m->listWasScrolled(); + } + + void updateVisibleArea (const bool makeSureItUpdatesContent) + { + hasUpdated = false; + + auto& content = *getViewedComponent(); + auto newX = content.getX(); + auto newY = content.getY(); + auto newW = jmax (owner.minimumRowWidth, getMaximumVisibleWidth()); + auto newH = owner.getContentHeight(); + + if (newY + newH < getMaximumVisibleHeight() && newH > getMaximumVisibleHeight()) + newY = getMaximumVisibleHeight() - newH; + + content.setBounds (newX, newY, newW, newH); + + if (makeSureItUpdatesContent && ! hasUpdated) + updateContents(); + } + + void updateContents() + { + hasUpdated = true; + auto& content = *getViewedComponent(); + + auto y = getViewPositionY(); + auto w = content.getWidth(); + + if (w == 0 || owner.model == nullptr) + return; + + if (owner.totalItems > 0) + { + firstIndex = owner.getRowForPosition (y); + firstWholeIndex = firstIndex; + + if (owner.getPositionForRow (firstIndex) < y) + ++firstWholeIndex; + + auto lastRow = jmin (owner.getRowForPosition (y + getMaximumVisibleHeight()), owner.totalItems - 1); + + lastWholeIndex = lastRow - 1; + + auto numNeeded = lastRow - firstIndex + 1; + rows.removeRange (numNeeded, rows.size()); + + while (numNeeded > rows.size()) + { + auto newRow = new RowComponent (owner); + rows.add (newRow); + content.addAndMakeVisible (newRow); + } + + for (int i = 0; i < numNeeded; ++i) + { + const int row = i + firstIndex; + + if (auto* rowComp = getComponentForRow (row)) + { + rowComp->setBounds (0, owner.getPositionForRow (row), w, owner.getRowHeight (row)); + DBG (String (i) + " " + rowComp->getBounds().toString()); + rowComp->update (row, owner.isRowSelected (row)); + } + } + } + else + { + rows.clear(); + } + + + if (owner.headerComponent != nullptr) + owner.headerComponent->setBounds (owner.outlineThickness + content.getX(), + owner.outlineThickness, + jmax (owner.getWidth() - owner.outlineThickness * 2, content.getWidth()), + owner.headerComponent->getHeight()); + } + + void selectRow (const int row, const bool dontScroll, const int lastSelectedRow, const int totalRows, const bool isMouseClick) + { + hasUpdated = false; + + if (row < firstWholeIndex && ! dontScroll) + { + setViewPosition (getViewPositionX(), owner.getPositionForRow (row)); + } + else if (row >= lastWholeIndex && ! dontScroll) + { + const int rowsOnScreen = lastWholeIndex - firstWholeIndex; + + if (row >= lastSelectedRow + rowsOnScreen && rowsOnScreen < totalRows - 1 && ! isMouseClick) + { + // put row at the top of the screen, or as close as we can make it. but this position is already constrained by + // setViewPosition's internals. + // auto y = jlimit (0, jmax (0, totalRows - rowsOnScreen), row) * rowH; + setViewPosition (getViewPositionX(), owner.getPositionForRow (row)); + } + else + { + auto p = owner.getPositionForRow (row); + auto rh = owner.getRowHeight (row); + setViewPosition (getViewPositionX(), jmax (0, p + rh - getMaximumVisibleHeight())); + } + } + + if (! hasUpdated) + updateContents(); + } + + void scrollToEnsureRowIsOnscreen (const int row) + { + if (row < firstWholeIndex) + { + setViewPosition (getViewPositionX(), owner.getPositionForRow (row)); + } + else if (row >= lastWholeIndex) + { + auto p = owner.getPositionForRow (row); + auto rh = owner.getRowHeight (row); + setViewPosition (getViewPositionX(), jmax (0, p + rh - getMaximumVisibleHeight())); + } + } + + void paint (Graphics& g) override + { + if (isOpaque()) + g.fillAll (owner.findColour (ListBox::backgroundColourId)); + } + + bool keyPressed (const KeyPress& key) override + { + if (Viewport::respondsToKey (key)) + { + const int allowableMods = owner.multipleSelection ? ModifierKeys::shiftModifier : 0; + + if ((key.getModifiers().getRawFlags() & ~allowableMods) == 0) + { + // we want to avoid these keypresses going to the viewport, and instead allow + // them to pass up to our listbox.. + return false; + } + } + + return Viewport::keyPressed (key); + } + +private: + ListBox& owner; + OwnedArray rows; + int firstIndex = 0, firstWholeIndex = 0, lastWholeIndex = 0; + bool hasUpdated = false; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ListViewport) +}; + +//============================================================================== +struct ListBoxMouseMoveSelector : public MouseListener +{ + ListBoxMouseMoveSelector (ListBox& lb) : owner (lb) { owner.addMouseListener (this, true); } + + ~ListBoxMouseMoveSelector() override { owner.removeMouseListener (this); } + + void mouseMove (const MouseEvent& e) override + { + auto pos = e.getEventRelativeTo (&owner).position.toInt(); + owner.selectRow (owner.getRowContainingPosition (pos.x, pos.y), true); + } + + void mouseExit (const MouseEvent& e) override { mouseMove (e); } + + ListBox& owner; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ListBoxMouseMoveSelector) +}; + +int ListBoxModel::getRowForPosition (int yPos) +{ + if (! hasVariableHeightRows()) + return yPos / getRowHeight (0); + + auto y = 0; + auto numRows = getNumRows(); + + for (int r = 0; r < numRows; ++r) + { + y += getRowHeight (r); + + if (yPos < y) + return r; + } + + return numRows; +} + +int ListBoxModel::getPositionForRow (int rowNumber) +{ + if (! hasVariableHeightRows()) + return rowNumber * getRowHeight (0); + + auto y = 0; + + for (int r = 0; r < rowNumber; ++r) + y += getRowHeight (r); + + return y; +} //============================================================================== +ListBox::ListBox (const String& name, ListBoxModel* const m) : Component (name), model (m) +{ + viewport.reset (new ListViewport (*this)); + addAndMakeVisible (viewport.get()); + + ListBox::setWantsKeyboardFocus (true); + ListBox::colourChanged(); +} + +ListBox::~ListBox() +{ + headerComponent.reset(); + viewport.reset(); +} + +void ListBox::setModel (ListBoxModel* const newModel) +{ + if (model != newModel) + { + model = newModel; + repaint(); + updateContent(); + } +} + +void ListBox::setMultipleSelectionEnabled (bool b) noexcept +{ + multipleSelection = b; +} +void ListBox::setClickingTogglesRowSelection (bool b) noexcept +{ + alwaysFlipSelection = b; +} +void ListBox::setRowSelectedOnMouseDown (bool b) noexcept +{ + selectOnMouseDown = b; +} + +void ListBox::setMouseMoveSelectsRows (bool b) +{ + if (b) + { + if (mouseMoveSelector == nullptr) + mouseMoveSelector.reset (new ListBoxMouseMoveSelector (*this)); + } + else + { + mouseMoveSelector.reset(); + } +} + +//============================================================================== +void ListBox::paint (Graphics& g) +{ + if (! hasDoneInitialUpdate) + updateContent(); + + g.fillAll (findColour (backgroundColourId)); +} + +void ListBox::paintOverChildren (Graphics& g) +{ + if (outlineThickness > 0) + { + g.setColour (findColour (outlineColourId)); + g.drawRect (getLocalBounds(), outlineThickness); + } +} + +void ListBox::resized() +{ + viewport->setBoundsInset (BorderSize (outlineThickness + (headerComponent != nullptr ? headerComponent->getHeight() : 0), + outlineThickness, + outlineThickness, + outlineThickness)); + + // viewport->setSingleStepSizes (20, getRowHeight()); + + viewport->updateVisibleArea (false); +} + +void ListBox::visibilityChanged() +{ + viewport->updateVisibleArea (true); +} + +Viewport* ListBox::getViewport() const noexcept +{ + return viewport.get(); +} + +//============================================================================== +void ListBox::updateContent() +{ + hasDoneInitialUpdate = true; + totalItems = (model != nullptr) ? model->getNumRows() : 0; + + bool selectionChanged = false; + + if (selected.size() > 0 && selected[selected.size() - 1] >= totalItems) + { + selected.removeRange ({ totalItems, std::numeric_limits::max() }); + lastRowSelected = getSelectedRow (0); + selectionChanged = true; + } + + viewport->updateVisibleArea (isVisible()); + viewport->resized(); + + if (selectionChanged && model != nullptr) + model->selectedRowsChanged (lastRowSelected); +} + +//============================================================================== +void ListBox::selectRow (int row, bool dontScroll, bool deselectOthersFirst) +{ + selectRowInternal (row, dontScroll, deselectOthersFirst, false); +} + +void ListBox::selectRowInternal (const int row, bool dontScroll, bool deselectOthersFirst, bool isMouseClick) +{ + if (! multipleSelection) + deselectOthersFirst = true; + + if ((! isRowSelected (row)) || (deselectOthersFirst && getNumSelectedRows() > 1)) + { + if (isPositiveAndBelow (row, totalItems)) + { + if (deselectOthersFirst) + selected.clear(); + + selected.addRange ({ row, row + 1 }); + + if (getHeight() == 0 || getWidth() == 0) + dontScroll = true; + + viewport->selectRow (row, dontScroll, lastRowSelected, totalItems, isMouseClick); + + lastRowSelected = row; + model->selectedRowsChanged (row); + } + else + { + if (deselectOthersFirst) + deselectAllRows(); + } + } +} + +void ListBox::deselectRow (const int row) +{ + if (selected.contains (row)) + { + selected.removeRange ({ row, row + 1 }); + + if (row == lastRowSelected) + lastRowSelected = getSelectedRow (0); + + viewport->updateContents(); + model->selectedRowsChanged (lastRowSelected); + } +} + +void ListBox::setSelectedRows (const SparseSet& setOfRowsToBeSelected, const NotificationType sendNotificationEventToModel) +{ + selected = setOfRowsToBeSelected; + selected.removeRange ({ totalItems, std::numeric_limits::max() }); + + if (! isRowSelected (lastRowSelected)) + lastRowSelected = getSelectedRow (0); + + viewport->updateContents(); + + if (model != nullptr && sendNotificationEventToModel == sendNotification) + model->selectedRowsChanged (lastRowSelected); +} + +SparseSet ListBox::getSelectedRows() const +{ + return selected; +} + +void ListBox::selectRangeOfRows (int firstRow, int lastRow, bool dontScrollToShowThisRange) +{ + if (multipleSelection && (firstRow != lastRow)) + { + const int numRows = totalItems - 1; + firstRow = jlimit (0, jmax (0, numRows), firstRow); + lastRow = jlimit (0, jmax (0, numRows), lastRow); + + selected.addRange ({ jmin (firstRow, lastRow), jmax (firstRow, lastRow) + 1 }); + + selected.removeRange ({ lastRow, lastRow + 1 }); + } + + selectRowInternal (lastRow, dontScrollToShowThisRange, false, true); +} + +void ListBox::flipRowSelection (const int row) +{ + if (isRowSelected (row)) + deselectRow (row); + else + selectRowInternal (row, false, false, true); +} + +void ListBox::deselectAllRows() +{ + if (! selected.isEmpty()) + { + selected.clear(); + lastRowSelected = -1; + + viewport->updateContents(); + + if (model != nullptr) + model->selectedRowsChanged (lastRowSelected); + } +} + +void ListBox::selectRowsBasedOnModifierKeys (const int row, ModifierKeys mods, const bool isMouseUpEvent) +{ + if (multipleSelection && (mods.isCommandDown() || alwaysFlipSelection)) + { + flipRowSelection (row); + } + else if (multipleSelection && mods.isShiftDown() && lastRowSelected >= 0) + { + selectRangeOfRows (lastRowSelected, row); + } + else if ((! mods.isPopupMenu()) || ! isRowSelected (row)) + { + selectRowInternal (row, false, ! (multipleSelection && (! isMouseUpEvent) && isRowSelected (row)), true); + } +} + +int ListBox::getNumSelectedRows() const +{ + return selected.size(); +} + +int ListBox::getSelectedRow (const int index) const +{ + return (isPositiveAndBelow (index, selected.size())) ? selected[index] : -1; +} + +bool ListBox::isRowSelected (const int row) const +{ + return selected.contains (row); +} + +int ListBox::getLastRowSelected() const +{ + return isRowSelected (lastRowSelected) ? lastRowSelected : -1; +} + +//============================================================================== +int ListBox::getRowContainingPosition (const int x, const int y) const noexcept +{ + if (isPositiveAndBelow (x, getWidth())) + { + const int row = getRowForPosition (viewport->getViewPositionY() + y - viewport->getY()); + + if (isPositiveAndBelow (row, totalItems)) + return row; + } + + return -1; +} + +int ListBox::getRowHeight (int row) const +{ + if (model == nullptr) + return 0; + + return model->getRowHeight (row); +} + +int ListBox::getInsertionIndexForPosition (const int x, const int y) const noexcept +{ + if (isPositiveAndBelow (x, getWidth())) + { + auto cursorY = y + viewport->getViewPositionY() - viewport->getY(); + auto row = getRowForPosition (cursorY); + auto rowY = getPositionForRow (row); + auto rowCentre = rowY + getRowHeight (row) / 2; + + if (rowCentre < cursorY) + ++row; + + return jlimit (0, totalItems, row); + } + + return -1; +} + +Component* ListBox::getComponentForRowNumber (const int row) const noexcept +{ + if (auto* listRowComp = viewport->getComponentForRowIfOnscreen (row)) + return listRowComp->customComponent.get(); + + return nullptr; +} + +int ListBox::getRowNumberOfComponent (Component* const rowComponent) const noexcept +{ + return viewport->getRowNumberOfComponent (rowComponent); +} + +Rectangle ListBox::getRowPosition (int rowNumber, bool relativeToComponentTopLeft) const noexcept +{ + auto y = viewport->getY() + getPositionForRow (rowNumber); + + if (relativeToComponentTopLeft) + y -= viewport->getViewPositionY(); + + return { viewport->getX(), y, viewport->getViewedComponent()->getWidth(), getRowHeight (rowNumber) }; +} + +void ListBox::setVerticalPosition (const double proportion) +{ + auto offscreen = viewport->getViewedComponent()->getHeight() - viewport->getHeight(); + + viewport->setViewPosition (viewport->getViewPositionX(), jmax (0, roundToInt (proportion * offscreen))); +} + +double ListBox::getVerticalPosition() const +{ + auto offscreen = viewport->getViewedComponent()->getHeight() - viewport->getHeight(); + + return offscreen > 0 ? viewport->getViewPositionY() / (double) offscreen : 0; +} + +int ListBox::getVisibleRowWidth() const noexcept +{ + return viewport->getViewWidth(); +} + +void ListBox::scrollToEnsureRowIsOnscreen (const int row) +{ + viewport->scrollToEnsureRowIsOnscreen (row); +} + +//============================================================================== +bool ListBox::keyPressed (const KeyPress& key) +{ + const bool multiple = multipleSelection && lastRowSelected >= 0 && key.getModifiers().isShiftDown(); + + if (key.isKeyCode (KeyPress::upKey)) + { + if (multiple) + selectRangeOfRows (lastRowSelected, lastRowSelected - 1); + else + selectRow (jmax (0, lastRowSelected - 1)); + } + else if (key.isKeyCode (KeyPress::downKey)) + { + if (multiple) + selectRangeOfRows (lastRowSelected, lastRowSelected + 1); + else + selectRow (jmin (totalItems - 1, jmax (0, lastRowSelected + 1))); + } + else if (key.isKeyCode (KeyPress::pageUpKey)) + { + auto rowToSelect = jmax(0, lastRowSelected); + auto pageHeight = viewport->getMaximumVisibleHeight(); + + while (pageHeight > 0 && rowToSelect > 0) + { + pageHeight -= getRowHeight (rowToSelect); + + if (pageHeight > 0) + rowToSelect--; + } + + if (multiple) + selectRangeOfRows (lastRowSelected, rowToSelect); + else + selectRow (rowToSelect); + } + else if (key.isKeyCode (KeyPress::pageDownKey)) + { + auto rowToSelect = jmax(0, lastRowSelected); + auto pageHeight= viewport->getMaximumVisibleHeight(); + + while (pageHeight > 0 && rowToSelect < totalItems - 1) + { + pageHeight -= getRowHeight (rowToSelect); + + if (pageHeight > 0) + rowToSelect++; + } + + if (multiple) + selectRangeOfRows (lastRowSelected, rowToSelect); + else + selectRow (rowToSelect); + } + else if (key.isKeyCode (KeyPress::homeKey)) + { + if (multiple) + selectRangeOfRows (lastRowSelected, 0); + else + selectRow (0); + } + else if (key.isKeyCode (KeyPress::endKey)) + { + if (multiple) + selectRangeOfRows (lastRowSelected, totalItems - 1); + else + selectRow (totalItems - 1); + } + else if (key.isKeyCode (KeyPress::returnKey) && isRowSelected (lastRowSelected)) + { + if (model != nullptr) + model->returnKeyPressed (lastRowSelected); + } + else if ((key.isKeyCode (KeyPress::deleteKey) || key.isKeyCode (KeyPress::backspaceKey)) && isRowSelected (lastRowSelected)) + { + if (model != nullptr) + model->deleteKeyPressed (lastRowSelected); + } + else if (multipleSelection && key == KeyPress ('a', ModifierKeys::commandModifier, 0)) + { + selectRangeOfRows (0, std::numeric_limits::max()); + } + else + { + return false; + } + + return true; +} + +bool ListBox::keyStateChanged (const bool isKeyDown) +{ + return isKeyDown + && (KeyPress::isKeyCurrentlyDown (KeyPress::upKey) || KeyPress::isKeyCurrentlyDown (KeyPress::pageUpKey) + || KeyPress::isKeyCurrentlyDown (KeyPress::downKey) || KeyPress::isKeyCurrentlyDown (KeyPress::pageDownKey) + || KeyPress::isKeyCurrentlyDown (KeyPress::homeKey) || KeyPress::isKeyCurrentlyDown (KeyPress::endKey) + || KeyPress::isKeyCurrentlyDown (KeyPress::returnKey)); +} + +void ListBox::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel) +{ + bool eventWasUsed = false; + + if (wheel.deltaX != 0.0f && getHorizontalScrollBar().isVisible()) + { + eventWasUsed = true; + getHorizontalScrollBar().mouseWheelMove (e, wheel); + } + + if (wheel.deltaY != 0.0f && getVerticalScrollBar().isVisible()) + { + eventWasUsed = true; + getVerticalScrollBar().mouseWheelMove (e, wheel); + } + + if (! eventWasUsed) + Component::mouseWheelMove (e, wheel); +} + +void ListBox::mouseUp (const MouseEvent& e) +{ + if (e.mouseWasClicked() && model != nullptr) + model->backgroundClicked (e); +} + +//============================================================================== + +int ListBox::getNumRowsOnScreen() const noexcept +{ + // todo: not clear this function can work as previously intended + + auto start = getRowContainingPosition (0, viewport->getViewPositionY()); + auto maxPosition = viewport->getViewArea().getBottom(); + auto rowNumber = start; + + while (rowNumber < totalItems && getPositionForRow (rowNumber) < maxPosition) + rowNumber++; + + return rowNumber - start; +} + +void ListBox::setMinimumContentWidth (const int newMinimumWidth) +{ + minimumRowWidth = newMinimumWidth; + updateContent(); +} + +int ListBox::getVisibleContentWidth() const noexcept +{ + return viewport->getMaximumVisibleWidth(); +} + +ScrollBar& ListBox::getVerticalScrollBar() const noexcept +{ + return viewport->getVerticalScrollBar(); +} +ScrollBar& ListBox::getHorizontalScrollBar() const noexcept +{ + return viewport->getHorizontalScrollBar(); +} + +void ListBox::colourChanged() +{ + setOpaque (findColour (backgroundColourId).isOpaque()); + viewport->setOpaque (isOpaque()); + repaint(); +} + +void ListBox::parentHierarchyChanged() +{ + colourChanged(); +} + +void ListBox::setOutlineThickness (int newThickness) +{ + outlineThickness = newThickness; + resized(); +} + +void ListBox::setHeaderComponent (std::unique_ptr newHeaderComponent) +{ + headerComponent = std::move (newHeaderComponent); + addAndMakeVisible (headerComponent.get()); + ListBox::resized(); +} + +void ListBox::repaintRow (const int rowNumber) noexcept +{ + repaint (getRowPosition (rowNumber, true)); +} + +Image ListBox::createSnapshotOfRows (const SparseSet& rows, int& imageX, int& imageY) +{ + Rectangle imageArea; + auto firstRow = getRowContainingPosition (0, viewport->getY()); + + for (int i = getNumRowsOnScreen() + 2; --i >= 0;) + { + if (rows.contains (firstRow + i)) + { + if (auto* rowComp = viewport->getComponentForRowIfOnscreen (firstRow + i)) + { + auto pos = getLocalPoint (rowComp, Point()); + + imageArea = imageArea.getUnion ({ pos.x, pos.y, rowComp->getWidth(), rowComp->getHeight() }); + } + } + } + + imageArea = imageArea.getIntersection (getLocalBounds()); + imageX = imageArea.getX(); + imageY = imageArea.getY(); + + auto listScale = Component::getApproximateScaleFactorForComponent (this); + Image snapshot ( + Image::ARGB, roundToInt ((float) imageArea.getWidth() * listScale), roundToInt ((float) imageArea.getHeight() * listScale), true); + + for (int i = getNumRowsOnScreen() + 2; --i >= 0;) + { + if (rows.contains (firstRow + i)) + { + if (auto* rowComp = viewport->getComponentForRowIfOnscreen (firstRow + i)) + { + Graphics g (snapshot); + g.setOrigin (getLocalPoint (rowComp, Point()) - imageArea.getPosition()); + + auto rowScale = Component::getApproximateScaleFactorForComponent (rowComp); + + if (g.reduceClipRegion (rowComp->getLocalBounds() * rowScale)) + { + g.beginTransparencyLayer (0.6f); + g.addTransform (AffineTransform::scale (rowScale)); + rowComp->paintEntireComponent (g, false); + g.endTransparencyLayer(); + } + } + } + } + + return snapshot; +} + +void ListBox::startDragAndDrop (const MouseEvent& e, + const SparseSet& rowsToDrag, + const var& dragDescription, + bool allowDraggingToOtherWindows) +{ + if (auto* dragContainer = DragAndDropContainer::findParentDragContainerFor (this)) + { + int x, y; + auto dragImage = createSnapshotOfRows (rowsToDrag, x, y); + + auto p = Point (x, y) - e.getEventRelativeTo (this).position.toInt(); + dragContainer->startDragging (dragDescription, this, dragImage, allowDraggingToOtherWindows, &p, &e.source); + } + else + { + // to be able to do a drag-and-drop operation, the listbox needs to + // be inside a component which is also a DragAndDropContainer. + jassertfalse; + } +} + +int ListBox::getContentHeight() const +{ + if (model == nullptr || totalItems == 0) + return 0; + + return getPositionForRow (totalItems - 1) + getRowHeight (totalItems - 1); +} + +int ListBox::getRowForPosition (int y) const +{ + if (model == nullptr || y < 0) + return 0; + + return model->getRowForPosition (y); +} + +int ListBox::getPositionForRow (int row) const +{ + if (model == nullptr) + return 0; + + jassert (row >= 0 && row < totalItems); + + return model->getPositionForRow (row); +} + +//============================================================================== +Component* ListBoxModel::refreshComponentForRow (int, bool, Component* existingComponentToUpdate) +{ + ignoreUnused (existingComponentToUpdate); + jassert (existingComponentToUpdate == nullptr); // indicates a failure in the code that recycles the components + return nullptr; +} + +void ListBoxModel::listBoxItemClicked (int, const MouseEvent&) {} +void ListBoxModel::listBoxItemDoubleClicked (int, const MouseEvent&) {} +void ListBoxModel::backgroundClicked (const MouseEvent&) {} +void ListBoxModel::selectedRowsChanged (int) {} +void ListBoxModel::deleteKeyPressed (int) {} +void ListBoxModel::returnKeyPressed (int) {} +void ListBoxModel::listWasScrolled() {} +var ListBoxModel::getDragSourceDescription (const SparseSet&) +{ + return {}; +} +String ListBoxModel::getTooltipForRow (int) +{ + return {}; +} +MouseCursor ListBoxModel::getMouseCursorForRow (int) +{ + return MouseCursor::NormalCursor; +} + +} // namespace jc +} // namespace juce \ No newline at end of file diff --git a/Source/components/VListBox.h b/Source/components/VListBox.h new file mode 100644 index 0000000..437e864 --- /dev/null +++ b/Source/components/VListBox.h @@ -0,0 +1,622 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - 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 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-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. + + ============================================================================== +*/ + +#include "JuceHeader.h" + +namespace juce { + namespace jc { + //============================================================================== + /** + A subclass of this is used to drive a ListBox. + + @see ListBox + + @tags{GUI} + */ + class ListBoxModel { + public: + //============================================================================== + /** Destructor. */ + virtual ~ListBoxModel() = default; + + //============================================================================== + /** This has to return the number of items in the list. + @see ListBox::getNumRows() + */ + virtual int getNumRows() = 0; + + /** This method must be implemented to draw a row of the list. + Note that the rowNumber value may be greater than the number of rows in your + list, so be careful that you don't assume it's less than getNumRows(). + */ + virtual void paintListBoxItem(int rowNumber, Graphics& g, int width, int height, bool rowIsSelected) = 0; + + /** This is used to create or update a custom component to go in a row of the list. + + Any row may contain a custom component, or can just be drawn with the paintListBoxItem() method + and handle mouse clicks with listBoxItemClicked(). + + This method will be called whenever a custom component might need to be updated - e.g. + when the list is changed, or ListBox::updateContent() is called. + + If you don't need a custom component for the specified row, then return nullptr. + (Bear in mind that even if you're not creating a new component, you may still need to + delete existingComponentToUpdate if it's non-null). + + If you do want a custom component, and the existingComponentToUpdate is null, then + this method must create a suitable new component and return it. + + If the existingComponentToUpdate is non-null, it will be a pointer to a component previously created + by this method. In this case, the method must either update it to make sure it's correctly representing + the given row (which may be different from the one that the component was created for), or it can + delete this component and return a new one. + + The component that your method returns will be deleted by the ListBox when it is no longer needed. + + Bear in mind that if you put a custom component inside the row but still want the + listbox to automatically handle clicking, selection, etc, then you'll need to make sure + your custom component doesn't intercept all the mouse events that land on it, e.g by + using Component::setInterceptsMouseClicks(). + */ + virtual Component* refreshComponentForRow(int rowNumber, bool isRowSelected, Component* existingComponentToUpdate); + + /** This can be overridden to react to the user clicking on a row. + @see listBoxItemDoubleClicked + */ + virtual void listBoxItemClicked(int row, const MouseEvent&); + + /** This can be overridden to react to the user double-clicking on a row. + @see listBoxItemClicked + */ + virtual void listBoxItemDoubleClicked(int row, const MouseEvent&); + + /** This can be overridden to react to the user clicking on a part of the list where + there are no rows. + @see listBoxItemClicked + */ + virtual void backgroundClicked(const MouseEvent&); + + /** Override this to be informed when rows are selected or deselected. + + This will be called whenever a row is selected or deselected. If a range of + rows is selected all at once, this will just be called once for that event. + + @param lastRowSelected the last row that the user selected. If no + rows are currently selected, this may be -1. + */ + virtual void selectedRowsChanged(int lastRowSelected); + + /** Override this to be informed when the delete key is pressed. + + If no rows are selected when they press the key, this won't be called. + + @param lastRowSelected the last row that had been selected when they pressed the + key - if there are multiple selections, this might not be + very useful + */ + virtual void deleteKeyPressed(int lastRowSelected); + + /** Override this to be informed when the return key is pressed. + + If no rows are selected when they press the key, this won't be called. + + @param lastRowSelected the last row that had been selected when they pressed the + key - if there are multiple selections, this might not be + very useful + */ + virtual void returnKeyPressed(int lastRowSelected); + + /** Override this to be informed when the list is scrolled. + + This might be caused by the user moving the scrollbar, or by programmatic changes + to the list position. + */ + virtual void listWasScrolled(); + + /** To allow rows from your list to be dragged-and-dropped, implement this method. + + If this returns a non-null variant then when the user drags a row, the listbox will + try to find a DragAndDropContainer in its parent hierarchy, and will use it to trigger + a drag-and-drop operation, using this string as the source description, with the listbox + itself as the source component. + + @see DragAndDropContainer::startDragging + */ + virtual var getDragSourceDescription(const SparseSet& rowsToDescribe); + + /** You can override this to provide tool tips for specific rows. + @see TooltipClient + */ + virtual String getTooltipForRow(int row); + + /** You can override this to return a custom mouse cursor for each row. */ + virtual MouseCursor getMouseCursorForRow(int row); + + /** + * Override this to return the row height for a given row. + */ + virtual int getRowHeight(int row) { return 22; } + + /** + * Override this if your list may have variable row heights. + * + * Performance is slightly improved if this returns false, but getRowHeight + * must then return the same number for all rows. + */ + virtual bool hasVariableHeightRows() const { return false; } + + /** + * You can override this to improve performance with very long lists. + * + * If you have many variable height rows you may be able to improve + * performance by directly calculating the row for a given y position. + * + * If the y position is greater than the number of rows return the number + * of rows. yPos will never be less than zero. + */ + virtual int getRowForPosition(int yPos); + + /** + * You can override this to improve performance with very long lists. + * + * If you have a large number (e.g. thousands of) variable height rows you + * may be able to improve performance by overriding this function and + * directly calculating the y position of a given row. + */ + virtual int getPositionForRow(int rowNumber); + }; + + //============================================================================== + /** + A list of items that can be scrolled vertically. + + To create a list, you'll need to create a subclass of ListBoxModel. This can + either paint each row of the list and respond to events via callbacks, or for + more specialised tasks, it can supply a custom component to fill each row. + + @see ComboBox, TableListBox + + @tags{GUI} + */ + class JUCE_API ListBox : public Component, public SettableTooltipClient { + public: + //============================================================================== + /** Creates a ListBox. + + The model pointer passed-in can be null, in which case you can set it later + with setModel(). + */ + ListBox(const String& componentName = String(), ListBoxModel* model = nullptr); + + /** Destructor. */ + ~ListBox() override; + + //============================================================================== + /** Changes the current data model to display. */ + void setModel(ListBoxModel* newModel); + + /** Returns the current list model. */ + ListBoxModel* getModel() const noexcept { return model; } + + //============================================================================== + /** Causes the list to refresh its content. + + Call this when the number of rows in the list changes, or if you want it + to call refreshComponentForRow() on all the row components. + + This must only be called from the main message thread. + */ + void updateContent(); + + //============================================================================== + /** Turns on multiple-selection of rows. + + By default this is disabled. + + When your row component gets clicked you'll need to call the + selectRowsBasedOnModifierKeys() method to tell the list that it's been + clicked and to get it to do the appropriate selection based on whether + the ctrl/shift keys are held down. + */ + void setMultipleSelectionEnabled(bool shouldBeEnabled) noexcept; + + /** If enabled, this makes the listbox flip the selection status of + each row that the user clicks, without affecting other selected rows. + + (This only has an effect if multiple selection is also enabled). + If not enabled, you can still get the same row-flipping behaviour by holding + down CMD or CTRL when clicking. + */ + void setClickingTogglesRowSelection(bool flipRowSelection) noexcept; + + /** Sets whether a row should be selected when the mouse is pressed or released. + By default this is true, but you may want to turn it off. + */ + void setRowSelectedOnMouseDown(bool isSelectedOnMouseDown) noexcept; + + /** Makes the list react to mouse moves by selecting the row that the mouse if over. + + This function is here primarily for the ComboBox class to use, but might be + useful for some other purpose too. + */ + void setMouseMoveSelectsRows(bool shouldSelect); + + //============================================================================== + /** Selects a row. + + If the row is already selected, this won't do anything. + + @param rowNumber the row to select + @param dontScrollToShowThisRow if true, the list's position won't change; if false and + the selected row is off-screen, it'll scroll to make + sure that row is on-screen + @param deselectOthersFirst if true and there are multiple selections, these will + first be deselected before this item is selected + @see isRowSelected, selectRowsBasedOnModifierKeys, flipRowSelection, deselectRow, + deselectAllRows, selectRangeOfRows + */ + void selectRow(int rowNumber, bool dontScrollToShowThisRow = false, bool deselectOthersFirst = true); + + /** Selects a set of rows. + + This will add these rows to the current selection, so you might need to + clear the current selection first with deselectAllRows() + + @param firstRow the first row to select (inclusive) + @param lastRow the last row to select (inclusive) + @param dontScrollToShowThisRange if true, the list's position won't change; if false and + the selected range is off-screen, it'll scroll to make + sure that the range of rows is on-screen + */ + void selectRangeOfRows(int firstRow, int lastRow, bool dontScrollToShowThisRange = false); + + /** Deselects a row. + If it's not currently selected, this will do nothing. + @see selectRow, deselectAllRows + */ + void deselectRow(int rowNumber); + + /** Deselects any currently selected rows. + @see deselectRow + */ + void deselectAllRows(); + + /** Selects or deselects a row. + If the row's currently selected, this deselects it, and vice-versa. + */ + void flipRowSelection(int rowNumber); + + /** Returns a sparse set indicating the rows that are currently selected. + @see setSelectedRows + */ + SparseSet getSelectedRows() const; + + /** Sets the rows that should be selected, based on an explicit set of ranges. + + If sendNotificationEventToModel is true, the ListBoxModel::selectedRowsChanged() + method will be called. If it's false, no notification will be sent to the model. + + @see getSelectedRows + */ + void setSelectedRows(const SparseSet& setOfRowsToBeSelected, NotificationType sendNotificationEventToModel = sendNotification); + + /** Checks whether a row is selected. + */ + bool isRowSelected(int rowNumber) const; + + /** Returns the number of rows that are currently selected. + @see getSelectedRow, isRowSelected, getLastRowSelected + */ + int getNumSelectedRows() const; + + /** Returns the row number of a selected row. + + This will return the row number of the Nth selected row. The row numbers returned will + be sorted in order from low to high. + + @param index the index of the selected row to return, (from 0 to getNumSelectedRows() - 1) + @returns the row number, or -1 if the index was out of range or if there aren't any rows + selected + @see getNumSelectedRows, isRowSelected, getLastRowSelected + */ + int getSelectedRow(int index = 0) const; + + /** Returns the last row that the user selected. + + This isn't the same as the highest row number that is currently selected - if the user + had multiply-selected rows 10, 5 and then 6 in that order, this would return 6. + + If nothing is selected, it will return -1. + */ + int getLastRowSelected() const; + + /** Multiply-selects rows based on the modifier keys. + + If no modifier keys are down, this will select the given row and + deselect any others. + + If the ctrl (or command on the Mac) key is down, it'll flip the + state of the selected row. + + If the shift key is down, it'll select up to the given row from the + last row selected. + + @see selectRow + */ + void selectRowsBasedOnModifierKeys(int rowThatWasClickedOn, ModifierKeys modifiers, bool isMouseUpEvent); + + //============================================================================== + /** Scrolls the list to a particular position. + + The proportion is between 0 and 1.0, so 0 scrolls to the top of the list, + 1.0 scrolls to the bottom. + + If the total number of rows all fit onto the screen at once, then this + method won't do anything. + + @see getVerticalPosition + */ + void setVerticalPosition(double newProportion); + + /** Returns the current vertical position as a proportion of the total. + + This can be used in conjunction with setVerticalPosition() to save and restore + the list's position. It returns a value in the range 0 to 1. + + @see setVerticalPosition + */ + double getVerticalPosition() const; + + /** Scrolls if necessary to make sure that a particular row is visible. */ + void scrollToEnsureRowIsOnscreen(int row); + + /** Returns a reference to the vertical scrollbar. */ + ScrollBar& getVerticalScrollBar() const noexcept; + + /** Returns a reference to the horizontal scrollbar. */ + ScrollBar& getHorizontalScrollBar() const noexcept; + + /** Finds the row index that contains a given x,y position. + The position is relative to the ListBox's top-left. + If no row exists at this position, the method will return -1. + @see getComponentForRowNumber + */ + int getRowContainingPosition(int x, int y) const noexcept; + + /** + * Returns the height of the specified row. Returns zero if no model has + * been set. + */ + int getRowHeight(int row) const; + + /** Finds a row index that would be the most suitable place to insert a new + item for a given position. + + This is useful when the user is e.g. dragging and dropping onto the listbox, + because it lets you easily choose the best position to insert the item that + they drop, based on where they drop it. + + If the position is out of range, this will return -1. If the position is + beyond the end of the list, it will return getNumRows() to indicate the end + of the list. + + @see getComponentForRowNumber + */ + int getInsertionIndexForPosition(int x, int y) const noexcept; + + /** Returns the position of one of the rows, relative to the top-left of + the listbox. + + This may be off-screen, and the range of the row number that is passed-in is + not checked to see if it's a valid row. + */ + Rectangle getRowPosition(int rowNumber, bool relativeToComponentTopLeft) const noexcept; + + /** Finds the row component for a given row in the list. + + The component returned will have been created using ListBoxModel::refreshComponentForRow(). + + If the component for this row is off-screen or if the row is out-of-range, + this will return nullptr. + + @see getRowContainingPosition + */ + Component* getComponentForRowNumber(int rowNumber) const noexcept; + + /** Returns the row number that the given component represents. + If the component isn't one of the list's rows, this will return -1. + */ + int getRowNumberOfComponent(Component* rowComponent) const noexcept; + + /** Returns the width of a row (which may be less than the width of this component + if there's a scrollbar). + */ + int getVisibleRowWidth() const noexcept; + + //============================================================================== + + /** Returns the number of rows actually visible. + + This is the number of whole rows which will fit on-screen, so the value might + be more than the actual number of rows in the list. + */ + int getNumRowsOnScreen() const noexcept; + + //============================================================================== + /** A set of colour IDs to use to change the colour of various aspects of the label. + + These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() + methods. + + @see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour + */ + enum ColourIds { + backgroundColourId = 0x1002800, /**< The background colour to fill the list with. + Make this transparent if you don't want the background to be filled. */ + outlineColourId = 0x1002810, /**< An optional colour to use to draw a border around the list. + Make this transparent to not have an outline. */ + textColourId = 0x1002820 /**< The preferred colour to use for drawing text in the listbox. */ + }; + + /** Sets the thickness of a border that will be drawn around the box. + + To set the colour of the outline, use @code setColour (ListBox::outlineColourId, colourXYZ); @endcode + @see outlineColourId + */ + void setOutlineThickness(int outlineThickness); + + /** Returns the thickness of outline that will be drawn around the listbox. + @see setOutlineColour + */ + int getOutlineThickness() const noexcept { return outlineThickness; } + + /** Sets a component that the list should use as a header. + + This will position the given component at the top of the list, maintaining the + height of the component passed-in, but rescaling it horizontally to match the + width of the items in the listbox. + + The component will be deleted when setHeaderComponent() is called with a + different component, or when the listbox is deleted. + */ + void setHeaderComponent(std::unique_ptr newHeaderComponent); + + /** Returns whatever header component was set with setHeaderComponent(). */ + Component* getHeaderComponent() const noexcept { return headerComponent.get(); } + + /** Changes the width of the rows in the list. + + This can be used to make the list's row components wider than the list itself - the + width of the rows will be either the width of the list or this value, whichever is + greater, and if the rows become wider than the list, a horizontal scrollbar will + appear. + + The default value for this is 0, which means that the rows will always + be the same width as the list. + */ + void setMinimumContentWidth(int newMinimumWidth); + + /** Returns the space currently available for the row items, taking into account + borders, scrollbars, etc. + */ + int getVisibleContentWidth() const noexcept; + + /** Repaints one of the rows. + + This does not invoke updateContent(), it just invokes a straightforward repaint + for the area covered by this row. + */ + void repaintRow(int rowNumber) noexcept; + + /** This fairly obscure method creates an image that shows the row components specified + in rows (for example, these could be the currently selected row components). + + It's a handy method for doing drag-and-drop, as it can be passed to the + DragAndDropContainer for use as the drag image. + + Note that it will make the row components temporarily invisible, so if you're + using custom components this could affect them if they're sensitive to that + sort of thing. + + @see Component::createComponentSnapshot + */ + virtual Image createSnapshotOfRows(const SparseSet& rows, int& x, int& y); + + /** Returns the viewport that this ListBox uses. + + You may need to use this to change parameters such as whether scrollbars + are shown, etc. + */ + Viewport* getViewport() const noexcept; + + //============================================================================== + /** @internal */ + bool keyPressed(const KeyPress&) override; + /** @internal */ + bool keyStateChanged(bool isKeyDown) override; + /** @internal */ + void paint(Graphics&) override; + /** @internal */ + void paintOverChildren(Graphics&) override; + /** @internal */ + void resized() override; + /** @internal */ + void visibilityChanged() override; + /** @internal */ + void mouseWheelMove(const MouseEvent&, const MouseWheelDetails&) override; + /** @internal */ + void mouseUp(const MouseEvent&) override; + /** @internal */ + void colourChanged() override; + /** @internal */ + void parentHierarchyChanged() override; + /** @internal */ + void startDragAndDrop(const MouseEvent&, + const SparseSet& rowsToDrag, + const var& dragDescription, + bool allowDraggingToOtherWindows); + + private: + //============================================================================== + JUCE_PUBLIC_IN_DLL_BUILD(class ListViewport) + JUCE_PUBLIC_IN_DLL_BUILD(class RowComponent) + friend class ListViewport; + friend class TableListBox; + ListBoxModel* model; + std::unique_ptr viewport; + std::unique_ptr headerComponent; + std::unique_ptr mouseMoveSelector; + SparseSet selected; + int totalItems = 0, minimumRowWidth = 0; + int outlineThickness = 0; + int lastRowSelected = -1; + bool multipleSelection = false, alwaysFlipSelection = false, hasDoneInitialUpdate = false, selectOnMouseDown = true; + + void selectRowInternal(int rowNumber, bool dontScrollToShowThisRow, bool deselectOthersFirst, bool isMouseClick); + int getContentHeight() const; + int getRowForPosition(int y) const; + int getPositionForRow(int row) const; + +#if JUCE_CATCH_DEPRECATED_CODE_MISUSE + // This method's bool parameter has changed: see the new method signature. + JUCE_DEPRECATED(void setSelectedRows(const SparseSet&, bool)); + + // Rows can now have different heights. See getRowHeight(int rowNumber) instead. + JUCE_DEPRECATED(void getRowHeight()); + + // Row height is now set by the ListBoxModel as rows can have different heights. + // Implement ListBoxModel::getRowHeight(int rowNumber). + JUCE_DEPRECATED(void setRowHeight(int)); + + // This method has been replaced by the more flexible method createSnapshotOfRows. + // Please call createSnapshotOfRows (getSelectedRows(), x, y) to get the same behaviour. + JUCE_DEPRECATED(virtual void createSnapshotOfSelectedRows(int&, int&)) {} +#endif + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ListBox) + }; + + } // namespace jc +} // namespace juce \ No newline at end of file diff --git a/osci-render.jucer b/osci-render.jucer index ebdc90b..05f3b32 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -79,6 +79,9 @@ + + + +