diff --git a/CMakeLists.txt b/CMakeLists.txt index 1979e6f..b13e19d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,6 +132,7 @@ add_library(${PROJECT_NAME} STATIC src/sst/jucegui/components/HSliderFilled.cpp src/sst/jucegui/components/JogUpDownButton.cpp src/sst/jucegui/components/Knob.cpp + src/sst/jucegui/components/ListView.cpp src/sst/jucegui/components/MenuButton.cpp src/sst/jucegui/components/MultiSwitch.cpp src/sst/jucegui/components/NamedPanel.cpp diff --git a/examples/component-demo/ListViewDemo.h b/examples/component-demo/ListViewDemo.h new file mode 100644 index 0000000..df6020c --- /dev/null +++ b/examples/component-demo/ListViewDemo.h @@ -0,0 +1,180 @@ +/* + * sst-jucegui - an open source library of juce widgets + * built by Surge Synth Team. + * + * Copyright 2023-2024, various authors, as described in the GitHub + * transaction log. + * + * sst-jucegui is released under the MIT license, as described + * by "LICENSE.md" in this repository. This means you may use this + * in commercial software if you are a JUCE Licensee. If you use JUCE + * in the open source / GPL3 context, your combined work must be + * released under GPL3. + * + * All source in sst-jucegui available at + * https://github.com/surge-synthesizer/sst-jucegui + */ + +#ifndef SSTJUCEGUI_EXAMPLES_COMPONENT_DEMO_LISTVIEWDEMO_H +#define SSTJUCEGUI_EXAMPLES_COMPONENT_DEMO_LISTVIEWDEMO_H + +#include +#include +#include +#include +#include +#include "ExampleUtils.h" + +struct ListViewDemo : public sst::jucegui::components::WindowPanel +{ + static constexpr const char *name = "ListViews"; + + struct AList : juce::Component + { + struct RowComp : juce::Component + { + AList *parent{nullptr}; + RowComp(AList *parent) : parent(parent) {} + uint32_t row{0}; + bool selected{false}; + void setRow(uint32_t r) + { + row = r; + repaint(); + } + void paint(juce::Graphics &g) override + { + g.fillAll(juce::Colour((row * 17) % 255, 0, 120)); + if (selected) + g.setColour(juce::Colours::white); + else + g.setColour(juce::Colours::green); + g.setFont(12); + g.drawText(std::to_string(row), getLocalBounds(), juce::Justification::centred); + g.drawRect(getLocalBounds().reduced(1)); + } + void mouseDown(const juce::MouseEvent &e) override + { + parent->listView->rowSelected( + row, !selected, + sst::jucegui::components::ListView::selectionAddActionForModifier(e.mods)); + } + }; + int rowCount{275}, rowHeight{18}; + AList() + { + listView = std::make_unique(); + addAndMakeVisible(*listView); + + for (int i = 0; i < 3; ++i) + { + auto tb = std::make_unique(); + auto amt = (i == 0 ? 275 : i == 1 ? 11 : 750); + tb->setLabel(std::to_string(amt) + " rows"); + tb->setOnCallback([amt, w = juce::Component::SafePointer(this)]() { + if (!w) + return; + w->rowCount = amt; + w->listView->refresh(); + }); + addAndMakeVisible(*tb); + buttons[i] = std::move(tb); + } + + for (int i = 3; i < 5; ++i) + { + auto tb = std::make_unique(); + auto amt = (i == 3 ? 18 : 67); + tb->setLabel(std::to_string(amt) + " height"); + tb->setOnCallback([amt, w = juce::Component::SafePointer(this)]() { + if (!w) + return; + w->rowHeight = amt; + w->listView->refresh(); + }); + addAndMakeVisible(*tb); + buttons[i] = std::move(tb); + } + + for (int i = 5; i < 7; ++i) + { + auto tb = std::make_unique(); + auto amt = + (i == 5 ? sst::jucegui::components::ListView::SelectionMode::SINGLE_SELECTION + : sst::jucegui::components::ListView::SelectionMode::MULTI_SELECTION); + tb->setLabel(i == 5 ? "SingSel" : "MultSel"); + tb->setOnCallback([amt, w = juce::Component::SafePointer(this)]() { + if (!w) + return; + w->listView->setSelectionMode(amt); + w->listView->refresh(); + }); + addAndMakeVisible(*tb); + buttons[i] = std::move(tb); + } + + listView->getRowHeight = []() { return 18; }; + listView->getRowHeight = [w = juce::Component::SafePointer(this)]() { + if (!w) + return 0; + return w->rowHeight; + }; + listView->getRowCount = [w = juce::Component::SafePointer(this)]() { + if (!w) + return 0; + return w->rowCount; + }; + listView->makeRowComponent = [this]() { return std::make_unique(this); }; + listView->assignComponentToRow = [](const auto &rc, auto r) { + auto rcomp = dynamic_cast(rc.get()); + if (rcomp) + { + rcomp->setRow(r); + } + }; + listView->setRowSelection = [](const auto &rc, auto r) { + auto rcomp = dynamic_cast(rc.get()); + if (rcomp) + { + rcomp->selected = r; + rcomp->repaint(); + } + }; + listView->refresh(); + } + ~AList() {} + void resized() override + { + listView->setBounds(getLocalBounds().withTrimmedTop(25)); + auto br = getLocalBounds().withHeight(22); + auto bw = br.getWidth() / buttons.size(); + br = br.withWidth(bw); + for (const auto &b : buttons) + { + if (b) + { + b->setBounds(br.reduced(1)); + } + br = br.translated(bw, 0); + } + } + + std::unique_ptr listView; + + std::array, 7> buttons; + }; + + ListViewDemo() + { + panelOne = std::make_unique("List View"); + panelOne->setContentAreaComponent(std::make_unique()); + + addAndMakeVisible(*panelOne); + } + + void resized() override { panelOne->setBounds(getLocalBounds().reduced(10)); } + + std::unique_ptr panelOne; +}; + +#endif // SST_JUCEGUI_ListViewDemo_H diff --git a/examples/component-demo/SSTJuceGuiDemo.cpp b/examples/component-demo/SSTJuceGuiDemo.cpp index 72c9df1..37f4e9a 100644 --- a/examples/component-demo/SSTJuceGuiDemo.cpp +++ b/examples/component-demo/SSTJuceGuiDemo.cpp @@ -33,6 +33,7 @@ #include "SevenSegmentDemo.h" #include "VUMeterDemo.h" #include "ZoomContainerDemo.h" +#include "ListViewDemo.h" struct SSTJuceGuiDemo : public juce::JUCEApplication { @@ -133,6 +134,10 @@ struct SSTJuceGuiDemo : public juce::JUCEApplication mk(); mk(); mk(); + mk(); + + // Comment this out to also auto launch the last item + buttons.back()->onClick(); } void paint(juce::Graphics &g) override { g.fillAll(juce::Colours::black); } void resized() override diff --git a/include/sst/jucegui/components/ListView.h b/include/sst/jucegui/components/ListView.h new file mode 100644 index 0000000..7de56d3 --- /dev/null +++ b/include/sst/jucegui/components/ListView.h @@ -0,0 +1,96 @@ +/* + * sst-jucegui - an open source library of juce widgets + * built by Surge Synth Team. + * + * Copyright 2023-2024, various authors, as described in the GitHub + * transaction log. + * + * sst-jucegui is released under the MIT license, as described + * by "LICENSE.md" in this repository. This means you may use this + * in commercial software if you are a JUCE Licensee. If you use JUCE + * in the open source / GPL3 context, your combined work must be + * released under GPL3. + * + * All source in sst-jucegui available at + * https://github.com/surge-synthesizer/sst-jucegui + */ + +#ifndef INCLUDE_SST_JUCEGUI_COMPONENTS_LISTVIEW_H +#define INCLUDE_SST_JUCEGUI_COMPONENTS_LISTVIEW_H + +#include +#include +#include +#include +#include +#include + +#include "BaseStyles.h" +#include "Viewport.h" + +namespace sst::jucegui::components +{ +struct ListView : public juce::Component, + public style::StyleConsumer, + public style::SettingsConsumer +{ + struct Styles : base_styles::Base + { + SCLASS(listview); + + static void initialize() + { + style::StyleSheet::addClass(styleClass).withBaseClass(base_styles::Base::styleClass); + } + }; + + enum ComponentStrategy + { + BRUTE_FORCE, // just make a component per row. + BRUTE_FORCE_NO_REUSE + } strategy{BRUTE_FORCE}; + + enum SelectionMode + { + NO_SELECTION, + SINGLE_SELECTION, + MULTI_SELECTION + } selectionMode{SINGLE_SELECTION}; + + ListView(const juce::String &cn = juce::String()); + ~ListView(); + + void refresh(); + + void resized() override + { + viewPort->setBounds(getLocalBounds()); + refresh(); + } + + void setSelectionMode(SelectionMode s); + + enum SelectionAddAction + { + SINGLE, + ADD_NON_CONTIGUOUS, + ADD_CONTIGUOUS + }; + void rowSelected(uint32_t r, bool select, SelectionAddAction addMode = SINGLE); + static SelectionAddAction selectionAddActionForModifier(const juce::ModifierKeys &); + + std::function getRowCount{nullptr}; + std::function getRowHeight{nullptr}; + std::function()> makeRowComponent{nullptr}; + std::function &, uint32_t)> assignComponentToRow{ + nullptr}; + std::function &, bool)> setRowSelection{nullptr}; + + std::unique_ptr viewPort; + + struct Innards; + std::unique_ptr innards; +}; +} // namespace sst::jucegui::components + +#endif // LISTVIEW_H diff --git a/src/sst/jucegui/components/ListView.cpp b/src/sst/jucegui/components/ListView.cpp new file mode 100644 index 0000000..201882b --- /dev/null +++ b/src/sst/jucegui/components/ListView.cpp @@ -0,0 +1,202 @@ +/* + * sst-jucegui - an open source library of juce widgets + * built by Surge Synth Team. + * + * Copyright 2023-2024, various authors, as described in the GitHub + * transaction log. + * + * sst-jucegui is released under the MIT license, as described + * by "LICENSE.md" in this repository. This means you may use this + * in commercial software if you are a JUCE Licensee. If you use JUCE + * in the open source / GPL3 context, your combined work must be + * released under GPL3. + * + * All source in sst-jucegui available at + * https://github.com/surge-synthesizer/sst-jucegui + */ + +#include +#include "sst/jucegui/components/ListView.h" + +namespace sst::jucegui::components +{ +struct ListView::Innards : juce::Component +{ + std::vector> components; + std::unordered_set selectedRows; + int32_t contiguousStart{-1}; + + void pruneSelectionsAfterRefresh() + { + auto numrow = components.size(); + auto it = selectedRows.begin(); + while (it != selectedRows.end()) + { + if (*it > numrow) + { + it = selectedRows.erase(it); + } + else + { + it++; + } + } + contiguousStart = -1; + } +}; +ListView::ListView(const juce::String &cn) : StyleConsumer(Styles::styleClass) +{ + viewPort = std::make_unique(cn); + + innards = std::make_unique(); + viewPort->setViewedComponent(innards.get(), false); + + addAndMakeVisible(*viewPort); +} +ListView::~ListView() {} +void ListView::refresh() +{ + if (!getRowCount || !getRowHeight || !makeRowComponent || !assignComponentToRow) + return; + + auto rh = getRowHeight(); + auto rc = getRowCount(); + auto ht = rc * rh; + + innards->setBounds(0, 0, getWidth() - viewPort->getScrollBarThickness(), ht); + + // Brute force strategy + innards->removeAllChildren(); + auto ics = innards->components.size(); + if (ics < rc) + { + for (int i = innards->components.size(); i < rc; ++i) + { + auto c = makeRowComponent(); + innards->components.emplace_back(std::move(c)); + } + } + else if (ics > rc) + { + auto b = innards->components.begin(); + innards->components.erase(b + rc, innards->components.end()); + } + + uint32_t idx{0}; + for (const auto &c : innards->components) + { + assignComponentToRow(c, idx); + innards->addAndMakeVisible(*c); + c->setBounds(0, idx * rh, innards->getWidth(), rh); + idx++; + } + + innards->pruneSelectionsAfterRefresh(); + + repaint(); +} + +void ListView::rowSelected(uint32_t r, bool b, SelectionAddAction addMode) +{ + if (selectionMode == SINGLE_SELECTION || + (selectionMode == MULTI_SELECTION && addMode == SINGLE)) + { + assert(setRowSelection); + if (b) + { + for (auto &rs : innards->selectedRows) + { + if (rs != r) + { + setRowSelection(innards->components[rs], false); + } + } + innards->selectedRows.clear(); + innards->selectedRows.insert(r); + setRowSelection(innards->components[r], b); + } + else + { + innards->selectedRows.clear(); + setRowSelection(innards->components[r], b); + } + if (b) + innards->contiguousStart = r; + } + else if (addMode == ADD_CONTIGUOUS) + { + if (innards->contiguousStart < 0) + { + if (innards->selectedRows.empty()) + { + innards->contiguousStart = r; + } + else + { + innards->contiguousStart = *innards->selectedRows.begin(); + } + } + auto start = (uint32_t)innards->contiguousStart; + auto end = r; + if (start > end) + std::swap(start, end); + + auto it = innards->selectedRows.begin(); + while (it != innards->selectedRows.end()) + { + auto sr = *it; + if (sr < start || sr > end) + { + setRowSelection(innards->components[sr], false); + it = innards->selectedRows.erase(it); + } + else + { + it++; + } + } + for (int i = start; i <= end; ++i) + { + if (innards->selectedRows.find(i) == innards->selectedRows.end()) + { + setRowSelection(innards->components[i], true); + innards->selectedRows.insert(i); + } + } + } + else if (addMode == ADD_NON_CONTIGUOUS) + { + if (b) + { + setRowSelection(innards->components[r], true); + innards->selectedRows.insert(r); + } + else + { + setRowSelection(innards->components[r], true); + innards->selectedRows.erase(r); + } + } +} + +void ListView::setSelectionMode(SelectionMode s) +{ + selectionMode = s; + for (auto &rs : innards->selectedRows) + { + setRowSelection(innards->components[rs], false); + } + innards->selectedRows.clear(); + // TODO - cleanup selections +} + +ListView::SelectionAddAction ListView::selectionAddActionForModifier(const juce::ModifierKeys &mods) +{ + if (mods.isShiftDown()) + return ADD_CONTIGUOUS; + if (mods.isCommandDown()) + return ADD_NON_CONTIGUOUS; + return SINGLE; +} + +} // namespace sst::jucegui::components \ No newline at end of file