Skip to content
58 changes: 29 additions & 29 deletions src/lib/score/graphics/widgets/QGraphicsCombo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <QApplication>
#include <QGraphicsSceneMouseEvent>
#include <QMenu>
#include <QPainter>
#include <QScreen>

Expand Down Expand Up @@ -92,51 +93,50 @@ struct SCORE_LIB_BASE_EXPORT ComboBoxWithEnter final : public QComboBox

struct DefaultComboImpl
{
static void mousePressEvent(QGraphicsCombo& self, QGraphicsSceneMouseEvent* event)
// Native combobox behavior: the popup menu opens on press; QMenu handles
// both click-then-click and press-drag-release-on-item selection.
static void popupMenu(QGraphicsCombo& self, QGraphicsSceneMouseEvent* event)
{
if(event->button() == Qt::LeftButton)
QMenu menu;
for(int i = 0; i < self.array.size(); i++)
{
auto* act = menu.addAction(self.array[i]);
act->setCheckable(true);
act->setChecked(i == self.m_value);
act->setData(i);
}
if(auto* chosen = menu.exec(event->screenPos()))
{
self.m_grab = true;
InfiniteScroller::start(self, double(self.m_value) / (self.array.size() - 1));
const int idx = chosen->data().toInt();
if(idx != self.m_value)
{
self.m_value = idx;
self.update();
self.sliderMoved();
self.sliderReleased();
}
}
}

static void mousePressEvent(QGraphicsCombo& self, QGraphicsSceneMouseEvent* event)
{
if(event->button() == Qt::LeftButton && !self.array.empty())
{
popupMenu(self, event);
}
event->accept();
}

static void mouseMoveEvent(QGraphicsCombo& self, QGraphicsSceneMouseEvent* event)
{
if((event->buttons() & Qt::LeftButton) && self.m_grab)
{
double v = InfiniteScroller::move(event);
int curPos = std::round(v * (self.array.size() - 1));
if(curPos != self.m_value)
{
self.m_value = std::clamp(curPos, 0, int(self.array.size() - 1));
self.sliderMoved();
self.update();
}
}
event->accept();
}

static void mouseReleaseEvent(QGraphicsCombo& self, QGraphicsSceneMouseEvent* event)
{
if(event->button() == Qt::LeftButton)
{
InfiniteScroller::stop(self, event);

if(self.m_grab)
{
double v = InfiniteScroller::move(event);
int curPos = std::round(v * (self.array.size() - 1));
if(curPos != self.m_value)
{
self.m_value = std::clamp(curPos, 0, int(self.array.size() - 1));
self.update();
}
self.m_grab = false;
}
self.sliderReleased();
// Selection is handled by the popup opened on press.
}
else if(event->button() == Qt::RightButton)
{ /*
Expand Down
49 changes: 39 additions & 10 deletions src/plugins/score-lib-process/Process/Dataflow/ControlWidgets.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1333,15 +1333,15 @@ struct ComboBox
static auto make_widget(
T& inlet, const score::DocumentContext& ctx, QWidget* parent, QObject* context)
{
const auto& values = inlet.getValues();
auto sl = new score::ComboBox{parent};
for(auto& e : values)
for(auto& e : inlet.getValues())
{
sl->addItem(e.first);
}
sl->setContentsMargins(0, 0, 0, 0);

auto set_index = [values, sl](const ossia::value& val) {
auto set_index = [&inlet, sl](const ossia::value& val) {
const auto& values = inlet.getValues();
auto it
= ossia::find_if(values, [&](const auto& pair) { return pair.second == val; });
if(it != values.end())
Expand All @@ -1353,14 +1353,28 @@ struct ComboBox

QObject::connect(
sl, SignalUtils::QComboBox_currentIndexChanged_int(), context,
[values, &inlet, &ctx](int idx) {
CommandDispatcher<>{ctx.commandStack}.submit<SetControlValue<T>>(
inlet, values[idx].second);
[&inlet, &ctx](int idx) {
const auto& values = inlet.getValues();
if(idx >= 0 && idx < std::ssize(values))
CommandDispatcher<>{ctx.commandStack}.submit<SetControlValue<T>>(
inlet, values[idx].second);
});

QObject::connect(
&inlet, &T::valueChanged, sl, [=](const ossia::value& val) { set_index(val); });

if constexpr(requires { &T::alternativesChanged; })
{
QObject::connect(
&inlet, &T::alternativesChanged, sl, [&inlet, sl, set_index] {
QSignalBlocker b{sl};
sl->clear();
for(auto& e : inlet.getValues())
sl->addItem(e.first);
set_index(inlet.value());
});
}

return sl;
}

Expand All @@ -1380,7 +1394,8 @@ struct ComboBox
auto sl = new score::QGraphicsCombo{arr, nullptr};
initWidgetProperties(inlet, *sl);

auto set_index = [values, sl](const ossia::value& val) {
auto set_index = [&slider, sl](const ossia::value& val) {
const auto& values = slider.getValues();
auto it
= ossia::find_if(values, [&](const auto& pair) { return pair.second == val; });
if(it != values.end())
Expand All @@ -1391,10 +1406,12 @@ struct ComboBox
set_index(inlet.value());

QObject::connect(
sl, &score::QGraphicsCombo::sliderMoved, context, [values, sl, &inlet, &ctx] {
sl, &score::QGraphicsCombo::sliderMoved, context, [&slider, sl, &inlet, &ctx] {
sl->moving = true;
ctx.dispatcher.submit<SetControlValue<Control_T>>(
inlet, values[sl->value()].second);
const auto& values = slider.getValues();
if(sl->value() >= 0 && sl->value() < std::ssize(values))
ctx.dispatcher.submit<SetControlValue<Control_T>>(
inlet, values[sl->value()].second);
});
QObject::connect(sl, &score::QGraphicsCombo::sliderReleased, context, [sl, &ctx] {
ctx.dispatcher.commit();
Expand All @@ -1408,6 +1425,18 @@ struct ComboBox
set_index(val);
});

if constexpr(requires { &Control_T::alternativesChanged; })
{
QObject::connect(
&inlet, &Control_T::alternativesChanged, sl, [&slider, sl, set_index, &inlet] {
sl->array.clear();
for(auto& e : slider.getValues())
sl->array.push_back(e.first);
set_index(inlet.value());
sl->update();
});
}

return sl;
}
};
Expand Down
89 changes: 87 additions & 2 deletions src/plugins/score-lib-process/Process/Dataflow/WidgetInlets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
#include <ossia-qt/invoke.hpp>

#include <QCoreApplication>
#include <QDir>
#include <QFileSystemWatcher>

#include <wobjectimpl.h>

W_OBJECT_IMPL(Process::ComboBox)
W_OBJECT_IMPL(Process::FileChooserBase)
W_OBJECT_IMPL(Process::FileChooser)
W_OBJECT_IMPL(Process::FolderChooser)
Expand Down Expand Up @@ -106,6 +109,72 @@ void ComboBox::setupExecution(ossia::inlet& inl, QObject* exec_context) const no
port.domain = domain().get();
}

void ComboBox::repopulateFromFolder(const QString& folderPath)
{
std::vector<std::pair<QString, ossia::value>> alts;
if(!folderPath.isEmpty())
{
QDir dir(folderPath);
const auto files = fileExtensions.isEmpty()
? dir.entryList(QDir::Files, QDir::Name)
: dir.entryList(fileExtensions, QDir::Files, QDir::Name);
for(const auto& f : files)
alts.emplace_back(f, f.toStdString()); // value = the file name itself
}
if(alts.empty())
alts.emplace_back(QStringLiteral("-"), std::string("-"));
setAlternatives(std::move(alts));

// Watch the folder so files created while the document is open (e.g. by an
// upstream process writing its analysis) appear in the list right away.
if(folderPath != m_watchedFolder)
{
if(m_folderWatcher)
{
delete m_folderWatcher;
m_folderWatcher = nullptr;
}
m_watchedFolder = folderPath;
if(!folderPath.isEmpty() && QDir{folderPath}.exists())
{
m_folderWatcher = new QFileSystemWatcher{{folderPath}, this};
connect(
m_folderWatcher, &QFileSystemWatcher::directoryChanged, this,
[this](const QString& path) { repopulateFromFolder(path); });
}
}
}

void ComboBox::setAlternatives(std::vector<std::pair<QString, ossia::value>> values)
{
if(values == alternatives)
return;
alternatives = std::move(values);

std::vector<ossia::value> vals;
for(auto& v : alternatives)
vals.push_back(v.second);
setDomain(State::Domain{ossia::make_domain(vals)});

// Keep the current value if it is still in the list; otherwise fall back to
// the init value when available. A value absent from the list is left
// untouched: for folder-backed comboboxes it may name a file that simply
// does not exist *yet* — it becomes selectable (and shows up as selected)
// once the file appears.
if(!alternatives.empty())
{
auto in_list = [this](const ossia::value& v) {
return ossia::find_if(
alternatives, [&](const auto& pair) { return pair.second == v; })
!= alternatives.end();
};
if(!in_list(value()) && in_list(init()))
setValue(init());
}

alternativesChanged();
}

ComboBox::~ComboBox() { }

ComboBox::ComboBox(JSONObject::Deserializer& vis, QObject* parent)
Expand Down Expand Up @@ -1328,25 +1397,41 @@ SCORE_LIB_PROCESS_EXPORT void
DataStreamReader::read(const Process::ComboBox& p)
{
read((const Process::ControlInlet&)p);
m_stream << p.alternatives;
m_stream << p.alternatives << p.folderPortName << p.fileExtensions;
}
template <>
SCORE_LIB_PROCESS_EXPORT void
DataStreamWriter::write(Process::ComboBox& p)
{
m_stream >> p.alternatives;
m_stream >> p.alternatives >> p.folderPortName >> p.fileExtensions;
}
template <>
SCORE_LIB_PROCESS_EXPORT void
JSONReader::read(const Process::ComboBox& p)
{
read((const Process::ControlInlet&)p);
obj["Values"] = p.alternatives;
// Folder-backed combobox metadata (empty for a plain combobox). Persisted so
// a loaded document can re-scan the folder and re-establish live refresh;
// see ProcessModel::init_folder_comboboxes. Extensions are stored as one
// space-joined string ("*.json *.wav"), like FileChooser's filters.
obj["FolderPort"] = p.folderPortName;
obj["FileExtensions"] = p.fileExtensions.join(QChar(' '));
}
template <>
SCORE_LIB_PROCESS_EXPORT void JSONWriter::write(Process::ComboBox& p)
{
p.alternatives <<= obj["Values"];
// Guarded for documents saved before these fields existed.
if(obj.tryGet("FolderPort"))
p.folderPortName <<= obj["FolderPort"];
if(obj.tryGet("FileExtensions"))
{
QString exts;
exts <<= obj["FileExtensions"];
p.fileExtensions
= exts.isEmpty() ? QStringList{} : exts.split(QChar(' '), Qt::SkipEmptyParts);
}
}
template <>
SCORE_LIB_PROCESS_EXPORT void
Expand Down
23 changes: 23 additions & 0 deletions src/plugins/score-lib-process/Process/Dataflow/WidgetInlets.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

#include <ossia/network/domain/domain.hpp>

class QFileSystemWatcher;

namespace Process
{
struct FloatSlider;
Expand Down Expand Up @@ -442,13 +444,34 @@ struct SCORE_LIB_PROCESS_EXPORT ProgramEdit : public Process::ControlInlet

struct SCORE_LIB_PROCESS_EXPORT ComboBox : public Process::ControlInlet
{
W_OBJECT(ComboBox)
public:
MODEL_METADATA_IMPL(ComboBox)
std::vector<std::pair<QString, ossia::value>> alternatives;
ComboBox(
std::vector<std::pair<QString, ossia::value>> values, ossia::value init,
const QString& name, Id<Process::Port> id, QObject* parent);
~ComboBox();

void setAlternatives(std::vector<std::pair<QString, ossia::value>> values);
void alternativesChanged() W_SIGNAL(alternativesChanged);

// Folder-backed combobox (halp::folder_combobox): items are the files of the
// sibling port named folderPortName, filtered by fileExtensions ("*.wav"…).
// Empty folderPortName = plain combobox. Populated at edit/load time, and
// refreshed by a filesystem watcher while the document is open so that files
// written by other processes (an upstream analysis, a recorder…) show up
// without reloading.
QString folderPortName;
QStringList fileExtensions;
void repopulateFromFolder(const QString& folderPath);

private:
QFileSystemWatcher* m_folderWatcher{};
QString m_watchedFolder;

public:

const auto& getValues() const noexcept { return alternatives; }
auto count() const noexcept { return alternatives.size(); }

Expand Down
Loading
Loading