|
- /*
- ==============================================================================
-
- This file is part of the JUCE examples.
- Copyright (c) 2022 - Raw Material Software Limited
-
- The code included in this file is provided under the terms of the ISC license
- http://www.isc.org/downloads/software-support-policy/isc-license. Permission
- To use, copy, modify, and/or distribute this software for any purpose with or
- without fee is hereby granted provided that the above copyright notice and
- this permission notice appear in all copies.
-
- THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
- WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
- PURPOSE, ARE DISCLAIMED.
-
- ==============================================================================
- */
-
- /*******************************************************************************
- The block below describes the properties of this PIP. A PIP is a short snippet
- of code that can be read by the Projucer and used to generate a JUCE project.
-
- BEGIN_JUCE_PIP_METADATA
-
- name: CapabilityInquiryDemo
- version: 1.0.0
- vendor: JUCE
- website: http://juce.com
- description: Performs MIDI Capability Inquiry transactions
-
- dependencies: juce_audio_basics, juce_audio_devices, juce_core,
- juce_data_structures, juce_events, juce_graphics,
- juce_gui_basics, juce_midi_ci
- exporters: xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone
-
- moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
-
- type: Component
- mainClass: CapabilityInquiryDemo
-
- useLocalCopy: 1
-
- END_JUCE_PIP_METADATA
-
- *******************************************************************************/
-
- #pragma once
-
- /** This allows listening for changes to some data. Unlike ValueTree, it remembers the types
- of all the data that's stored, which makes it a bit nicer to use. Also unlike ValueTree,
- every mutation necessitates a deep copy of the model, so this isn't necessarily suitable
- for large models that change frequently.
-
- Use operator-> or operator* to find the current state (without copying!).
-
- Use operator= to assign new state.
- After assigning new state, the old and new states will be compared, and
- observers will be notified if the new state is different to the old state.
-
- Use operator[] to retrieve nested states.
- This is useful when a component only depends on a small part of a larger
- model. Assigning a new value to the nested state will also cause observers
- of the parent state to be notified. Similarly, assigning a new state to
- the parent state will cause observers of the nested state to be notified,
- if the nested state actually changes in value.
- */
- template <typename Value>
- class State
- {
- public:
- State() : State (Value{}) {}
-
- State (Value v, std::function<Value (const Value&, Value)> n)
- : impl (std::make_unique<Root> (std::move (v), std::move (n))) {}
-
- explicit State (Value v)
- : State (std::move (v), [] (const Value&, Value newer) { return std::move (newer); }) {}
-
- template <typename Parent>
- State (State<Parent> p, Value Parent::* member)
- : impl (std::make_unique<Member<Parent>> (std::move (p), member)) {}
-
- template <typename Parent, typename Ind>
- State (State<Parent> p, Ind index)
- : impl (std::make_unique<Index<Parent, Ind>> (std::move (p), std::move (index))) {}
-
- State& operator= (const Value& v)
- {
- impl->set (v);
- return *this;
- }
-
- State& operator= (Value&& v)
- {
- impl->set (std::move (v));
- return *this;
- }
-
- const Value& operator* () const { return impl->get(); }
- const Value* operator->() const { return &impl->get(); }
-
- ErasedScopeGuard observe (std::function<void (const Value&)> onChange)
- {
- // The idea is that observers want to know whenever the state changes, in order to repaint.
- // They'll also want to repaint upon first observing, so that painting is up-to-date.
- onChange (impl->get());
-
- // In the case that observe is called on a temporary, caching impl means that the
- // ErasedScopeGuard can still detach safely
- return impl->observe ([cached = *this, fn = std::move (onChange)] (const auto& x)
- {
- fn (x);
- });
- }
-
- template <typename T>
- auto operator[] (T member) const
- -> State<std::decay_t<decltype (std::declval<Value>().*std::declval<T>())>>
- {
- return { *this, std::move (member) };
- }
-
- template <typename T>
- auto operator[] (T index) const
- -> State<std::decay_t<decltype (std::declval<Value>()[std::declval<T>()])>>
- {
- return { *this, std::move (index) };
- }
-
- private:
- class Provider : public std::enable_shared_from_this<Provider>
- {
- public:
- virtual ~Provider() = default;
- virtual void set (const Value&) = 0;
- virtual void set (Value&&) = 0;
- virtual const Value& get() const = 0;
-
- ErasedScopeGuard observe (std::function<void (const Value&)> fn)
- {
- return ErasedScopeGuard { [t = this->shared_from_this(), it = list.insert (list.end(), std::move (fn))]
- {
- t->list.erase (it);
- } };
- }
-
- void notify (const Value& v) const
- {
- for (auto& callback : list)
- callback (v);
- }
-
- private:
- using List = std::list<std::function<void (const Value&)>>;
- List list;
- };
-
- class Root final : public Provider
- {
- public:
- explicit Root (Value v, std::function<Value (const Value&, Value&&)> n)
- : value (std::move (v)), normalise (std::move (n)) {}
-
- void set (const Value& m) override { setImpl (m); }
- void set (Value&& m) override { setImpl (std::move (m)); }
- const Value& get() const override { return value; }
-
- private:
- template <typename T>
- void setImpl (T&& t)
- {
- // If this fails, you're updating the model from inside an observer callback.
- // That's not very unidirectional-data-flow of you!
- jassert (! reentrant);
- const ScopedValueSetter scope { reentrant, true };
-
- auto normalised = normalise (value, std::forward<T> (t));
-
- if (normalised != value)
- this->notify (std::exchange (value, std::move (normalised)));
- }
-
- Value value;
- std::function<Value (const Value&, Value)> normalise;
- bool reentrant = false;
- };
-
- template <typename Parent>
- class Member final : public Provider
- {
- public:
- Member (State<Parent> p, Value Parent::* m) : parent (std::move (p)), member (m) {}
-
- void set (const Value& m) override { setImpl (m); }
- void set (Value&& m) override { setImpl (std::move (m)); }
- const Value& get() const override { return (*parent).*member; }
-
- private:
- template <typename T>
- void setImpl (T&& t)
- {
- auto updated = *parent;
- updated.*member = std::forward<T> (t);
- parent = std::move (updated);
- }
-
- State<Parent> parent;
- Value Parent::*member;
- ErasedScopeGuard listener = parent.observe ([this] (const auto& old)
- {
- if (old.*member != get())
- this->notify (old.*member);
- });
- };
-
- template <typename Parent, typename Ind>
- class Index final : public Provider
- {
- public:
- Index (State<Parent> p, Ind i) : parent (std::move (p)), index (i) {}
-
- void set (const Value& m) override { setImpl (m); }
- void set (Value&& m) override { setImpl (std::move (m)); }
- const Value& get() const override { return (*parent)[index]; }
-
- private:
- template <typename T>
- void setImpl (T&& t)
- {
- auto updated = *parent;
- updated[index] = std::forward<T> (t);
- parent = std::move (updated);
- }
-
- State<Parent> parent;
- Ind index;
- ErasedScopeGuard listener = parent.observe ([this] (const auto& old)
- {
- if (isPositiveAndBelow (index, std::size (*parent))
- && isPositiveAndBelow (index, std::size (old))
- && old[index] != get())
- {
- this->notify (old[index]);
- }
- });
- };
-
- std::shared_ptr<Provider> impl;
- };
-
- /**
- Data types used to represent the state of the application.
- These should all be types with value semantics, so there should not be pointers or references
- between different parts of the model.
- */
- struct Model
- {
- Model() = delete;
-
- template <typename Range, typename Fn>
- static Array<var> toVarArray (Range&& range, Fn&& fn)
- {
- Array<var> result;
- result.resize ((int) std::size (range));
- std::transform (std::begin (range),
- std::end (range),
- result.begin(),
- std::forward<Fn> (fn));
- return result;
- }
-
- template <typename Fn, typename T>
- static auto fromVarArray (var in, Fn&& fn, std::vector<T> fallback) -> std::vector<T>
- {
- auto* list = in.getArray();
-
- if (list == nullptr)
- return fallback;
-
- std::vector<T> result;
- for (auto& t : *list)
- result.push_back (fn (t));
- return result;
- }
-
- #define JUCE_TUPLE_RELATIONAL_OP(classname, op) \
- bool operator op (const classname& other) const \
- { \
- return tie() op other.tie(); \
- }
-
- #define JUCE_TUPLE_RELATIONAL_OPS(classname) \
- JUCE_TUPLE_RELATIONAL_OP(classname, ==) \
- JUCE_TUPLE_RELATIONAL_OP(classname, !=)
-
- template <typename T>
- struct ListWithSelection
- {
- std::vector<T> items;
- int selection = -1;
-
- static constexpr auto marshallingVersion = std::nullopt;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("items", t.items),
- named ("selection", t.selection));
- }
-
- auto tie() const { return std::tie (items, selection); }
- JUCE_TUPLE_RELATIONAL_OPS (ListWithSelection)
-
- private:
- template <typename This, typename Index>
- static auto* get (This& t, Index index)
- {
- if (isPositiveAndBelow (index, t.items.size()))
- return &t.items[(size_t) index];
-
- return static_cast<decltype (t.items.data())> (nullptr);
- }
-
- template <typename This>
- static auto* getSelected (This& t)
- {
- return get (t, t.selection);
- }
-
- public:
- auto* getSelected() { return getSelected (*this); }
- auto* getSelected() const { return getSelected (*this); }
-
- auto* get (int index) { return get (*this, index); }
- auto* get (int index) const { return get (*this, index); }
- };
-
- enum class ProfileMode
- {
- edit,
- use,
- };
-
- struct Profiles
- {
- ListWithSelection<ci::Profile> profiles;
- std::map<ci::ProfileAtAddress, ci::SupportedAndActive> channels;
- std::optional<ci::ChannelAddress> selectedChannel;
- ProfileMode profileMode{};
-
- std::optional<ci::ProfileAtAddress> getSelectedProfileAtAddress() const
- {
- if (selectedChannel.has_value())
- if (auto* item = profiles.getSelected())
- return ci::ProfileAtAddress { *item, *selectedChannel };
-
- return {};
- }
-
- static constexpr auto marshallingVersion = 0;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("profiles", t.profiles),
- named ("channels", t.channels),
- named ("selectedChannel", t.selectedChannel),
- named ("profileMode", t.profileMode));
- }
-
- auto tie() const { return std::tie (profiles, channels, selectedChannel, profileMode); }
- JUCE_TUPLE_RELATIONAL_OPS (Profiles)
- };
-
- enum class DataViewMode
- {
- ascii,
- hex,
- };
-
- #define JUCE_CAN_SET X(none) X(full) X(partial)
-
- enum class CanSet
- {
- #define X(name) name,
- JUCE_CAN_SET
- #undef X
- };
-
- struct CanSetUtils
- {
- CanSetUtils() = delete;
-
- static std::optional<CanSet> fromString (const char* str)
- {
- #define X(name) if (StringRef (str) == StringRef (#name)) return CanSet::name;
- JUCE_CAN_SET
- #undef X
-
- return std::nullopt;
- }
-
- static const char* toString (CanSet c)
- {
- switch (c)
- {
- #define X(name) case CanSet::name: return #name;
- JUCE_CAN_SET
- #undef X
- }
-
- return nullptr;
- }
- };
-
- #undef JUCE_CAN_SET
-
- struct PropertyValue
- {
- std::vector<std::byte> bytes; ///< decoded bytes
- String mediaType = "application/json";
-
- static constexpr auto marshallingVersion = 0;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("bytes", t.bytes),
- named ("mediaType", t.mediaType));
- }
-
- auto tie() const { return std::tie (bytes, mediaType); }
- JUCE_TUPLE_RELATIONAL_OPS (PropertyValue)
- };
-
- struct ReadableDeviceInfo
- {
- String manufacturer, family, model, version;
- };
-
- struct Property
- {
- String name;
- var schema;
- std::set<ci::Encoding> encodings { ci::Encoding::ascii };
- std::vector<String> mediaTypes { "application/json" };
- var columns;
- CanSet canSet = CanSet::none;
- bool canGet = true, canSubscribe = false, requireResId = false, canPaginate = false;
- PropertyValue value;
-
- [[nodiscard]] std::optional<ci::Encoding> getBestCommonEncoding (
- ci::Encoding firstChoice = ci::Encoding::ascii) const
- {
- if (encodings.count (firstChoice) != 0)
- return firstChoice;
-
- if (! encodings.empty())
- return *encodings.begin();
-
- return {};
- }
-
- std::optional<ReadableDeviceInfo> getReadableDeviceInfo() const
- {
- if (name != "DeviceInfo")
- return {};
-
- const auto parsed = ci::Encodings::jsonFrom7BitText (value.bytes);
- auto* object = parsed.getDynamicObject();
-
- if (object == nullptr)
- return {};
-
- if (! object->hasProperty ("manufacturer")
- || ! object->hasProperty ("family")
- || ! object->hasProperty ("model")
- || ! object->hasProperty ("version"))
- {
- return {};
- }
-
- return ReadableDeviceInfo { object->getProperty ("manufacturer"),
- object->getProperty ("family"),
- object->getProperty ("model"),
- object->getProperty ("version") };
- }
-
- static Property fromResourceListEntry (var entry)
- {
- Model::Property p;
- p.name = entry.getProperty ("resource", "");
- p.canGet = entry.getProperty ("canGet", p.canGet);
- const auto set = entry.getProperty ("canSet", {}).toString();
- p.canSet = Model::CanSetUtils::fromString (set.toRawUTF8()).value_or (p.canSet);
- p.canSubscribe = entry.getProperty ("canSubscribe", p.canSubscribe);
- p.requireResId = entry.getProperty ("requireResId", p.requireResId);
- p.mediaTypes = fromVarArray (entry.getProperty ("mediaTypes", {}),
- [] (String str) { return str; },
- p.mediaTypes);
- p.schema = entry.getProperty ("schema", p.schema);
- p.canPaginate = entry.getProperty ("canPaginate", p.canPaginate);
- p.columns = entry.getProperty ("columns", p.columns);
-
- const auto stringToEncoding = [] (String str)
- {
- return ci::EncodingUtils::toEncoding (str.toRawUTF8());
- };
- const auto parsedEncodings = fromVarArray (entry.getProperty ("encodings", {}),
- stringToEncoding,
- std::vector<std::optional<ci::Encoding>>{});
- std::set<ci::Encoding> encodingsSet;
- for (const auto& parsed : parsedEncodings)
- if (parsed.has_value())
- encodingsSet.insert (*parsed);
-
- p.encodings = encodingsSet.empty() ? p.encodings : encodingsSet;
- return p;
- }
-
- var getResourceListEntry() const
- {
- const Model::Property defaultInfo;
- auto obj = std::make_unique<DynamicObject>();
-
- obj->setProperty ("resource", name);
-
- if (canGet != defaultInfo.canGet)
- obj->setProperty ("canGet", canGet);
-
- if (canSet != defaultInfo.canSet)
- obj->setProperty ("canSet", Model::CanSetUtils::toString (canSet));
-
- if (canSubscribe != defaultInfo.canSubscribe)
- obj->setProperty ("canSubscribe", canSubscribe);
-
- if (requireResId != defaultInfo.requireResId)
- obj->setProperty ("requireResId", requireResId);
-
- if (mediaTypes != defaultInfo.mediaTypes)
- {
- obj->setProperty ("mediaTypes",
- toVarArray (mediaTypes, [] (auto str) { return str; }));
- }
-
- if (encodings != defaultInfo.encodings)
- {
- obj->setProperty ("encodings",
- toVarArray (encodings, ci::EncodingUtils::toString));
- }
-
- if (schema != defaultInfo.schema)
- obj->setProperty ("schema", schema);
-
- if (name.endsWith ("List"))
- {
- if (canPaginate != defaultInfo.canPaginate)
- obj->setProperty ("canPaginate", canPaginate);
-
- if (columns != defaultInfo.columns)
- obj->setProperty ("columns", columns);
- }
-
- return obj.release();
- }
-
- static constexpr auto marshallingVersion = 0;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("name", t.name),
- named ("schema", t.schema),
- named ("encodings", t.encodings),
- named ("mediaTypes", t.mediaTypes),
- named ("columns", t.columns),
- named ("canSet", t.canSet),
- named ("canGet", t.canGet),
- named ("canSubscribe", t.canSubscribe),
- named ("requireResId", t.requireResId),
- named ("canPaginate", t.canPaginate),
- named ("value", t.value));
- }
-
- auto tie() const
- {
- return std::tie (name,
- schema,
- encodings,
- mediaTypes,
- columns,
- canSet,
- canGet,
- canSubscribe,
- requireResId,
- canPaginate,
- value);
- }
-
- JUCE_TUPLE_RELATIONAL_OPS (Property)
- };
-
- struct Properties
- {
- ListWithSelection<Property> properties;
- DataViewMode mode{};
-
- std::optional<ReadableDeviceInfo> getReadableDeviceInfo() const
- {
- for (const auto& prop : properties.items)
- if (auto info = prop.getReadableDeviceInfo())
- return info;
-
- return {};
- }
-
- static constexpr auto marshallingVersion = 0;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("properties", t.properties),
- named ("mode", t.mode));
- }
-
- auto tie() const { return std::tie (properties, mode); }
- JUCE_TUPLE_RELATIONAL_OPS (Properties)
- };
-
- struct IOSelection
- {
- std::optional<MidiDeviceInfo> input, output;
-
- static constexpr auto marshallingVersion = 0;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("input", t.input),
- named ("output", t.output));
- }
-
- auto tie() const { return std::tie (input, output); }
- JUCE_TUPLE_RELATIONAL_OPS (IOSelection)
- };
-
- struct DeviceInfo
- {
- ump::DeviceInfo deviceInfo;
- size_t maxSysExSize { 512 };
- uint8_t numPropertyExchangeTransactions { 127 };
- bool profilesSupported { false }, propertiesSupported { false };
-
- static constexpr auto marshallingVersion = 0;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("deviceInfo", t.deviceInfo),
- named ("maxSysExSize", t.maxSysExSize),
- named ("numPropertyExchangeTransactions",
- t.numPropertyExchangeTransactions),
- named ("profilesSupported", t.profilesSupported),
- named ("propertiesSupported", t.propertiesSupported));
- }
-
- auto tie() const
- {
- return std::tie (deviceInfo,
- maxSysExSize,
- numPropertyExchangeTransactions,
- profilesSupported,
- propertiesSupported);
- }
- JUCE_TUPLE_RELATIONAL_OPS (DeviceInfo)
- };
-
- enum class MessageKind
- {
- incoming,
- outgoing,
- };
-
- struct LogView
- {
- std::optional<MessageKind> filter;
- DataViewMode mode = DataViewMode::ascii;
-
- static constexpr auto marshallingVersion = 0;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("filter", t.filter),
- named ("mode", t.mode));
- }
-
- auto tie() const { return std::tie (filter, mode); }
- JUCE_TUPLE_RELATIONAL_OPS (LogView)
- };
-
- /**
- The bits of the app state that we want to save and restore
- */
- struct Saved
- {
- DeviceInfo fundamentals;
- IOSelection ioSelection;
- Profiles profiles;
- Properties properties;
- LogView logView;
-
- static constexpr auto marshallingVersion = 0;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("fundamentals", t.fundamentals),
- named ("ioSelection", t.ioSelection),
- named ("profiles", t.profiles),
- named ("properties", t.properties),
- named ("logView", t.logView));
- }
-
- auto tie() const
- {
- return std::tie (fundamentals,
- ioSelection,
- profiles,
- properties,
- logView);
- }
- JUCE_TUPLE_RELATIONAL_OPS (Saved)
- };
-
- struct Device
- {
- ci::MUID muid = ci::MUID::makeUnchecked (0);
- DeviceInfo info;
- Profiles profiles;
- Properties properties;
- std::map<String, String> subscribeIdForResource;
-
- std::optional<String> getSubscriptionId (const String& resource) const
- {
- const auto iter = subscribeIdForResource.find (resource);
- return iter != subscribeIdForResource.end() ? std::optional (iter->second)
- : std::nullopt;
- }
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("muid", t.muid),
- named ("info", t.info),
- named ("profiles", t.profiles),
- named ("properties", t.properties),
- named ("subscribeIdForResource", t.subscribeIdForResource));
- }
-
- auto tie() const
- {
- return std::tie (muid, info, profiles, properties, subscribeIdForResource);
- }
- JUCE_TUPLE_RELATIONAL_OPS (Device)
- };
-
- struct LogEntry
- {
- std::vector<std::byte> message;
- uint8_t group{};
- Time time;
- MessageKind kind;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("message", t.message),
- named ("group", t.group),
- named ("time", t.time),
- named ("kind", t.kind));
- }
-
- auto tie() const { return std::tie (message, group, time, kind); }
- JUCE_TUPLE_RELATIONAL_OPS (LogEntry)
- };
-
- /**
- App state that needs to be displayed somehow, but which shouldn't be saved or restored.
- */
- struct Transient
- {
- // property -> device -> subId
- std::map<String, std::map<ci::MUID, std::set<String>>> subscribers;
- ListWithSelection<Device> devices;
- std::deque<LogEntry> logEntries;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("subscribers", t.subscribers),
- named ("devices", t.devices),
- named ("logEntries", t.logEntries));
- }
-
- auto tie() const { return std::tie (subscribers, devices, logEntries); }
- JUCE_TUPLE_RELATIONAL_OPS (Transient)
- };
-
- struct App
- {
- Saved saved;
- Transient transient;
-
- /** Removes properties from Transient::subscribers that are no longer present in
- Properties::properties.
- */
- void syncSubscribers()
- {
- std::set<String> currentProperties;
- for (auto& v : saved.properties.properties.items)
- currentProperties.insert (v.name);
-
- std::set<String> removedProperties;
- for (const auto& oldSubscriber : transient.subscribers)
- if (currentProperties.find (oldSubscriber.first) == currentProperties.end())
- removedProperties.insert (oldSubscriber.first);
-
- for (const auto& removed : removedProperties)
- transient.subscribers.erase (removed);
- }
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& t)
- {
- return archive (named ("saved", t.saved),
- named ("transient", t.transient));
- }
-
- auto tie() const { return std::tie (saved, transient); }
- JUCE_TUPLE_RELATIONAL_OPS (App)
- };
-
- #undef JUCE_TUPLE_RELATIONAL_OPS
- #undef JUCE_TUPLE_RELATIONAL_OP
- };
-
- template <>
- struct juce::SerialisationTraits<MidiDeviceInfo>
- {
- static constexpr auto marshallingVersion = std::nullopt;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& x)
- {
- archive (named ("name", x.name), named ("identifier", x.identifier));
- }
- };
-
- template <>
- struct juce::SerialisationTraits<ci::ChannelAddress>
- {
- static constexpr auto marshallingVersion = std::nullopt;
-
- template <typename Archive>
- static auto load (Archive& archive, ci::ChannelAddress& x)
- {
- auto group = x.getGroup();
- auto channel = x.getChannel();
- archive (named ("group", group),
- named ("channel", channel));
- x = ci::ChannelAddress{}.withGroup (group).withChannel (channel);
- }
-
- template <typename Archive>
- static auto save (Archive& archive, const ci::ChannelAddress& x)
- {
- archive (named ("group", x.getGroup()),
- named ("channel", x.getChannel()));
- }
- };
-
- template <>
- struct juce::SerialisationTraits<ci::ProfileAtAddress>
- {
- static constexpr auto marshallingVersion = std::nullopt;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& x)
- {
- archive (named ("profile", x.profile), named ("address", x.address));
- }
- };
-
- template <>
- struct juce::SerialisationTraits<ci::SupportedAndActive>
- {
- static constexpr auto marshallingVersion = std::nullopt;
-
- template <typename Archive, typename This>
- static auto serialise (Archive& archive, This& x)
- {
- archive (named ("supported", x.supported), named ("active", x.active));
- }
- };
-
- class MonospaceEditor : public TextEditor
- {
- public:
- MonospaceEditor()
- {
- setFont (Font { Font::getDefaultMonospacedFontName(), 12, 0 });
- }
-
- void onCommit (std::function<void()> fn)
- {
- onEscapeKey = onReturnKey = onFocusLost = std::move (fn);
- }
-
- void set (const String& str)
- {
- setText (str, false);
- }
- };
-
- class MonospaceLabel : public Label
- {
- public:
- MonospaceLabel()
- {
- setFont (Font { Font::getDefaultMonospacedFontName(), 12, 0 });
- setMinimumHorizontalScale (1.0f);
- setInterceptsMouseClicks (false, false);
- }
-
- void onCommit (std::function<void()>) {}
-
- void set (const String& str)
- {
- setText (str, dontSendNotification);
- }
- };
-
- enum class Editable
- {
- no,
- yes,
- };
-
- template <Editable>
- class TextField;
-
- template <>
- class TextField<Editable::yes> : public MonospaceEditor { using MonospaceEditor::MonospaceEditor; };
-
- template <>
- class TextField<Editable::no> : public MonospaceLabel { using MonospaceLabel::MonospaceLabel; };
-
- struct Utils
- {
- Utils() = delete;
-
- static constexpr auto padding = 10;
- static constexpr auto standardControlHeight = 30;
-
- template <typename T, typename... Ts>
- static auto makeVector (T&& t, Ts&&... ts)
- {
- std::vector<T> result;
- result.reserve (1 + sizeof... (ts));
- result.emplace_back (std::forward<T> (t));
- (result.emplace_back (std::forward<Ts> (ts)), ...);
- return result;
- }
-
- static std::unique_ptr<Label> makeListRowLabel (StringRef text, bool selected)
- {
- auto label = std::make_unique<TextField<Editable::no>>();
- label->set (text);
-
- if (selected)
- {
- const auto fg = label->findColour (TextEditor::ColourIds::highlightedTextColourId);
- label->setColour (Label::ColourIds::textColourId, fg);
-
- const auto bg = label->findColour (TextEditor::ColourIds::highlightColourId);
- label->setColour (Label::ColourIds::backgroundColourId, bg);
- }
-
- return label;
- }
-
- static SparseSet<int> setWithSingleIndex (int i)
- {
- SparseSet<int> result;
- result.addRange ({ i, i + 1 });
- return result;
- }
-
- template <typename... Items>
- static void doTwoColumnLayout (Rectangle<int> bounds, Items&&... items)
- {
- Grid grid;
- grid.autoFlow = Grid::AutoFlow::row;
- grid.autoColumns = grid.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
- grid.columnGap = grid.rowGap = Grid::Px { Utils::padding };
- grid.templateColumns = { Grid::TrackInfo { Grid::Px { 400 } },
- Grid::TrackInfo { Grid::Fr { 1 } } };
- grid.items = { GridItem { items }... };
- grid.performLayout (bounds);
- }
-
- template <typename... Items>
- static void doColumnLayout (Rectangle<int> bounds, Items&&... items)
- {
- Grid grid;
- grid.autoFlow = Grid::AutoFlow::column;
- grid.autoColumns = grid.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
- grid.columnGap = grid.rowGap = Grid::Px { Utils::padding };
- grid.items = { GridItem { items }... };
- grid.performLayout (bounds);
- }
-
- template <size_t N>
- static void stringToByteArray (const String& str, std::array<std::byte, N>& a)
- {
- const auto asInt = str.removeCharacters (" ").getHexValue64();
-
- for (size_t i = 0; i < N; ++i)
- a[i] = std::byte ((asInt >> (8 * (N - 1 - i))) & 0x7f);
- }
-
- template <typename Callback>
- static void forAllChannelAddresses (Callback&& callback)
- {
- for (uint8_t group = 0; group < 16; ++group)
- {
- for (uint8_t channel = 0; channel < 17; ++channel)
- {
- const auto channelKind = channel < 16 ? ci::ChannelInGroup (channel)
- : ci::ChannelInGroup::wholeGroup;
- callback (ci::ChannelAddress{}.withGroup (group).withChannel (channelKind));
- }
- }
-
- callback (ci::ChannelAddress{}.withChannel (ci::ChannelInGroup::wholeBlock));
- }
-
- template <typename Request>
- static std::tuple<Model::Property, String> attemptSetPartial (Model::Property prop,
- const Request& request)
- {
- if (request.header.mediaType != "application/json")
- {
- return std::tuple (std::move (prop),
- "Partial updates are only supported when the request body is JSON");
- }
-
- if (prop.mediaTypes != std::vector<String> { "application/json" })
- {
- return std::tuple (std::move (prop),
- "Partial updates are only supported when the target property is "
- "JSON");
- }
-
- const auto pointers = JSON::parse (String::fromUTF8 (
- reinterpret_cast<const char*> (request.body.data()),
- (int) request.body.size()));
- const auto* obj = pointers.getDynamicObject();
-
- if (obj == nullptr)
- {
- return std::tuple (std::move (prop),
- "Property data for a partial update must be a json object");
- }
-
- for (const auto& pair : obj->getProperties())
- {
- if (pair.value.isObject() || pair.value.isArray())
- {
- return std::tuple (std::move (prop),
- "Property data for a partial update must not contain nested "
- "arrays or objects");
- }
- }
-
- const auto updatedPropertyValue = [&]() -> std::optional<var>
- {
- std::optional result { JSON::parse (String::fromUTF8 (
- reinterpret_cast<const char*> (prop.value.bytes.data()),
- (int) prop.value.bytes.size())) };
-
- for (const auto& [key, value] : obj->getProperties())
- {
- if (! result.has_value())
- return {};
-
- result = JSONUtils::setPointer (*result, key.toString(), value);
- }
-
- return result;
- }();
-
- if (! updatedPropertyValue)
- {
- return std::tuple (std::move (prop),
- "Partial updates do not apply, perhaps the JSON pointer does not "
- "refer to an extant node");
- }
-
- prop.value.bytes = ci::Encodings::jsonTo7BitText (*updatedPropertyValue);
- return std::tuple (std::move (prop), String());
- }
-
- template <typename... Args>
- static auto forwardFunction (std::function<void (Args...)>& fn)
- {
- return [&fn] (auto&&... args)
- {
- NullCheckedInvocation::invoke (fn, std::forward<decltype (args)> (args)...);
- };
- }
- };
-
- template <typename Kind>
- class IOPickerModel : public ListBoxModel
- {
- public:
- explicit IOPickerModel (State<std::optional<MidiDeviceInfo>> s)
- : state (s) {}
-
- int getNumRows() override
- {
- return Kind::getAvailableDevices().size();
- }
-
- void paintListBoxItem (int, Graphics&, int, int, bool) override {}
-
- Component* refreshComponentForRow (int rowNumber,
- bool rowIsSelected,
- Component* existingComponentToUpdate) override
- {
- const auto toDelete = rawToUniquePtr (existingComponentToUpdate);
- const auto info = Kind::getAvailableDevices()[rowNumber];
- return Utils::makeListRowLabel (info.name, rowIsSelected).release();
- }
-
- void selectedRowsChanged (int newSelection) override
- {
- state = getInfoForRow (newSelection);
- }
-
- std::optional<MidiDeviceInfo> getInfoForRow (int row) const
- {
- return isPositiveAndBelow (row, Kind::getAvailableDevices().size())
- ? std::optional (Kind::getAvailableDevices()[row])
- : std::nullopt;
- }
-
- private:
- State<std::optional<MidiDeviceInfo>> state;
- };
-
- template <typename Kind>
- class IOPickerList : public Component
- {
- public:
- explicit IOPickerList (State<std::optional<MidiDeviceInfo>> s)
- : state (s),
- model (s)
- {
- addAndMakeVisible (label);
- label.setJustificationType (Justification::centred);
-
- addAndMakeVisible (list);
- list.setMultipleSelectionEnabled (false);
- list.setClickingTogglesRowSelection (true);
- list.updateContent();
- }
-
- void resized() override
- {
- FlexBox fb;
- fb.flexDirection = FlexBox::Direction::column;
- fb.items = { FlexItem { label }.withHeight (Utils::standardControlHeight),
- FlexItem{}.withHeight (Utils::padding),
- FlexItem { list }.withFlex (1) };
- fb.performLayout (getLocalBounds());
- }
-
- private:
- void setSelected (std::optional<MidiDeviceInfo> selected)
- {
- list.setSelectedRows ({}, dontSendNotification);
- list.updateContent();
-
- for (auto i = 0; i < model.getNumRows(); ++i)
- if (model.getInfoForRow (i) == selected)
- list.setSelectedRows (Utils::setWithSingleIndex (i), dontSendNotification);
- }
-
- static String getLabel (std::in_place_type_t<MidiInput>) { return "Input"; }
- static String getLabel (std::in_place_type_t<MidiOutput>) { return "Output"; }
-
- State<std::optional<MidiDeviceInfo>> state;
-
- Label label { "", getLabel (std::in_place_type<Kind>) };
- IOPickerModel<Kind> model;
- ListBox list { "Devices", &model };
-
- MidiDeviceListConnection connection = MidiDeviceListConnection::make ([this]
- {
- list.updateContent();
- setSelected (*state);
- });
-
- ErasedScopeGuard listener = state.observe ([this] (auto)
- {
- setSelected (*state);
- });
- };
-
- class IOPickerLists : public Component
- {
- public:
- explicit IOPickerLists (State<Model::IOSelection> selection)
- : inputs (selection[&Model::IOSelection::input]),
- outputs (selection[&Model::IOSelection::output])
- {
- addAndMakeVisible (inputs);
- addAndMakeVisible (outputs);
- }
-
- void resized() override
- {
- Utils::doColumnLayout (getLocalBounds().reduced (Utils::padding), inputs, outputs);
- }
-
- private:
- IOPickerList<MidiInput> inputs;
- IOPickerList<MidiOutput> outputs;
- };
-
- class SectionHeader : public Component
- {
- public:
- explicit SectionHeader (String text)
- : label ("", text)
- {
- addAndMakeVisible (label);
- setInterceptsMouseClicks (false, false);
- setSize (1, Utils::standardControlHeight);
- }
-
- void resized() override
- {
- label.setBounds (getLocalBounds());
- }
-
- void paint (Graphics& g) override
- {
- g.setColour (findColour (TextEditor::ColourIds::highlightColourId));
- g.fillRoundedRectangle (getLocalBounds().reduced (2).toFloat(), 2.0f);
- }
-
- void set (String str)
- {
- label.setText (str, dontSendNotification);
- }
-
- private:
- Label label;
- };
-
- class ToggleSectionHeader : public Component
- {
- public:
- ToggleSectionHeader (String text, State<bool> s)
- : state (s),
- button (text)
- {
- addAndMakeVisible (button);
- setSize (1, Utils::standardControlHeight);
- button.onClick = [this] { state = button.getToggleState(); };
- }
-
- void resized() override
- {
- button.setBounds (getLocalBounds());
- }
-
- void paint (Graphics& g) override
- {
- g.setColour (findColour (TextEditor::ColourIds::highlightColourId));
- g.fillRoundedRectangle (getLocalBounds().reduced (2).toFloat(), 2.0f);
- }
-
- private:
- State<bool> state;
- ToggleButton button;
- ErasedScopeGuard listener = state.observe ([this] (auto)
- {
- button.setToggleState (*state, dontSendNotification);
- });
- };
-
- class PropertySectionHeader : public Component
- {
- public:
- explicit PropertySectionHeader (State<Model::DeviceInfo> s)
- : state (s)
- {
- addAndMakeVisible (toggle);
-
- addAndMakeVisible (numConcurrentLabel);
- addAndMakeVisible (numConcurrent);
- numConcurrent.setJustification (Justification::centredLeft);
-
- setSize (1, toggle.getHeight());
-
- numConcurrent.onCommit ([this]
- {
- const auto clamped = (uint8_t) jlimit (1, 127, numConcurrent.getText().getIntValue());
- state[&Model::DeviceInfo::numPropertyExchangeTransactions] = clamped;
- refresh();
- });
- }
-
- void resized() override
- {
- toggle.setBounds (getLocalBounds());
-
- Utils::doTwoColumnLayout (getLocalBounds(), nullptr, numConcurrent);
-
- Grid grid;
- grid.autoFlow = Grid::AutoFlow::row;
- grid.autoColumns = grid.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
- grid.columnGap = grid.rowGap = Grid::Px { Utils::padding };
- grid.templateColumns = { Grid::TrackInfo { Grid::Fr { 1 } },
- Grid::TrackInfo { Grid::Fr { 1 } } };
- grid.items = { GridItem { numConcurrentLabel }, GridItem { numConcurrent } };
- grid.performLayout (numConcurrent.getBounds().reduced (Utils::padding, 4));
- }
-
- private:
- void refresh()
- {
- numConcurrent.set (String (state->numPropertyExchangeTransactions));
- }
-
- State<Model::DeviceInfo> state;
-
- ToggleSectionHeader toggle { "Properties", state[&Model::DeviceInfo::propertiesSupported] };
- Label numConcurrentLabel { "", "Max Concurrent Streams" };
- TextField<Editable::yes> numConcurrent;
-
- std::vector<ErasedScopeGuard> listeners = Utils::makeVector
- (
- state[&Model::DeviceInfo::propertiesSupported].observe ([this] (auto)
- {
- const auto enabled = state->propertiesSupported;
- numConcurrentLabel.setEnabled (enabled);
- numConcurrent.setEnabled (enabled);
- }),
- state[&Model::DeviceInfo::numPropertyExchangeTransactions].observe ([this] (auto)
- {
- refresh();
- })
- );
- };
-
- class HeterogeneousListContents : public Component
- {
- public:
- void addItem (Component& item)
- {
- contents.emplace_back (&item);
- addAndMakeVisible (item);
- }
-
- void clear()
- {
- for (auto* c : contents)
- removeChildComponent (c);
-
- contents.clear();
- }
-
- void resized() override
- {
- auto y = 0;
-
- for (auto* item : contents)
- {
- item->setBounds ({ 0, y, getWidth(), item->getHeight() });
- y += item->getHeight();
- }
- }
-
- int getIdealHeight() const
- {
- return std::accumulate (contents.begin(), contents.end(), 0, [] (auto acc, auto& item)
- {
- return acc + item->getHeight();
- });
- }
-
- private:
- std::vector<Component*> contents;
- };
-
- class HeterogeneousListView : public Component
- {
- public:
- HeterogeneousListView()
- {
- addAndMakeVisible (viewport);
- viewport.setViewedComponent (&contents, false);
- viewport.setScrollBarsShown (true, false);
- }
-
- void resized() override
- {
- viewport.setBounds (getLocalBounds());
- contents.setBounds ({ getWidth(), contents.getIdealHeight() });
- }
-
- void addItem (Component& item)
- {
- contents.addItem (item);
- }
-
- void clear()
- {
- contents.clear();
- }
-
- private:
- HeterogeneousListContents contents;
- Viewport viewport;
- };
-
- class ChannelStateButtonGrid : public Component
- {
- public:
- static constexpr auto numChannelColumns = 17;
- static constexpr auto numGroups = 16;
-
- explicit ChannelStateButtonGrid (State<Model::Profiles> s)
- : state (s)
- {
- auto index = 0;
-
- for (auto& b : buttons)
- {
- const auto group = index / numChannelColumns;
- const auto channel = index % numChannelColumns;
-
- addAndMakeVisible (b);
- b.setButtonText ("Group " + String (group) + " Channel " + String (channel));
- b.onClick = [this, group, channel]
- {
- const auto channelKind = channel < 16 ? ci::ChannelInGroup (channel)
- : ci::ChannelInGroup::wholeGroup;
- const auto address = ci::ChannelAddress{}.withGroup (group)
- .withChannel (channelKind);
-
- auto selected = state[&Model::Profiles::selectedChannel];
-
- if (*selected != address)
- selected = address;
- else
- selected = std::nullopt;
- };
-
- ++index;
- }
-
- addAndMakeVisible (block);
- block.setButtonText ("Function Block");
- block.setDimensions ({ numChannelColumns, 1.0f });
- block.onClick = [this]
- {
- const auto address = ci::ChannelAddress{}.withChannel (ci::ChannelInGroup::wholeBlock);
-
- auto selected = state[&Model::Profiles::selectedChannel];
-
- if (*selected != address)
- selected = address;
- else
- selected = std::nullopt;
- };
-
- updateButtonVisibility();
- }
-
- void resized() override
- {
- const auto availableBounds = getLocalBounds().reduced (Utils::padding)
- .withTrimmedLeft (labelWidth)
- .withTrimmedTop (labelHeight);
- const auto maxButtonSize = std::min (availableBounds.getWidth() / numChannelColumns,
- availableBounds.getHeight() / (numGroups + 1));
-
- Grid grid;
- grid.autoFlow = Grid::AutoFlow::row;
- grid.rowGap = grid.columnGap = Grid::Px { 1 };
- grid.autoColumns = grid.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
- grid.templateColumns = []
- {
- Array<Grid::TrackInfo> array;
- for (auto i = 0; i < 17; ++i)
- array.add (Grid::TrackInfo { Grid::Fr { 1 } });
- return array;
- }();
-
- for (auto& b : buttons)
- grid.items.add (GridItem { b });
-
- grid.items.add (GridItem { block }.withArea ({}, GridItem::Span { numChannelColumns }));
-
- const Rectangle<int> gridBounds { maxButtonSize * numChannelColumns,
- maxButtonSize * (numGroups + 1) };
- const RectanglePlacement placement(RectanglePlacement::yTop | RectanglePlacement::xMid);
- grid.performLayout (placement.appliedTo (gridBounds, availableBounds));
-
- block.setDimensions (block.getBounds().toFloat());
- }
-
- void paint (Graphics& g) override
- {
- g.setColour (findColour (Label::ColourIds::textColourId));
-
- const auto groupWidth = 100;
- GlyphArrangement groupArrangement;
- groupArrangement.addJustifiedText ({},
- "Group",
- 0,
- 0,
- groupWidth,
- Justification::horizontallyCentred);
-
- Path groupPath;
- groupArrangement.createPath (groupPath);
-
- g.fillPath (groupPath,
- AffineTransform().rotated (-MathConstants<float>::halfPi, groupWidth / 2, 0)
- .translated (-groupWidth / 2, 0)
- .translated (Point { buttons[0].getX() - 40,
- buttons[8 * 17].getY() }.toFloat()));
-
- for (auto i = 0; i < 16; ++i)
- {
- const auto bounds = buttons[(size_t) i * 17].getBounds();
- g.drawSingleLineText (String (i + 1),
- bounds.getX() - Utils::padding,
- bounds.getBottom(),
- Justification::right);
- }
-
- g.drawSingleLineText ("Block",
- block.getX() - Utils::padding,
- block.getBottom(),
- Justification::right);
-
- g.drawSingleLineText ("Channel",
- buttons[8].getBounds().getCentreX(),
- buttons[0].getY() - 40,
- Justification::horizontallyCentred);
-
- for (auto i = 0; i < 17; ++i)
- {
- const auto bounds = buttons[(size_t) i].getBounds();
-
- GlyphArrangement channelArrangement;
- channelArrangement.addJustifiedText ({},
- i < 16 ? String (i + 1) : "All",
- 0,
- 0,
- groupWidth,
- Justification::left);
-
- Path channelPath;
- channelArrangement.createPath (channelPath);
-
- const auto transform = AffineTransform()
- .rotated (-MathConstants<float>::halfPi * 0.8, 0, 0)
- .translated (Point { bounds.getRight(),
- bounds.getY() - Utils::padding }
- .toFloat());
- g.fillPath (channelPath, transform);
- }
-
- if (auto selected = state->selectedChannel)
- {
- auto buttonBounds = getButtonForAddress (*selected).getBounds();
- g.setColour (Colours::cyan);
- g.drawRect (buttonBounds, 2);
- }
- }
-
- private:
- class Button : public ShapeButton
- {
- public:
- Button()
- : ShapeButton ("",
- Colours::black.withAlpha (0.4f),
- Colours::black.withAlpha (0.4f).brighter(),
- Colours::black.withAlpha (0.4f).darker())
- {
- setDimensions ({ 1.0f, 1.0f });
-
- const auto onColour = findColour (TextEditor::ColourIds::highlightColourId);
- setOnColours (onColour, onColour.brighter(), onColour.darker());
- shouldUseOnColours (true);
- setClickingTogglesState (true);
- }
-
- void setDimensions (Rectangle<float> dimensions)
- {
- Path s;
- s.addRectangle (dimensions);
- setShape (s, false, true, false);
- }
- };
-
- Button& getButtonForAddress (ci::ChannelAddress address)
- {
- if (address.isBlock())
- return block;
-
- const auto channelIndex = address.getChannel() != ci::ChannelInGroup::wholeGroup
- ? (size_t) address.getChannel()
- : 16;
- const auto buttonIndex = (size_t) address.getGroup() * numChannelColumns + channelIndex;
- return buttons[buttonIndex];
- }
-
- void updateButtonVisibility()
- {
- if (auto* profile = state->profiles.getSelected())
- {
- Utils::forAllChannelAddresses ([&] (auto address)
- {
- auto& button = getButtonForAddress (address);
-
- const auto iter = state->channels.find ({ *profile, address });
-
- const auto visible = state->profileMode == Model::ProfileMode::edit
- || iter != state->channels.end();
- button.setVisible (visible);
-
- const auto lit = [&]
- {
- if (iter == state->channels.end())
- return false;
-
- if (state->profileMode == Model::ProfileMode::edit)
- return iter->second.supported != 0;
-
- return iter->second.active != 0;
- }();
-
- button.setToggleState (lit, dontSendNotification);
- });
- }
- }
-
- static constexpr auto labelWidth = 50;
- static constexpr auto labelHeight = 50;
- State<Model::Profiles> state;
- std::array<Button, numGroups * numChannelColumns> buttons;
- Button block;
-
- ErasedScopeGuard listener = state.observe ([this] (const auto& old)
- {
- if (old.selectedChannel != state->selectedChannel)
- repaint();
-
- updateButtonVisibility();
- });
- };
-
- class ProfileListModel : public ListBoxModel
- {
- public:
- explicit ProfileListModel (State<Model::Profiles> s)
- : state (s) {}
-
- int getNumRows() override
- {
- return (int) state->profiles.items.size();
- }
-
- void paintListBoxItem (int, Graphics&, int, int, bool) override {}
-
- Component* refreshComponentForRow (int rowNumber,
- bool rowIsSelected,
- Component* existingComponentToUpdate) override
- {
- const auto toDelete = rawToUniquePtr (existingComponentToUpdate);
-
- const auto currentState = *state;
-
- if (auto* item = currentState.profiles.get (rowNumber))
- {
- const auto name = String::toHexString (item->data(), (int) item->size());
- return Utils::makeListRowLabel (name, rowIsSelected).release();
- }
-
- return nullptr;
- }
-
- void selectedRowsChanged (int newSelection) override
- {
- state[&Model::Profiles::profiles]
- [&Model::ListWithSelection<ci::Profile>::selection] = newSelection;
- state[&Model::Profiles::selectedChannel] = std::nullopt;
- }
-
- private:
- State<Model::Profiles> state;
- };
-
- template <Editable editable>
- class ProfileList : public Component
- {
- public:
- explicit ProfileList (State<Model::Profiles> s)
- : state (s)
- {
- addAndMakeVisible (list);
-
- if constexpr (editable == Editable::yes)
- {
- addAndMakeVisible (add);
- add.onClick = [this]
- {
- auto updated = *state;
- updated.profiles.items.emplace_back();
- updated.profiles.selection = (int) updated.profiles.items.size() - 1;
- updated.selectedChannel = std::nullopt;
- state = std::move (updated);
- };
-
- addAndMakeVisible (remove);
- remove.onClick = [this]
- {
- auto updated = *state;
-
- if (auto* item = updated.profiles.getSelected())
- {
- const auto toErase = *item;
-
- Utils::forAllChannelAddresses ([&] (auto address)
- {
- updated.channels.erase (ci::ProfileAtAddress { toErase, address });
- });
-
- const auto erase = updated.profiles.items.begin() + updated.profiles.selection;
- updated.profiles.items.erase (erase);
- updated.profiles.selection = -1;
-
- state = std::move (updated);
- }
- };
- }
- }
-
- void resized() override
- {
- if constexpr (editable == Editable::yes)
- {
- FlexBox fb;
- fb.flexDirection = FlexBox::Direction::column;
- fb.items = { FlexItem { list }.withFlex (1),
- FlexItem{}.withHeight (Utils::padding),
- FlexItem{}.withHeight (Utils::standardControlHeight) };
- fb.performLayout (getLocalBounds());
- Utils::doColumnLayout (fb.items.getLast().currentBounds.getSmallestIntegerContainer(),
- add,
- remove);
- }
- else
- {
- list.setBounds (getLocalBounds());
- }
- }
-
- private:
- State<Model::Profiles> state;
- ProfileListModel model { state };
- ListBox list { "Profiles", &model };
- TextButton add { "+" }, remove { "-" };
-
- ErasedScopeGuard listener = state.observe ([this] (auto)
- {
- list.setSelectedRows ({}, dontSendNotification);
- list.updateContent();
-
- const auto& profs = state->profiles;
- auto* item = profs.getSelected();
-
- remove.setEnabled (item != nullptr);
-
- if (item != nullptr)
- {
- list.setSelectedRows (Utils::setWithSingleIndex (profs.selection),
- dontSendNotification);
- }
- });
- };
-
- class ProfileNamePane : public Component
- {
- public:
- explicit ProfileNamePane (State<Model::Profiles> s)
- : state (s)
- {
- addAndMakeVisible (label);
- addAndMakeVisible (field);
- field.onCommit ([this]
- {
- auto updated = *state;
-
- if (auto* item = updated.profiles.getSelected())
- {
- Utils::stringToByteArray (field.getText(), *item);
- state = std::move (updated);
- }
-
- refresh();
- });
- }
-
- void resized() override
- {
- Utils::doColumnLayout (getLocalBounds(), label, field);
- }
-
- private:
- void refresh()
- {
- if (auto* item = state->profiles.getSelected())
- field.set (String::toHexString (item->data(), (int) item->size()));
- }
-
- State<Model::Profiles> state;
- Label label { "", "Profile Identifier" };
- TextField<Editable::yes> field;
-
- ErasedScopeGuard listener = state.observe ([this] (auto) { refresh(); });
- };
-
- class NumChannelsPane : public Component
- {
- public:
- explicit NumChannelsPane (State<Model::Profiles> s)
- : state (s)
- {
- addAndMakeVisible (label);
- addAndMakeVisible (field);
- field.onCommit ([this]
- {
- NullCheckedInvocation::invoke (onChannelsRequested,
- (uint16_t) field.getText().getIntValue());
- refresh();
- });
- }
-
- void resized() override
- {
- Utils::doColumnLayout (getLocalBounds(), label, field);
- }
-
- std::function<void (uint16_t)> onChannelsRequested;
-
- private:
- void refresh()
- {
- const auto attributes = [&]() -> ci::SupportedAndActive
- {
- const auto selected = state->getSelectedProfileAtAddress();
-
- if (! selected.has_value())
- return {};
-
- const auto iter = state->channels.find (*selected);
-
- if (iter == state->channels.end())
- return {};
-
- return iter->second;
- }();
-
- const auto numToShow = state->profileMode == Model::ProfileMode::edit
- ? attributes.supported
- : attributes.active;
- field.set (String (numToShow));
- }
-
- State<Model::Profiles> state;
- Label label;
- TextField<Editable::yes> field;
-
- ErasedScopeGuard listener = state.observe ([this] (auto)
- {
- const auto text = state->profileMode == Model::ProfileMode::edit
- ? "Num Supported Channels"
- : "Num Active Channels";
- label.setText (text, dontSendNotification);
- refresh();
- });
- };
-
- template <Editable editable>
- class ProfileDetailsPane : public Component
- {
- public:
- explicit ProfileDetailsPane (State<Model::Profiles> s)
- : state (s)
- {
- if constexpr (editable == Editable::yes)
- {
- addAndMakeVisible (name);
- addAndMakeVisible (edit);
- edit.setClickingTogglesState (false);
- edit.onClick = [this]
- {
- state[&Model::Profiles::profileMode] = Model::ProfileMode::edit;
- };
-
- addAndMakeVisible (use);
- use.setClickingTogglesState (false);
- use.onClick = [this]
- {
- state[&Model::Profiles::profileMode] = Model::ProfileMode::use;
- };
-
- channels.onChannelsRequested = [this] (auto numChannels)
- {
- auto updated = *state;
- const auto selected = updated.getSelectedProfileAtAddress();
-
- if (! selected.has_value())
- return;
-
- auto& value = updated.channels[*selected];
-
- if (updated.profileMode == Model::ProfileMode::edit)
- value.supported = jmax ((uint16_t) 0, numChannels);
- else
- value.active = jlimit ((uint16_t) 0, value.supported, numChannels);
-
- state = std::move (updated);
- };
-
- invert.onClick = [this]
- {
- if (const auto selected = state->getSelectedProfileAtAddress())
- {
- auto updated = *state;
- auto& value = updated.channels[*selected];
-
- auto& toInvert = updated.profileMode == Model::ProfileMode::edit
- ? value.supported
- : value.active;
-
- toInvert = toInvert == 0 ? 1 : 0;
- state = std::move (updated);
- }
- };
- }
- else
- {
- channels.onChannelsRequested = Utils::forwardFunction (onChannelsRequested);
-
- invert.onClick = [this]
- {
- if (const auto selected = state->getSelectedProfileAtAddress())
- {
- const auto iter = state->channels.find (*selected);
-
- if (iter == state->channels.end())
- return;
-
- auto& toInvert = state->profileMode == Model::ProfileMode::edit
- ? iter->second.supported
- : iter->second.active;
-
- NullCheckedInvocation::invoke (onChannelsRequested,
- (uint16) (toInvert == 0 ? 1 : 0));
- }
- };
- }
-
- addAndMakeVisible (grid);
- addAndMakeVisible (channels);
- addAndMakeVisible (invert);
- }
-
- void resized() override
- {
- Grid g;
- g.autoFlow = Grid::AutoFlow::row;
- g.autoColumns = g.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
- g.columnGap = g.rowGap = Grid::Px { Utils::padding };
- g.templateColumns = { Grid::TrackInfo { Grid::Fr { 1 } },
- Grid::TrackInfo { Grid::Fr { 1 } } };
-
- if constexpr (editable == Editable::yes)
- {
- g.templateRows = { Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
- Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
- Grid::TrackInfo { Grid::Fr { 1 } },
- Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
- Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } } };
- g.items = { GridItem { name }.withArea ({}, GridItem::Span { 2 }),
- GridItem { edit }, GridItem { use },
- GridItem { grid }.withArea ({}, GridItem::Span { 2 }),
- GridItem { invert }.withArea ({}, GridItem::Span { 2 }),
- GridItem { channels }.withArea ({}, GridItem::Span { 2 }) };
- }
- else
- {
- g.templateRows = { Grid::TrackInfo { Grid::Fr { 1 } },
- Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
- Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } } };
- g.items = { GridItem { grid }.withArea ({}, GridItem::Span { 2 }),
- GridItem { invert }.withArea ({}, GridItem::Span { 2 }),
- GridItem { channels }.withArea ({}, GridItem::Span { 2 }) };
- }
-
- g.performLayout (getLocalBounds());
- }
-
- std::function<void (uint16_t)> onChannelsRequested;
-
- private:
- State<Model::Profiles> state;
- ProfileNamePane name { state };
- ToggleButton edit { "Show Supported Channels" }, use { "Show Active Channels" };
- TextButton invert { "Toggle Member Channels" };
- ChannelStateButtonGrid grid { state };
- NumChannelsPane channels { state };
-
- std::vector<ErasedScopeGuard> listeners = Utils::makeVector
- (
- state[&Model::Profiles::profileMode].observe ([this] (auto)
- {
- edit.setToggleState (state->profileMode == Model::ProfileMode::edit,
- dontSendNotification);
- use.setToggleState (state->profileMode == Model::ProfileMode::use,
- dontSendNotification);
- }),
- state.observe ([this] (auto)
- {
- invert.setVisible (state->getSelectedProfileAtAddress().has_value());
- channels.setVisible (state->getSelectedProfileAtAddress().has_value());
- })
- );
- };
-
- template <Editable editable>
- class ProfileConfigPanel : public Component
- {
- public:
- explicit ProfileConfigPanel (State<Model::Profiles> s)
- : state (s)
- {
- setSize (1, 500);
-
- addAndMakeVisible (list);
- }
-
- void resized() override
- {
- auto* d = details.has_value() ? &*details : nullptr;
- Utils::doTwoColumnLayout (getLocalBounds().reduced (Utils::padding), list, d);
- }
-
- std::function<void (uint16_t)> onChannelsRequested;
-
- private:
- State<Model::Profiles> state;
- ProfileList<editable> list { state };
- std::optional<ProfileDetailsPane<editable>> details;
-
- ErasedScopeGuard listener = state.observe ([this] (auto)
- {
- if (state->profiles.getSelected() != nullptr)
- {
- if (! details.has_value())
- {
- addAndMakeVisible (details.emplace (state));
- details->onChannelsRequested = Utils::forwardFunction (onChannelsRequested);
- resized();
- }
- }
- else
- {
- details.reset();
- }
- });
- };
-
- template <Editable editable>
- class DiscoveryInfoPanel : public Component
- {
- public:
- DiscoveryInfoPanel (State<ci::MUID> m, State<Model::DeviceInfo> s)
- : muidState (m), state (s)
- {
- [&] (auto&&... item)
- {
- (addAndMakeVisible (item), ...);
- ((item.onCommit ([this] { setStateFromUI(); })), ...);
- } (manufacturer, family, modelNumber, revision, maxSysExSize);
-
- [&] (auto&&... item)
- {
- (addAndMakeVisible (item), ...);
- } (muid,
- muidLabel,
- manufacturerLabel,
- familyLabel,
- modelNumberLabel,
- revisionLabel,
- maxSysExSizeLabel);
-
- setSize (1, 6 * Utils::standardControlHeight + 7 * Utils::padding);
- }
-
- void resized() override
- {
- Utils::doTwoColumnLayout (getLocalBounds().reduced (Utils::padding),
- muidLabel, muid,
- maxSysExSizeLabel, maxSysExSize,
- manufacturerLabel, manufacturer,
- familyLabel, family,
- modelNumberLabel, modelNumber,
- revisionLabel, revision);
- }
-
- private:
- void setStateFromUI()
- {
- auto updated = *state;
-
- Utils::stringToByteArray (manufacturer.getText(), updated.deviceInfo.manufacturer);
- Utils::stringToByteArray (family .getText(), updated.deviceInfo.family);
- Utils::stringToByteArray (modelNumber .getText(), updated.deviceInfo.modelNumber);
- Utils::stringToByteArray (revision .getText(), updated.deviceInfo.revision);
- updated.maxSysExSize = (size_t) maxSysExSize.getText().getIntValue();
-
- state = std::move (updated);
- refresh();
- }
-
- void refresh()
- {
- maxSysExSize.set (String (state->maxSysExSize));
-
- auto& info = state->deviceInfo;
- manufacturer.set (byteArrayToString (info.manufacturer));
- family .set (byteArrayToString (info.family));
- modelNumber .set (byteArrayToString (info.modelNumber));
- revision .set (byteArrayToString (info.revision));
- }
-
- static String byteArrayToString (Span<const std::byte> bytes)
- {
- return String::toHexString (bytes.data(), (int) bytes.size());
- }
-
- State<ci::MUID> muidState;
- State<Model::DeviceInfo> state;
-
- Label muidLabel { "", "MUID (hex)" },
- maxSysExSizeLabel { "", "Maximum SysEx size (decimal)" },
- manufacturerLabel { "", "Manufacturer (hex, 3 bytes)" },
- familyLabel { "", "Family (hex, 2 bytes)" },
- modelNumberLabel { "", "Model (hex, 2 bytes)" },
- revisionLabel { "", "Revision (hex, 4 bytes)" };
-
- TextField<Editable::no> muid;
- TextField<editable> maxSysExSize, manufacturer, family, modelNumber, revision;
-
- std::vector<ErasedScopeGuard> listeners = Utils::makeVector
- (
- muidState.observe ([this] (auto)
- {
- muid.set (String::toHexString (muidState->get()));
- }),
- state.observe ([this] (auto) { refresh(); })
- );
- };
-
- class PropertyListModel : public ListBoxModel
- {
- public:
- explicit PropertyListModel (State<Model::Properties> s)
- : state (s) {}
-
- int getNumRows() override
- {
- return (int) state->properties.items.size();
- }
-
- void paintListBoxItem (int, Graphics&, int, int, bool) override {}
-
- Component* refreshComponentForRow (int rowNumber,
- bool rowIsSelected,
- Component* existingComponentToUpdate) override
- {
- const auto toDelete = rawToUniquePtr (existingComponentToUpdate);
-
- const auto currentState = *state;
-
- if (auto* item = currentState.properties.get (rowNumber))
- {
- const auto name = item->name;
- return Utils::makeListRowLabel (name, rowIsSelected).release();
- }
-
- return nullptr;
- }
-
- void selectedRowsChanged (int newSelection) override
- {
- state[&Model::Properties::properties]
- [&Model::ListWithSelection<Model::Property>::selection] = newSelection;
- }
-
- private:
- State<Model::Properties> state;
- };
-
- template <Editable editable>
- class PropertyList : public Component
- {
- public:
- explicit PropertyList (State<Model::Properties> s)
- : state (s)
- {
- addAndMakeVisible (list);
-
- if constexpr (editable == Editable::yes)
- {
- addAndMakeVisible (add);
- add.onClick = [this]
- {
- auto updated = *state;
- updated.properties.items.emplace_back();
- updated.properties.selection = (int) updated.properties.items.size() - 1;
- state = std::move (updated);
- };
-
- addAndMakeVisible (remove);
- remove.onClick = [this]
- {
- auto updated = *state;
- auto& props = updated.properties;
-
- if (auto* item = props.getSelected())
- {
- const auto toErase = props.items.begin() + props.selection;
- props.items.erase (toErase);
- props.selection = -1;
-
- state = std::move (updated);
- }
- };
- }
- }
-
- void resized() override
- {
- if constexpr (editable == Editable::yes)
- {
- FlexBox fb;
- fb.flexDirection = FlexBox::Direction::column;
- fb.items = { FlexItem { list }.withFlex (1),
- FlexItem{}.withHeight (Utils::padding),
- FlexItem{}.withHeight (Utils::standardControlHeight) };
- fb.performLayout (getLocalBounds());
-
- Utils::doColumnLayout (fb.items.getLast().currentBounds.getSmallestIntegerContainer(),
- add,
- remove);
- }
- else
- {
- list.setBounds (getLocalBounds());
- }
- }
-
- private:
- State<Model::Properties> state;
- PropertyListModel model { state };
- ListBox list { "Profiles", &model };
- TextButton add { "+" }, remove { "-" };
-
- ErasedScopeGuard listener = state.observe ([this] (auto)
- {
- list.setSelectedRows ({}, dontSendNotification);
- list.updateContent();
-
- const auto& props = state->properties;
- auto* item = props.getSelected();
-
- remove.setEnabled (item != nullptr);
-
- if (item != nullptr)
- {
- list.setSelectedRows (Utils::setWithSingleIndex (props.selection),
- dontSendNotification);
- }
- });
- };
-
- class PropertySubscribersModel : public ListBoxModel
- {
- public:
- explicit PropertySubscribersModel (State<Model::App> s) : state (s) {}
-
- int getNumRows() override { return (int) entries.size(); }
-
- void paintListBoxItem (int, Graphics&, int, int, bool) override {}
-
- Component* refreshComponentForRow (int rowNumber,
- bool,
- Component* existingComponentToUpdate) override
- {
- const auto toDelete = rawToUniquePtr (existingComponentToUpdate);
-
- if (! isPositiveAndBelow (rowNumber, entries.size()))
- return nullptr;
-
- const auto info = entries[(size_t) rowNumber];
- return Utils::makeListRowLabel (info, false).release();
- }
-
- void refresh()
- {
- entries = [&]() -> std::vector<String>
- {
- const auto& props = state->saved.properties;
-
- if (auto* item = props.properties.getSelected())
- {
- const auto selected = item->name;
- const auto& subs = state->transient.subscribers;
- const auto iter = subs.find (selected);
-
- if (iter == subs.end())
- return {};
-
- std::vector<String> result;
-
- for (const auto& [device, subscriptions] : iter->second)
- for (const auto& s : subscriptions)
- result.push_back (String::toHexString (device.get()) + " (" + s + ")");
-
- return result;
- }
-
- return {};
- }();
- }
-
- private:
- State<Model::App> state;
- std::vector<String> entries;
- };
-
- template <Editable editable>
- class PropertySubscribersPanel : public Component
- {
- public:
- explicit PropertySubscribersPanel (State<Model::App> s)
- : state (s)
- {
- addAndMakeVisible (list);
- list.setMultipleSelectionEnabled (false);
- list.setClickingTogglesRowSelection (false);
- }
-
- void resized() override
- {
- list.setBounds (getLocalBounds().reduced (Utils::padding));
- }
-
- private:
- State<Model::App> state;
- PropertySubscribersModel model { state };
- ListBox list { "Subscribers", &model };
-
- ErasedScopeGuard listener = state.observe ([this] (auto)
- {
- model.refresh();
- list.updateContent();
- });
- };
-
- template <Editable editable>
- class PropertyValuePanel : public Component
- {
- enum class Kind { full, partial };
-
- auto getFileChooserCallback (Kind kind)
- {
- return [this, kind] (const auto&)
- {
- if (propertyFileChooser.getResults().isEmpty())
- return;
-
- auto updated = *state;
-
- if (auto* item = updated.properties.getSelected())
- {
- MemoryBlock block;
- propertyFileChooser.getResult().loadFileAsData (block);
- const auto* data = static_cast<const std::byte*> (block.getData());
-
- if constexpr (editable == Editable::yes)
- {
- std::vector<std::byte> asByteVec (data, data + block.getSize());
-
- if (kind == Kind::full)
- {
- item->value.bytes = std::move (asByteVec);
- }
- else
- {
- struct Header
- {
- String mediaType;
- ci::Encoding mutualEncoding{};
- };
-
- struct Request
- {
- Header header;
- std::vector<std::byte> body;
- };
-
- Request request { { "application/json", ci::Encoding::ascii },
- std::move (asByteVec) };
-
- auto [newItem, error] = Utils::attemptSetPartial (std::move (*item),
- std::move (request));
- *item = std::move (newItem);
- jassert (error.isEmpty()); // Inspect error to find out what went wrong
- }
-
- state = std::move (updated);
- }
- else
- {
- NullCheckedInvocation::invoke (kind == Kind::full ? onSetFullRequested
- : onSetPartialRequested,
- Span (data, block.getSize()));
- }
- }
- };
- }
-
- public:
- explicit PropertyValuePanel (State<Model::Properties> s)
- : PropertyValuePanel (s, {}) {}
-
- PropertyValuePanel (State<Model::Properties> s, State<std::map<String, String>> subState)
- : state (s), subscriptions (subState)
- {
- addAndMakeVisible (value);
- value.setReadOnly (true);
- value.setMultiLine (true);
-
- addAndMakeVisible (formatLabel);
- addAndMakeVisible (format);
-
- format.onCommit ([this]
- {
- auto adjusted = *state;
-
- if (auto* item = adjusted.properties.getSelected())
- {
- item->value.mediaType = format.getText();
- state = adjusted;
- }
-
- refresh();
- });
-
- addAndMakeVisible (viewAsHex);
- viewAsHex.onClick = [this]
- {
- state[&Model::Properties::mode] = Model::DataViewMode::hex;
- };
-
- addAndMakeVisible (viewAsAscii);
- viewAsAscii.onClick = [this]
- {
- state[&Model::Properties::mode] = Model::DataViewMode::ascii;
- };
-
- addAndMakeVisible (setFull);
- setFull.onClick = [this]
- {
- propertyFileChooser.launchAsync (FileBrowserComponent::canSelectFiles
- | FileBrowserComponent::openMode,
- getFileChooserCallback (Kind::full));
- };
-
- addAndMakeVisible (setPartial);
- setPartial.onClick = [this]
- {
- propertyFileChooser.launchAsync (FileBrowserComponent::canSelectFiles
- | FileBrowserComponent::openMode,
- getFileChooserCallback (Kind::partial));
- };
-
- if constexpr (editable == Editable::no)
- {
- addAndMakeVisible (get);
- get.onClick = Utils::forwardFunction (onGetRequested);
-
- addAndMakeVisible (subscribe);
- subscribe.onClick = Utils::forwardFunction (onSubscribeRequested);
- }
- }
-
- void resized() override
- {
- Grid grid;
- grid.autoFlow = Grid::AutoFlow::row;
- grid.autoColumns = grid.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
- grid.columnGap = grid.rowGap = Grid::Px { Utils::padding };
- grid.templateColumns = { Grid::TrackInfo { Grid::Fr { 1 } },
- Grid::TrackInfo { Grid::Fr { 1 } } };
- if constexpr (editable == Editable::yes)
- {
- grid.templateRows = { Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
- Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
- Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
- Grid::TrackInfo { Grid::Fr { 1 } } };
- grid.items = { GridItem { setFull }, GridItem { setPartial },
- GridItem { formatLabel }, GridItem { format },
- GridItem { viewAsHex }, GridItem { viewAsAscii },
- GridItem { value }.withArea ({}, GridItem::Span { 2 }) };
- }
- else
- {
- grid.templateRows = { Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
- Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
- Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
- Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
- Grid::TrackInfo { Grid::Fr { 1 } } };
- grid.items = { GridItem { setFull }, GridItem { setPartial },
- GridItem { get }, GridItem { subscribe },
- GridItem { formatLabel }, GridItem { format },
- GridItem { viewAsHex }, GridItem { viewAsAscii },
- GridItem { value }.withArea ({}, GridItem::Span { 2 }) };
- }
- grid.performLayout (getLocalBounds().reduced (Utils::padding));
- }
-
- std::function<void()> onGetRequested, onSubscribeRequested;
- std::function<void (Span<const std::byte>)> onSetFullRequested, onSetPartialRequested;
-
- private:
- void refresh()
- {
- const auto& currentState = *state;
-
- if (auto* item = currentState.properties.getSelected())
- format.set (item->value.mediaType);
-
- const auto mode = state->mode;
- viewAsHex.setToggleState (mode == Model::DataViewMode::hex, dontSendNotification);
- viewAsAscii.setToggleState (mode == Model::DataViewMode::ascii, dontSendNotification);
- }
-
- void updateSubButtonText()
- {
- const auto& sub = *subscriptions;
-
- if (const auto* selectedProp = state->properties.getSelected())
- {
- const auto text = sub.count (selectedProp->name) != 0 ? "Unsubscribe" : "Subscribe";
- subscribe.setButtonText (text);
- }
- }
-
- State<Model::Properties> state;
- State<std::map<String, String>> subscriptions;
-
- MonospaceEditor value;
- TextField<editable> format;
- Label formatLabel { "", "Media Type" };
- ToggleButton viewAsHex { "Hex" };
- ToggleButton viewAsAscii { "ASCII" };
- TextButton setFull { "Set Full..." },
- setPartial { "Set Partial..." },
- get { "Get" },
- subscribe { "Subscribe" };
-
- FileChooser propertyFileChooser { "Property Data", {}, "*", true, false, this };
-
- std::vector<ErasedScopeGuard> listeners = Utils::makeVector
- (
- state.observe ([this] (const auto& old)
- {
- updateSubButtonText();
- refresh();
-
- const auto& currentState = *state;
-
- if (auto* item = currentState.properties.getSelected())
- {
- const auto mode = currentState.mode;
-
- if (mode == old.mode
- && old.properties.getSelected() != nullptr
- && *old.properties.getSelected() == *item)
- {
- return;
- }
-
- const auto canSetFull = item->canSet != Model::CanSet::none
- || editable == Editable::yes;
- setFull.setEnabled (canSetFull);
- setPartial.setEnabled (item->canSet == Model::CanSet::partial);
- get.setEnabled (item->canGet);
-
- auto* oldSelection = old.properties.getSelected();
- const auto needsValueUpdate = old.mode != mode
- || oldSelection == nullptr
- || oldSelection->value.bytes != item->value.bytes;
-
- if (! needsValueUpdate)
- return;
-
- value.set ("");
-
- switch (mode)
- {
- case Model::DataViewMode::hex:
- value.setColour (TextEditor::ColourIds::textColourId,
- findColour (TextEditor::ColourIds::textColourId));
- value.setText (String::toHexString (item->value.bytes.data(),
- (int) item->value.bytes.size()),
- false);
- break;
-
- case Model::DataViewMode::ascii:
- String toShow;
-
- for (auto& b : item->value.bytes)
- {
- const char ascii[] { (char) b, 0 };
- toShow << (b < std::byte { 0x80 } ? ascii : "\xef\xbf\xbd");
- }
-
- value.set (toShow);
- break;
- }
- }
- }),
- subscriptions.observe ([this] (const auto&)
- {
- updateSubButtonText();
- })
- );
- };
-
- template <Editable editable>
- class PropertyInfoPanel : public Component
- {
- public:
- explicit PropertyInfoPanel (State<Model::Properties> s)
- : state (s)
- {
- if constexpr (editable == Editable::yes)
- {
-
- addAndMakeVisible (canSet);
- canSet.addItemList ({ "None", "Full", "Partial" }, 1);
- canSet.onChange = [this] { updateStateFromUI(); };
- }
- else
- {
- addAndMakeVisible (canSetField);
- }
-
- [&] (auto&&... args)
- {
- (addAndMakeVisible (args), ...);
- (args.setClickingTogglesState (editable == Editable::yes), ...);
- ((args.onClick = [this] { updateStateFromUI(); }), ...);
- } (canGet,
- canSubscribe,
- canPaginate,
- requireResId,
- canAsciiEncode,
- canMcoded7Encode,
- canZipMcoded7Encode);
-
- [&] (auto&&... args)
- {
- (addAndMakeVisible (args), ...);
- } (nameLabel, schemaLabel, mediaTypesLabel, columnsLabel, canSetLabel, listLabel);
-
- [&] (auto&&... args)
- {
- (addAndMakeVisible (args), ...);
- (args.setReadOnly (editable == Editable::no), ...);
- (args.setMultiLine (true), ...);
- ((args.onReturnKey = args.onEscapeKey
- = args.onFocusLost
- = [this] { updateStateFromUI(); }), ...);
- } (schema, mediaTypes, columns);
-
- addAndMakeVisible (name);
- name.onCommit ([this]
- {
- updateStateFromUI();
- refresh();
- });
-
- updateUIFromState();
- }
-
- void resized() override
- {
- Grid grid;
- grid.autoFlow = Grid::AutoFlow::row;
- grid.autoColumns = Grid::TrackInfo { Grid::Fr { 1 } };
- const Grid::TrackInfo tallRow { Grid::Px { 100 } },
- shortRow { Grid::Px { Utils::standardControlHeight } };
- grid.columnGap = grid.rowGap = Grid::Px { Utils::padding };
- grid.templateColumns = { Grid::TrackInfo { Grid::Fr { 1 } },
- Grid::TrackInfo { Grid::Fr { 1 } } };
- grid.templateRows = { shortRow,
- tallRow,
- tallRow,
- shortRow,
- shortRow,
- shortRow,
- shortRow,
- shortRow,
- shortRow,
- tallRow };
- auto* canSetControl = editable == Editable::yes ? static_cast<Component*> (&canSet)
- : static_cast<Component*> (&canSetField);
- grid.items = { GridItem { nameLabel }, GridItem { name },
- GridItem { mediaTypesLabel }, GridItem { mediaTypes },
- GridItem { schemaLabel }, GridItem { schema },
- GridItem { canSetLabel }, GridItem { canSetControl },
- GridItem { canGet }, GridItem { canSubscribe },
- GridItem { requireResId }, GridItem { canAsciiEncode },
- GridItem { canMcoded7Encode }, GridItem { canZipMcoded7Encode },
- GridItem { listLabel }.withArea ({}, GridItem::Span { 2 }),
- GridItem { canPaginate }.withArea ({}, GridItem::Span { 2 }),
- GridItem { columnsLabel }, GridItem { columns } };
- grid.performLayout (getLocalBounds().reduced (Utils::padding));
- }
-
- private:
- void refresh()
- {
- const auto currentState = *state;
-
- if (auto* item = state->properties.getSelected())
- name.set (item->name);
- }
-
- void updateStateFromUI()
- {
- if constexpr (editable == Editable::yes)
- {
- auto updated = *state;
- auto& props = updated.properties;
-
- if (auto* item = props.getSelected())
- {
- auto cachedData = item->value;
- *item = getInfoFromUI();
- item->value = cachedData;
-
- state = std::move (updated);
- }
- }
- }
-
- Model::Property getInfoFromUI() const
- {
- Model::Property result;
- result.name = name.getText();
- result.schema = JSON::fromString (schema.getText());
- auto lines = StringArray::fromLines (mediaTypes.getText() + "\n");
- lines.removeEmptyStrings();
- result.mediaTypes = std::vector<String> (lines.begin(), lines.end());
- result.columns = JSON::fromString (columns.getText());
-
- result.encodings = [&]
- {
- std::set<ci::Encoding> encodings;
-
- if (canAsciiEncode.getToggleState())
- encodings.insert (ci::Encoding::ascii);
-
- if (canMcoded7Encode.getToggleState())
- encodings.insert (ci::Encoding::mcoded7);
-
- if (canZipMcoded7Encode.getToggleState())
- encodings.insert (ci::Encoding::zlibAndMcoded7);
-
- return encodings;
- }();
-
- result.canSet = (Model::CanSet) canSet.getSelectedItemIndex();
- result.canGet = canGet.getToggleState();
- result.canSubscribe = canSubscribe.getToggleState();
- result.requireResId = requireResId.getToggleState();
- result.canPaginate = canPaginate.getToggleState();
- return result;
- }
-
- void updateUIFromState()
- {
- refresh();
-
- const auto currentState = *state;
-
- if (auto* item = state->properties.getSelected())
- {
- schema.setText (JSON::toString (item->schema), false);
-
- const auto pairs = { std::tuple (&canAsciiEncode, ci::Encoding::ascii),
- std::tuple (&canMcoded7Encode, ci::Encoding::mcoded7),
- std::tuple (&canZipMcoded7Encode, ci::Encoding::zlibAndMcoded7) };
-
- for (const auto& [button, encoding] : pairs)
- {
- button->setToggleState (item->encodings.count (encoding) != 0,
- dontSendNotification);
- }
-
- mediaTypes.setText (StringArray (item->mediaTypes.data(),
- (int) item->mediaTypes.size()).joinIntoString ("\n"),
- false);
-
- columns.setText (JSON::toString (item->columns), false);
-
- canSetField.set (Model::CanSetUtils::toString (item->canSet));
- canSet.setSelectedItemIndex ((int) item->canSet, dontSendNotification);
- canGet.setToggleState (item->canGet, dontSendNotification);
- canSubscribe.setToggleState (item->canSubscribe, dontSendNotification);
- requireResId.setToggleState (item->requireResId, dontSendNotification);
- canPaginate.setToggleState (item->canPaginate, dontSendNotification);
-
- const auto list = item->name.endsWith ("List");
- canPaginate.setEnabled (list);
- columnsLabel.setEnabled (list);
- columns.setEnabled (list);
- }
- }
-
- State<Model::Properties> state;
- Label nameLabel { "", "Name" },
- schemaLabel { "", "Schema" },
- mediaTypesLabel { "", "Media Types" },
- columnsLabel { "", "Columns (json array)" },
- canSetLabel { "", "Can Set" },
- listLabel { "", "List Properties (only valid when Name ends with \"List\")" };
-
- TextField<editable> name, canSetField;
- MonospaceEditor schema, mediaTypes, columns;
- ComboBox canSet;
- ToggleButton canGet { "Can Get" },
- canSubscribe { "Can Subscribe" },
- canPaginate { "Can Paginate" },
- requireResId { "Require Res ID" },
- canAsciiEncode { "Can ASCII Encode" },
- canMcoded7Encode { "Can Mcoded7 Encode" },
- canZipMcoded7Encode { "Can zlib+Mcoded7 Encode" };
-
- ErasedScopeGuard listener = state.observe ([this] (auto)
- {
- updateUIFromState();
- });
- };
-
- template <Editable>
- class PropertyEditPanel;
-
- template <>
- class PropertyEditPanel<Editable::yes> : public Component
- {
- public:
- explicit PropertyEditPanel (State<Model::App> s)
- : state (s)
- {
- addAndMakeVisible (tabs);
- tabs.addTab ("Info",
- findColour (DocumentWindow::backgroundColourId),
- &info,
- false);
- tabs.addTab ("Value",
- findColour (DocumentWindow::backgroundColourId),
- &value,
- false);
- tabs.addTab ("Subscribers",
- findColour (DocumentWindow::backgroundColourId),
- &subscribers,
- false);
- }
-
- void resized() override
- {
- tabs.setBounds (getLocalBounds());
- }
-
- private:
- State<Model::App> state;
- PropertyInfoPanel<Editable::yes> info { state[&Model::App::saved][&Model::Saved::properties] };
- PropertyValuePanel<Editable::yes> value { state[&Model::App::saved]
- [&Model::Saved::properties] };
- PropertySubscribersPanel<Editable::yes> subscribers { state };
- TabbedComponent tabs { TabbedButtonBar::Orientation::TabsAtTop };
- };
-
- template <>
- class PropertyEditPanel<Editable::no> : public Component
- {
- public:
- explicit PropertyEditPanel (State<Model::App> s)
- : state (s)
- {
- addAndMakeVisible (tabs);
- tabs.addTab ("Info",
- findColour (DocumentWindow::backgroundColourId),
- &info,
- false);
- tabs.addTab ("Value",
- findColour (DocumentWindow::backgroundColourId),
- &value,
- false);
-
- value.onSetFullRequested = Utils::forwardFunction (onSetFullRequested);
- value.onSetPartialRequested = Utils::forwardFunction (onSetPartialRequested);
- value.onGetRequested = Utils::forwardFunction (onGetRequested);
- value.onSubscribeRequested = Utils::forwardFunction (onSubscribeRequested);
- }
-
- void resized() override
- {
- tabs.setBounds (getLocalBounds());
- }
-
- std::function<void()> onGetRequested, onSubscribeRequested;
- std::function<void (Span<const std::byte>)> onSetFullRequested, onSetPartialRequested;
-
- private:
- State<Model::Device> getDeviceState() const
- {
- const auto selected = (size_t) state->transient.devices.selection;
- return state[&Model::App::transient]
- [&Model::Transient::devices]
- [&Model::ListWithSelection<Model::Device>::items]
- [selected];
- }
-
- State<Model::App> state;
- PropertyInfoPanel<Editable::no> info { getDeviceState()[&Model::Device::properties] };
- PropertyValuePanel<Editable::no> value
- {
- getDeviceState()[&Model::Device::properties],
- getDeviceState()[&Model::Device::subscribeIdForResource]
- };
- TabbedComponent tabs { TabbedButtonBar::Orientation::TabsAtTop };
- };
-
- template <Editable editable>
- class PropertyConfigPanel : public Component
- {
- public:
- explicit PropertyConfigPanel (State<Model::App> s)
- : state (s)
- {
- setSize (1, 700);
-
- addAndMakeVisible (list);
- }
-
- void resized() override
- {
- Utils::doTwoColumnLayout (getLocalBounds().reduced (Utils::padding),
- list,
- GridItem { info.has_value() ? &*info : nullptr });
- }
-
- std::function<void()> onGetRequested, onSubscribeRequested;
- std::function<void (Span<const std::byte>)> onSetFullRequested, onSetPartialRequested;
-
- private:
- State<Model::Properties> getPropertyState() const
- {
- if constexpr (editable == Editable::yes)
- {
- return state[&Model::App::saved][&Model::Saved::properties];
- }
- else
- {
- const auto selected = (size_t) state->transient.devices.selection;
- return state[&Model::App::transient]
- [&Model::Transient::devices]
- [&Model::ListWithSelection<Model::Device>::items]
- [selected]
- [&Model::Device::properties];
- }
- }
-
- State<Model::App> state;
- PropertyList<editable> list { getPropertyState() };
- std::optional<PropertyEditPanel<editable>> info;
-
- ErasedScopeGuard listener = state.observe ([this] (auto)
- {
- if (getPropertyState()->properties.getSelected() != nullptr)
- {
- if (! info.has_value())
- {
- addAndMakeVisible (info.emplace (state));
-
- if constexpr (editable == Editable::no)
- {
- info->onSetFullRequested = Utils::forwardFunction (onSetFullRequested);
- info->onSetPartialRequested = Utils::forwardFunction (onSetPartialRequested);
- info->onGetRequested = Utils::forwardFunction (onGetRequested);
- info->onSubscribeRequested = Utils::forwardFunction (onSubscribeRequested);
- }
-
- resized();
- }
- }
- else
- {
- info.reset();
- }
- });
- };
-
- class LocalConfigurationPanel : public Component
- {
- public:
- LocalConfigurationPanel (State<ci::MUID> m, State<Model::App> s)
- : muidState (m), state (s)
- {
- addAndMakeVisible (list);
-
- list.addItem (basicsHeader);
- list.addItem (discovery);
- list.addItem (profilesHeader);
- list.addItem (profiles);
- list.addItem (propertiesHeader);
- list.addItem (properties);
- }
-
- void resized() override
- {
- list.setBounds (getLocalBounds());
- }
-
- private:
- State<ci::MUID> muidState;
- State<Model::App> state;
- SectionHeader basicsHeader { "Basics" };
- ToggleSectionHeader profilesHeader { "Profiles",
- state[&Model::App::saved]
- [&Model::Saved::fundamentals]
- [&Model::DeviceInfo::profilesSupported] };
- PropertySectionHeader propertiesHeader { state[&Model::App::saved]
- [&Model::Saved::fundamentals] };
- DiscoveryInfoPanel<Editable::yes> discovery { muidState,
- state[&Model::App::saved]
- [&Model::Saved::fundamentals] };
- ProfileConfigPanel<Editable::yes> profiles { state[&Model::App::saved]
- [&Model::Saved::profiles] };
- PropertyConfigPanel<Editable::yes> properties { state };
- HeterogeneousListView list;
-
- ErasedScopeGuard listener = state[&Model::App::saved]
- [&Model::Saved::fundamentals].observe ([this] (auto)
- {
- const auto current = state->saved.fundamentals;
- profiles.setEnabled (current.profilesSupported);
- properties.setEnabled (current.propertiesSupported);
- });
- };
-
- class RemoteConfigurationPanel : public Component
- {
- public:
- explicit RemoteConfigurationPanel (State<Model::App> s)
- : state (s)
- {
- addAndMakeVisible (list);
- profiles.onChannelsRequested = Utils::forwardFunction (onChannelsRequested);
-
- properties.onSetFullRequested = Utils::forwardFunction (onPropertySetFullRequested);
- properties.onSetPartialRequested = Utils::forwardFunction (onPropertySetPartialRequested);
- properties.onGetRequested = Utils::forwardFunction (onPropertyGetRequested);
- properties.onSubscribeRequested = Utils::forwardFunction (onPropertySubscribeRequested);
-
- rebuildList();
- }
-
- void resized() override
- {
- list.setBounds (getLocalBounds());
- }
-
- std::function<void (uint16_t)> onChannelsRequested;
- std::function<void()> onPropertyGetRequested, onPropertySubscribeRequested;
- std::function<void (Span<const std::byte>)> onPropertySetFullRequested,
- onPropertySetPartialRequested;
-
- private:
- void rebuildList()
- {
- list.clear();
-
- list.addItem (basicsHeader);
- list.addItem (discovery);
-
- if (getDeviceState()->info.profilesSupported)
- {
- list.addItem (profilesHeader);
- list.addItem (profiles);
- }
-
- if (getDeviceState()->info.propertiesSupported)
- {
- list.addItem (propertiesHeader);
- list.addItem (properties);
- }
-
- list.resized();
- }
-
- State<Model::Device> getDeviceState() const
- {
- const auto selected = (size_t) state->transient.devices.selection;
- return state[&Model::App::transient]
- [&Model::Transient::devices]
- [&Model::ListWithSelection<Model::Device>::items]
- [selected];
- }
-
- State<Model::App> state;
- SectionHeader basicsHeader { "Basics" };
- DiscoveryInfoPanel<Editable::no> discovery { getDeviceState()[&Model::Device::muid],
- getDeviceState()[&Model::Device::info] };
- SectionHeader profilesHeader { "Profiles" }, propertiesHeader { "Properties" };
- ProfileConfigPanel<Editable::no> profiles { getDeviceState()[&Model::Device::profiles] };
- PropertyConfigPanel<Editable::no> properties { state };
- HeterogeneousListView list;
-
- ErasedScopeGuard listener = getDeviceState().observe ([this] (const auto& old)
- {
- const auto transactions = (int) getDeviceState()->info.numPropertyExchangeTransactions;
- const auto plural = transactions == 1 ? "transaction" : "transactions";
- propertiesHeader.set ("Properties ("
- + String (transactions)
- + " simultaneous "
- + String (plural)
- + " supported)");
-
- const auto& newInfo = getDeviceState()->info;
-
- if (std::tie (old.info.propertiesSupported, old.info.profilesSupported)
- != std::tie (newInfo.propertiesSupported, newInfo.profilesSupported))
- {
- rebuildList();
- }
- });
- };
-
- class DiscoveryPanel : public Component
- {
- public:
- explicit DiscoveryPanel (State<Model::App> s)
- : state (s)
- {
- addAndMakeVisible (discoveryButton);
- discoveryButton.onClick = Utils::forwardFunction (onDiscover);
-
- addAndMakeVisible (deviceCombo);
- deviceCombo.setTextWhenNoChoicesAvailable ("Press Discover Devices to find new devices");
- deviceCombo.setTextWhenNothingSelected ("No device selected");
- }
-
- void resized() override
- {
- const auto headerHeight = Utils::standardControlHeight + 2 * Utils::padding;
-
- auto b = getLocalBounds();
-
- Utils::doTwoColumnLayout (b.removeFromTop (headerHeight).reduced (Utils::padding),
- discoveryButton,
- deviceCombo);
-
- if (configPanel.has_value())
- configPanel->setBounds (b);
- }
-
- std::function<void (uint16_t)> onChannelsRequested;
- std::function<void()> onDiscover, onPropertyGetRequested, onPropertySubscribeRequested;
- std::function<void (Span<const std::byte>)> onPropertySetFullRequested,
- onPropertySetPartialRequested;
-
- private:
- State<Model::App> state;
- TextButton discoveryButton { "Discover Devices" };
- ComboBox deviceCombo;
- std::optional<RemoteConfigurationPanel> configPanel;
- ErasedScopeGuard listener = state[&Model::App::transient]
- [&Model::Transient::devices].observe ([this] (const auto& old)
- {
- if (old.items != state->transient.devices.items)
- {
- deviceCombo.getRootMenu()->clear();
-
- auto indexState = state[&Model::App::transient]
- [&Model::Transient::devices]
- [&Model::ListWithSelection<Model::Device>::selection];
-
- auto index = 0;
- for (auto& dev : state->transient.devices.items)
- {
- const auto suffix = [&]() -> String
- {
- if (const auto readable = dev.properties.getReadableDeviceInfo())
- {
- String result;
-
- if (readable->model.isNotEmpty())
- result << " " << readable->model;
-
- if (readable->manufacturer.isNotEmpty())
- result << " (" << readable->manufacturer << ")";
-
- return result;
- }
-
- return {};
- }();
-
- PopupMenu::Item item;
- item.action = [indexState, i = index++]() mutable { indexState = i; };
- item.text = String::toHexString (dev.muid.get()) + suffix;
- item.itemID = index;
- deviceCombo.getRootMenu()->addItem (std::move (item));
- }
- }
-
- deviceCombo.setSelectedItemIndex (state->transient.devices.selection, dontSendNotification);
-
- if (state->transient.devices.getSelected() != nullptr)
- {
- const auto newIndex = state->transient.devices.selection;
-
- if (old.selection != newIndex)
- {
- addAndMakeVisible (configPanel.emplace (state));
- configPanel->onChannelsRequested =
- Utils::forwardFunction (onChannelsRequested);
- configPanel->onPropertySetFullRequested =
- Utils::forwardFunction (onPropertySetFullRequested);
- configPanel->onPropertySetPartialRequested =
- Utils::forwardFunction (onPropertySetPartialRequested);
- configPanel->onPropertyGetRequested =
- Utils::forwardFunction (onPropertyGetRequested);
- configPanel->onPropertySubscribeRequested =
- Utils::forwardFunction (onPropertySubscribeRequested);
- }
- }
- else
- {
- configPanel.reset();
- }
-
- resized();
- });
- };
-
- /** Accumulates incoming MIDI messages on the MIDI thread, and passes these messages off to other
- Consumers on the main thread.
-
- This is useful because the ci::Device is only intended for single-threaded use. It is an error
- to call ci::Device::receiveMessage concurrently with any other member function on a particular
- instance of Device.
-
- This implementation uses a mutex to protect access to the accumulated packets, and an
- AsyncUpdater to signal the main thread to wake up and process the packets. Alternative
- approaches (e.g. using notify_one and wait from std::atomic_flag, or putting messages into a
- queue and polling periodically on the main thread) may be more suitable in production apps.
- */
- class MessageForwarder : public MidiInputCallback,
- private AsyncUpdater
- {
- public:
- void handleIncomingMidiMessage (MidiInput*, const MidiMessage& message) override
- {
- if (! message.isSysEx())
- return;
-
- {
- const std::scoped_lock lock { mutex };
- messages.push_back (message);
- }
-
- triggerAsyncUpdate();
- }
-
- [[nodiscard]] ErasedScopeGuard addConsumer (ci::DeviceMessageHandler& c)
- {
- return ErasedScopeGuard { [this, it = consumers.insert (&c).first] { consumers.erase (it); } };
- }
-
- private:
- void handleAsyncUpdate() override
- {
- const std::scoped_lock lock { mutex };
-
- for (auto* c : consumers)
- for (const auto& message : messages)
- c->processMessage ({ 0, message.getSysExDataSpan() });
-
- messages.clear();
- }
-
- std::set<ci::DeviceMessageHandler*> consumers;
-
- std::mutex mutex;
- std::vector<MidiMessage> messages;
- };
-
- class LoggingModel : public TableListBoxModel
- {
- public:
- enum Columns
- {
- messageTime = 1,
- group,
- direction,
- from,
- to,
- version,
- channel,
- description,
- };
-
- explicit LoggingModel (State<Model::App> s) : state (s) {}
-
- Component* refreshComponentForCell (int rowNumber,
- int columnId,
- bool,
- Component* existingComponentToUpdate) override
- {
- auto owned = rawToUniquePtr (existingComponentToUpdate);
- const auto filtered = getFiltered();
-
- if (! isPositiveAndBelow (rowNumber, filtered.size()))
- return nullptr;
-
- auto ownedLabel = [&owned]
- {
- auto* label = dynamic_cast<Label*> (owned.get());
-
- if (label == nullptr)
- return Utils::makeListRowLabel ("", false);
-
- owned.release();
- return rawToUniquePtr (label);
- }();
-
- const auto& row = filtered[(size_t) rowNumber];
-
- const auto text = [&]() -> String
- {
- const auto parsed = ci::Parser::parse (row.message);
-
- if (! parsed.has_value())
- return {};
-
- switch (columnId)
- {
- case messageTime:
- return row.time.toString (false, true, true, true);
-
- case group:
- return String (row.group);
-
- case direction:
- return row.kind == Model::MessageKind::incoming ? "in" : "out";
-
- case from:
- return String::toHexString (parsed->header.source.get());
-
- case to:
- return String::toHexString (parsed->header.destination.get());
-
- case version:
- return String::toHexString (parsed->header.version);
-
- case channel:
- return ci::ChannelInGroupUtils::toString (parsed->header.deviceID);
-
- case description:
- return state->saved.logView.mode == Model::DataViewMode::ascii
- ? ci::Parser::getMessageDescription (*parsed)
- : String::toHexString (row.message.data(), (int) row.message.size());
- }
-
- return {};
- }();
-
- ownedLabel->setText (text, dontSendNotification);
- return ownedLabel.release();
- }
-
- int getNumRows() override { return (int) getFiltered().size(); }
-
- void paintRowBackground (Graphics&, int, int, int, bool) override {}
- void paintCell (Graphics&, int, int, int, int, bool) override {}
-
- private:
- std::deque<Model::LogEntry> getFiltered() const
- {
- const auto& filter = state->saved.logView.filter;
- auto copy = state->transient.logEntries;
-
- if (! filter.has_value())
- return copy;
-
- const auto iter = std::remove_if (copy.begin(),
- copy.end(),
- [&] (const auto& e) { return e.kind != *filter; });
- copy.erase (iter, copy.end());
- return copy;
- }
-
- State<Model::App> state;
- };
-
- class LoggingList : public Component,
- private Timer
- {
- public:
- explicit LoggingList (State<Model::App> s)
- : state (s)
- {
- addAndMakeVisible (list);
-
- auto& header = list.getHeader();
- header.addColumn ("Time",
- LoggingModel::Columns::messageTime,
- 100,
- 100);
- header.addColumn ("Group",
- LoggingModel::Columns::group,
- 50,
- 50);
- header.addColumn ("IO",
- LoggingModel::Columns::direction,
- 50,
- 50);
- header.addColumn ("From",
- LoggingModel::Columns::from,
- 60,
- 50);
- header.addColumn ("To",
- LoggingModel::Columns::to,
- 60,
- 50);
- header.addColumn ("Version",
- LoggingModel::Columns::version,
- 50,
- 50);
- header.addColumn ("Channel",
- LoggingModel::Columns::channel,
- 100,
- 50);
- header.addColumn ("Description",
- LoggingModel::Columns::description,
- 300,
- 50);
- }
-
- void resized() override
- {
- list.setBounds (getLocalBounds());
- }
-
- void updateContent()
- {
- // Using a timer here means that we only repaint the UI after there haven't been any
- // new messages for a while, which avoids doing redundant expensive list-layouts.
- startTimer (16);
- }
-
- private:
- void timerCallback() override
- {
- const auto& vbar = list.getVerticalScrollBar();
- const auto endShowing = vbar.getCurrentRange().getEnd() >= vbar.getMaximumRangeLimit();
-
- stopTimer();
- list.updateContent();
-
- if (endShowing)
- list.scrollToEnsureRowIsOnscreen (list.getNumRows() - 1);
- }
-
- State<Model::App> state;
- LoggingModel model { state };
- TableListBox list { "Logs", &model };
- ErasedScopeGuard listener = state[&Model::App::transient]
- [&Model::Transient::logEntries].observe ([this] (auto)
- {
- updateContent();
- });
- };
-
- class LoggingPanel : public Component
- {
- public:
- explicit LoggingPanel (State<Model::App> stateIn)
- : state (stateIn)
- {
- addAndMakeVisible (list);
-
- addAndMakeVisible (onlyIncoming);
- onlyIncoming.onClick = [this]
- {
- auto s = state[&Model::App::saved][&Model::Saved::logView][&Model::LogView::filter];
- s = *s == Model::MessageKind::incoming ? std::nullopt
- : std::optional (Model::MessageKind::incoming);
- };
-
- addAndMakeVisible (onlyOutgoing);
- onlyOutgoing.onClick = [this]
- {
- auto s = state[&Model::App::saved][&Model::Saved::logView][&Model::LogView::filter];
- s = *s == Model::MessageKind::outgoing ? std::nullopt
- : std::optional (Model::MessageKind::outgoing);
- };
-
- addAndMakeVisible (readable);
- readable.onClick = [this]
- {
- auto s = state[&Model::App::saved][&Model::Saved::logView][&Model::LogView::mode];
- s = readable.getToggleState() ? Model::DataViewMode::ascii : Model::DataViewMode::hex;
- };
-
- addAndMakeVisible (clearButton);
- clearButton.onClick = [this]
- {
- state[&Model::App::transient]
- [&Model::Transient::logEntries] = std::deque<Model::LogEntry>();
- };
- }
-
- void resized() override
- {
- FlexBox fb;
- fb.flexDirection = FlexBox::Direction::column;
- fb.items = { FlexItem{}.withHeight (Utils::standardControlHeight),
- FlexItem{}.withHeight (Utils::padding),
- FlexItem { list }.withFlex (1) };
- fb.performLayout (getLocalBounds().reduced (Utils::padding));
-
- Utils::doColumnLayout (fb.items.getFirst().currentBounds.getSmallestIntegerContainer(),
- onlyIncoming,
- onlyOutgoing,
- readable,
- clearButton);
- }
-
- private:
- State<Model::App> state;
- LoggingList list { state };
- ToggleButton onlyIncoming { "Only Incoming" }, onlyOutgoing { "Only Outgoing" };
- ToggleButton readable { "Human-Readable" };
- TextButton clearButton { "Clear" };
- ErasedScopeGuard listener = state[&Model::App::saved]
- [&Model::Saved::logView].observe ([this] (auto)
- {
- list.updateContent();
-
- onlyIncoming.setToggleState (state->saved.logView.filter == Model::MessageKind::incoming,
- dontSendNotification);
- onlyOutgoing.setToggleState (state->saved.logView.filter == Model::MessageKind::outgoing,
- dontSendNotification);
- readable.setToggleState (state->saved.logView.mode == Model::DataViewMode::ascii,
- dontSendNotification);
- });
- };
-
- class CapabilityInquiryDemo : public Component
- {
- public:
- CapabilityInquiryDemo()
- {
- PropertiesFile::Options options;
- options.applicationName = "CapabilityInquiryDemo";
- options.filenameSuffix = "settings";
- options.osxLibrarySubFolder = "Application Support";
- applicationProperties.setStorageParameters (options);
-
- if (auto* userSettings = applicationProperties.getUserSettings())
- setSavedState (JSON::parse (userSettings->getValue ("savedState")));
-
- forwarder.addConsumer (inputHandler).release();
-
- setSize (800, 800);
-
- addAndMakeVisible (tabs);
- tabs.addTab ("MIDI IO",
- findColour (DocumentWindow::backgroundColourId),
- &lists,
- false);
- tabs.addTab ("Local Configuration",
- findColour (DocumentWindow::backgroundColourId),
- &local,
- false);
- tabs.addTab ("Discovery",
- findColour (DocumentWindow::backgroundColourId),
- &discovery,
- false);
- tabs.addTab ("Logging",
- findColour (DocumentWindow::backgroundColourId),
- &logging,
- false);
-
- addAndMakeVisible (loadButton);
- loadButton.onClick = [this] { loadState(); };
-
- addAndMakeVisible (saveButton);
- saveButton.onClick = [this] { saveState(); };
-
- discovery.onDiscover = [this] { discoverDevices(); };
- discovery.onPropertyGetRequested = [this] { getProperty(); };
- discovery.onPropertySubscribeRequested = [this] { subscribeToProperty(); };
- discovery.onChannelsRequested = [this] (auto channels) { setProfileChannels (channels); };
- discovery.onPropertySetFullRequested = [this] (Span<const std::byte> bytes)
- {
- setPropertyFull (bytes);
- };
- discovery.onPropertySetPartialRequested = [this] (Span<const std::byte> bytes)
- {
- setPropertyPartial (bytes);
- };
- }
-
- ~CapabilityInquiryDemo() override
- {
- // In a production app, it'd be a bit risky to write to a file from a destructor as it's
- // bad karma to throw an exception inside a destructor!
- if (auto* userSettings = applicationProperties.getUserSettings())
- userSettings->setValue ("savedState", JSON::toString (getSavedState()));
-
- if (auto* p = getPeer())
- p->setHasChangedSinceSaved (false);
- }
-
- void resized() override
- {
- tabs.setBounds (getLocalBounds());
-
- const auto buttonBounds = getLocalBounds().removeFromTop (tabs.getTabBarDepth())
- .removeFromRight (300)
- .reduced (2);
- Utils::doColumnLayout (buttonBounds, loadButton, saveButton);
- }
-
- private:
- std::optional<std::tuple<ci::MUID, String>> getPropertyRequestInfo() const
- {
- auto* selectedDevice = appState->transient.devices.getSelected();
-
- if (selectedDevice == nullptr)
- return {};
-
- auto* selectedProperty = selectedDevice->properties.properties.getSelected();
-
- if (selectedProperty == nullptr)
- return {};
-
- return std::tuple (selectedDevice->muid, selectedProperty->name);
- }
-
- void discoverDevices()
- {
- if (device.has_value())
- device->sendDiscovery();
- }
-
- void loadState()
- {
- fileChooser.launchAsync (FileBrowserComponent::canSelectFiles | FileBrowserComponent::openMode,
- [this] (const auto& fc)
- {
- if (fc.getResults().isEmpty())
- return;
-
- auto stream = fc.getResult().createInputStream();
-
- if (stream == nullptr)
- return;
-
- setSavedState (JSON::parse (*stream));
- });
- }
-
- void saveState()
- {
- const auto toWrite = JSON::toString (getSavedState());
- fileChooser.launchAsync (FileBrowserComponent::canSelectFiles | FileBrowserComponent::saveMode,
- [this, toWrite] (const auto& fc)
- {
- if (! fc.getResults().isEmpty())
- {
- fc.getResult().replaceWithText (toWrite);
-
- if (auto* p = getPeer())
- p->setHasChangedSinceSaved (false);
- }
- });
- }
-
- void setProfileChannels (uint16_t numChannels)
- {
- if (! device.has_value())
- return;
-
- if (auto* selectedDevice = appState->transient.devices.getSelected())
- {
- if (const auto selected = selectedDevice->profiles.getSelectedProfileAtAddress())
- {
- device->sendProfileEnablement (selectedDevice->muid,
- selected->address.getChannel(),
- selected->profile,
- numChannels);
- }
- }
- }
-
- void setPropertyFull (Span<const std::byte> bytes)
- {
- if (! device.has_value())
- return;
-
- if (const auto details = getPropertyRequestInfo())
- {
- const auto& [muid, propName] = *details;
-
- const auto encodingToUse = [&, muidCopy = muid, propNameCopy = propName]() -> std::optional<ci::Encoding>
- {
- if (auto* deviceResource = findDeviceResource (appState->transient, muidCopy, propNameCopy))
- return deviceResource->getBestCommonEncoding();
-
- return {};
- }();
-
- if (! encodingToUse.has_value())
- {
- // We can't set property data because we don't have any encodings in common with the other device.
- jassertfalse;
- return;
- }
-
- ci::PropertyRequestHeader header;
- header.resource = propName;
- header.mutualEncoding = *encodingToUse;
- device->sendPropertySetInquiry (muid, header, bytes, [] (const auto&)
- {
- // Could do error handling here, e.g. retry the request if the responder is busy
- });
- }
- }
-
- void setPropertyPartial (Span<const std::byte> bytes)
- {
- if (! device.has_value())
- return;
-
- if (const auto details = getPropertyRequestInfo())
- {
- const auto& [muid, propName] = *details;
-
- const auto encodingToUse = [&, muidCopy = muid, propNameCopy = propName]() -> std::optional<ci::Encoding>
- {
- if (auto* deviceResource = findDeviceResource (appState->transient, muidCopy, propNameCopy))
- return deviceResource->getBestCommonEncoding();
-
- return {};
- }();
-
- if (! encodingToUse.has_value())
- {
- // We can't set property data because we don't have any encodings in common with the other device.
- jassertfalse;
- return;
- }
-
- ci::PropertyRequestHeader header;
- header.resource = propName;
- header.mutualEncoding = *encodingToUse;
- header.setPartial = true;
- device->sendPropertySetInquiry (muid, header, bytes, [] (const auto&)
- {
- // Could do error handling here, e.g. retry the request if the responder is busy
- });
- }
- }
-
- void getProperty()
- {
- if (! device.has_value())
- return;
-
- if (const auto details = getPropertyRequestInfo())
- {
- const auto& [muid, propName] = *details;
- requestPropertyData (muid, propName);
- }
- }
-
- void subscribeToProperty()
- {
- if (! device.has_value())
- return;
-
- auto* selectedDevice = appState->transient.devices.getSelected();
-
- if (selectedDevice == nullptr)
- return;
-
- const auto details = getPropertyRequestInfo();
-
- if (! details.has_value())
- return;
-
- const auto& [muid, propName] = *details;
-
- const auto subId = [&, propNameCopy = propName]
- {
- const auto ongoing = device->getOngoingSubscriptionsForMuid (selectedDevice->muid);
- const auto iter = std::find_if (ongoing.begin(), ongoing.end(), [&] (const auto& sub)
- {
- return sub.resource == propNameCopy;
- });
-
- return iter != ongoing.end() ? iter->subscribeId : String();
- }();
-
- ci::PropertySubscriptionHeader header;
- header.resource = propName;
- header.command = subId.isEmpty() ? ci::PropertySubscriptionCommand::start : ci::PropertySubscriptionCommand::end;
- header.subscribeId = subId;
-
- auto callback = [this,
- target = muid,
- propertyName = propName,
- existingSubscription = subId.isNotEmpty()] (const ci::PropertyExchangeResult& response)
- {
- if (response.getError().has_value())
- return;
-
- auto updated = *appState;
-
- auto& knownDevices = updated.transient.devices.items;
- const auto deviceIter = std::find_if (knownDevices.begin(),
- knownDevices.end(),
- [target] (const auto& d) { return d.muid == target; });
-
- if (deviceIter == knownDevices.end())
- {
- // The device has gone away?
- jassertfalse;
- return;
- }
-
- const auto parsedHeader = response.getHeaderAsSubscriptionHeader();
-
- if (parsedHeader.subscribeId.isNotEmpty() && ! existingSubscription)
- deviceIter->subscribeIdForResource.emplace (propertyName, parsedHeader.subscribeId);
- else
- deviceIter->subscribeIdForResource.erase (propertyName);
-
- appState = std::move (updated);
- };
-
- if (subId.isEmpty())
- device->sendPropertySubscriptionStart (muid, header, callback);
- else
- device->sendPropertySubscriptionEnd (muid, subId, callback);
- }
-
- template <typename Transient>
- static auto findDeviceResourceImpl (Transient& transient, ci::MUID device, String resource)
- -> decltype (transient.devices.items.front().properties.properties.items.data())
- {
- auto& knownDevices = transient.devices.items;
- const auto deviceIter = std::find_if (knownDevices.begin(),
- knownDevices.end(),
- [&] (const auto& d) { return d.muid == device; });
-
- if (deviceIter == knownDevices.end())
- return nullptr;
-
- auto& props = deviceIter->properties.properties.items;
- const auto propIter = std::find_if (props.begin(), props.end(), [&] (const auto& prop)
- {
- return prop.name == resource;
- });
-
- if (propIter == props.end())
- return nullptr;
-
- return &*propIter;
- }
-
- static Model::Property* findDeviceResource (Model::Transient& transient,
- ci::MUID device,
- String resource)
- {
- return findDeviceResourceImpl (transient, device, resource);
- }
-
- static const Model::Property* findDeviceResource (const Model::Transient& transient,
- ci::MUID device,
- String resource)
- {
- return findDeviceResourceImpl (transient, device, resource);
- }
-
- void requestPropertyData (ci::MUID target, String propertyName)
- {
- const auto encodingToUse = [&]() -> std::optional<ci::Encoding>
- {
- if (auto* deviceResource = findDeviceResource (appState->transient, target, propertyName))
- return deviceResource->getBestCommonEncoding();
-
- return {};
- }();
-
- if (! encodingToUse.has_value())
- {
- // We can't request property data because we don't have any encodings in common with the other device.
- jassertfalse;
- return;
- }
-
- ci::PropertyRequestHeader header;
- header.resource = propertyName;
- header.mutualEncoding = *encodingToUse;
-
- const auto it = ongoingGetInquiries.insert (ongoingGetInquiries.end(), ErasedScopeGuard{});
- *it = device->sendPropertyGetInquiry (target, header, [this, it, target, propertyName] (const auto& response)
- {
- ongoingGetInquiries.erase (it);
-
- if (response.getError().has_value())
- return;
-
- auto updated = *appState;
-
- if (auto* deviceResource = findDeviceResource (updated.transient, target, propertyName))
- {
- deviceResource->value.bytes = std::vector<std::byte> (response.getBody().begin(),
- response.getBody().end());
- appState = std::move (updated);
- }
- });
- }
-
- void setSavedState (var json)
- {
- if (auto newState = FromVar::convert<Model::Saved> (json))
- appState[&Model::App::saved] = std::move (*newState);
-
- if (auto* p = getPeer())
- p->setHasChangedSinceSaved (false);
- }
-
- var getSavedState() const
- {
- if (auto json = ToVar::convert (appState->saved))
- return *json;
-
- return {};
- }
-
- ci::ProfileHost* getProfileHost()
- {
- if (device.has_value())
- return device->getProfileHost();
-
- return nullptr;
- }
-
- ci::PropertyHost* getPropertyHost()
- {
- if (device.has_value())
- return device->getPropertyHost();
-
- return nullptr;
- }
-
- [[nodiscard]] std::optional<MidiDeviceInfo> getInputInfo() const
- {
- if (input != nullptr)
- return input->getDeviceInfo();
-
- return std::nullopt;
- }
-
- [[nodiscard]] std::optional<MidiDeviceInfo> getOutputInfo() const
- {
- if (output != nullptr)
- return output->getDeviceInfo();
-
- return std::nullopt;
- }
-
- [[nodiscard]] Model::IOSelection getIOSelectionFromDevices() const
- {
- return { getInputInfo(), getOutputInfo() };
- }
-
- static bool isStillConnected (const MidiInput& x)
- {
- const auto devices = MidiInput::getAvailableDevices();
- return std::any_of (devices.begin(),
- devices.end(),
- [info = x.getDeviceInfo()] (const auto& d) { return d == info; });
- }
-
- static bool isStillConnected (const MidiOutput& x)
- {
- const auto devices = MidiOutput::getAvailableDevices();
- return std::any_of (devices.begin(),
- devices.end(),
- [info = x.getDeviceInfo()] (const auto& d) { return d == info; });
- }
-
- [[nodiscard]] static bool isStillConnected (const std::unique_ptr<MidiInput>& x)
- {
- if (x == nullptr)
- return false;
-
- return isStillConnected (*x);
- }
-
- [[nodiscard]] static bool isStillConnected (const std::unique_ptr<MidiOutput>& x)
- {
- if (x == nullptr)
- return false;
-
- return isStillConnected (*x);
- }
-
- void setDeviceProfileState (ci::ProfileAtAddress profileAtAddress,
- ci::SupportedAndActive state)
- {
- if (auto* h = getProfileHost())
- {
- if (state.supported == 0)
- h->removeProfile (profileAtAddress);
- else
- h->addProfile (profileAtAddress, state.supported);
-
- if (state.active == 0)
- h->disableProfile (profileAtAddress);
- else
- h->enableProfile (profileAtAddress, state.active);
- }
- }
-
- void notifySubscribersForProperty (StringRef propertyName)
- {
- if (! device.has_value())
- return;
-
- const auto& subscribers = appState->transient.subscribers;
- const auto iter = subscribers.find (propertyName);
-
- if (iter == subscribers.end())
- return;
-
- for (const auto& [receiver, subscriptions] : iter->second)
- {
- for (const auto& subId : subscriptions)
- {
- if (auto* host = getPropertyHost())
- {
- ci::PropertySubscriptionHeader header;
- header.command = ci::PropertySubscriptionCommand::notify;
- header.subscribeId = subId;
- header.resource = propertyName;
- host->sendSubscriptionUpdate (receiver, header, {}, {}).release();
- }
- }
- }
- }
-
- void addLogEntry (ump::BytesOnGroup entry,
- Model::MessageKind kind,
- Time time = Time::getCurrentTime())
- {
- static constexpr size_t maxNum = 1000;
-
- auto entries = appState[&Model::App::transient][&Model::Transient::logEntries];
- auto updated = *entries;
- updated.emplace (updated.end(), Model::LogEntry { { entry.bytes.begin(),
- entry.bytes.end() },
- entry.group,
- time,
- kind });
-
- while (updated.size() > maxNum)
- updated.pop_front();
-
- entries = std::move (updated);
- }
-
- static Model::App normalise (const Model::App& older, Model::App&& newer)
- {
- auto modified = std::move (newer);
-
- if (older.saved.fundamentals != modified.saved.fundamentals)
- {
- modified.transient.devices.items.clear();
- modified.transient.devices.selection = -1;
- modified.transient.subscribers.clear();
- }
-
- modified.syncSubscribers();
-
- return modified;
- }
-
- class DeviceListener : public ci::DeviceListener
- {
- public:
- explicit DeviceListener (CapabilityInquiryDemo& d) : demo (d) {}
-
- void deviceAdded (ci::MUID added) override
- {
- auto updated = *demo.appState;
-
- auto& devices = updated.transient.devices;
- auto& deviceVec = devices.items;
- const auto iterForMuid = [&] (auto m)
- {
- return std::find_if (deviceVec.begin(),
- deviceVec.end(),
- [&] (const auto& d) { return d.muid == m; });
- };
-
- const auto iter = iterForMuid (added);
- auto& toUpdate = iter != deviceVec.end() ? *iter : deviceVec.emplace_back();
- toUpdate.muid = added;
-
- if (devices.getSelected() == nullptr)
- devices.selection = (int) std::distance (deviceVec.begin(), iterForMuid (added));
-
- const auto response = demo.device.has_value() ? demo.device->getDiscoveryInfoForMuid (added)
- : std::nullopt;
-
- if (! response.has_value())
- return;
-
- const ci::DeviceFeatures features { response->capabilities };
- toUpdate.info.profilesSupported = features.isProfileConfigurationSupported();
- toUpdate.info.propertiesSupported = features.isPropertyExchangeSupported();
- toUpdate.info.deviceInfo = response->device;
- toUpdate.info.maxSysExSize = response->maximumSysexSize;
- toUpdate.info.numPropertyExchangeTransactions = 0;
-
- demo.appState = updated;
-
- if (! demo.device.has_value())
- return;
-
- if (features.isProfileConfigurationSupported())
- demo.device->sendProfileInquiry (added, ci::ChannelInGroup::wholeBlock);
-
- if (features.isPropertyExchangeSupported())
- demo.device->sendPropertyCapabilitiesInquiry (added);
- }
-
- void deviceRemoved (ci::MUID gone) override
- {
- auto updated = *demo.appState;
-
- auto& subs = updated.transient.subscribers;
-
- for (auto& u : subs)
- u.second.erase (gone);
-
- auto& devices = updated.transient.devices;
- const auto selectedMuid = [&]() -> std::optional<ci::MUID>
- {
- if (auto* item = devices.getSelected())
- return item->muid;
-
- return {};
- }();
-
- devices.items.erase (std::remove_if (devices.items.begin(),
- devices.items.end(),
- [&] (const auto& d) { return d.muid == gone; }),
- devices.items.end());
-
- const auto iter = std::find_if (devices.items.begin(),
- devices.items.end(),
- [&] (const auto& d) { return d.muid == selectedMuid; });
- devices.selection = iter != devices.items.end()
- ? (int) std::distance (devices.items.begin(), iter)
- : -1;
-
- demo.appState = updated;
- }
-
- void endpointReceived (ci::MUID, ci::Message::EndpointInquiryResponse) override
- {
- // No special handling
- }
-
- void messageNotAcknowledged (ci::MUID, ci::Message::NAK) override
- {
- // No special handling
- }
-
- void profileStateReceived (ci::MUID muid,
- ci::ChannelInGroup) override
- {
- updateProfilesForMuid (muid);
- }
-
- void profilePresenceChanged (ci::MUID muid,
- ci::ChannelInGroup,
- ci::Profile,
- bool) override
- {
- updateProfilesForMuid (muid);
- }
-
- void profileEnablementChanged (ci::MUID muid,
- ci::ChannelInGroup,
- ci::Profile,
- int) override
- {
- updateProfilesForMuid (muid);
- }
-
- void profileDetailsReceived (ci::MUID,
- ci::ChannelInGroup,
- ci::Profile,
- std::byte,
- Span<const std::byte>) override
- {
- // No special handling
- }
-
- void profileSpecificDataReceived (ci::MUID,
- ci::ChannelInGroup,
- ci::Profile,
- Span<const std::byte>) override
- {
- // No special handling
- }
-
- void propertyExchangeCapabilitiesReceived (ci::MUID muid) override
- {
- auto updated = *demo.appState;
-
- auto& devices = updated.transient.devices;
- const auto iter = std::find_if (devices.items.begin(),
- devices.items.end(),
- [&] (const auto& d) { return d.muid == muid; });
-
- if (iter == devices.items.end() || ! demo.device.has_value())
- return;
-
- const auto transactions = demo.device->getNumPropertyExchangeRequestsSupportedForMuid (muid);
-
- if (! transactions.has_value())
- return;
-
- iter->info.numPropertyExchangeTransactions = (uint8_t) *transactions;
-
- if (const auto resourceList = demo.device->getResourceListForMuid (muid); resourceList != var{})
- {
- if (auto* list = resourceList.getArray())
- {
- auto& items = updated.transient.devices.items;
- const auto found = std::find_if (items.begin(),
- items.end(),
- [&] (const auto& dev) { return dev.muid == muid; });
-
- if (found != updated.transient.devices.items.end())
- {
- found->properties.properties = {};
- auto& propItems = found->properties.properties.items;
-
- Model::Property resourceListProp;
- resourceListProp.name = "ResourceList";
- resourceListProp.canSet = Model::CanSet::none;
- resourceListProp.value.bytes = ci::Encodings::jsonTo7BitText (resourceList);
- propItems.push_back (resourceListProp);
-
- for (auto& entry : *list)
- propItems.push_back (Model::Property::fromResourceListEntry (entry));
- }
- }
- }
-
- if (const auto deviceInfo = demo.device->getDeviceInfoForMuid (muid); deviceInfo != var{})
- {
- if (auto* deviceResource = findDeviceResource (updated.transient, muid, "DeviceInfo"))
- {
- deviceResource->value.bytes = ci::Encodings::jsonTo7BitText (deviceInfo);
- }
- }
-
- demo.appState = std::move (updated);
- }
-
- void propertySubscriptionDataReceived (ci::MUID muid,
- const ci::PropertySubscriptionData& subscription) override
- {
- const auto resource = [&]
- {
- for (const auto& [subId, res] : demo.device->getOngoingSubscriptionsForMuid (muid))
- if (subId == subscription.header.subscribeId)
- return res;
-
- return String{};
- }();
-
- if (resource.isEmpty())
- {
- // Got a subscription message for a subscription that's no longer ongoing
- jassertfalse;
- return;
- }
-
- auto devicesState = demo.appState[&Model::App::transient]
- [&Model::Transient::devices]
- [&Model::ListWithSelection<Model::Device>::items];
- auto copiedDevices = *devicesState;
-
- const auto matchingDevice = [&]
- {
- const auto iter = std::find_if (copiedDevices.begin(),
- copiedDevices.end(),
- [&] (const auto& d) { return d.muid == muid; });
- return iter != copiedDevices.end() ? &*iter : nullptr;
- }();
-
- if (matchingDevice == nullptr)
- {
- // Got a subscription message for a device that we haven't recorded
- jassertfalse;
- return;
- }
-
- auto& propertyList = matchingDevice->properties.properties.items;
-
- const auto matchingProperty = [&]
- {
- const auto iter = std::find_if (propertyList.begin(),
- propertyList.end(),
- [&] (const auto& p) { return p.name == resource; });
- return iter != propertyList.end() ? &*iter : nullptr;
- }();
-
- if (matchingProperty == nullptr)
- {
- // Got a subscription message for a property that we haven't recorded
- jassertfalse;
- return;
- }
-
- switch (subscription.header.command)
- {
- case ci::PropertySubscriptionCommand::partial:
- {
- auto [updated, error] = Utils::attemptSetPartial (std::move (*matchingProperty),
- subscription);
- *matchingProperty = std::move (updated);
- jassert (error.isEmpty()); // Inspect 'error' to see what went wrong
- break;
- }
-
- case ci::PropertySubscriptionCommand::full:
- {
- matchingProperty->value.bytes = std::vector<std::byte> (subscription.body.begin(),
- subscription.body.end());
- matchingProperty->value.mediaType = subscription.header.mediaType;
- break;
- }
-
- case ci::PropertySubscriptionCommand::notify:
- {
- demo.requestPropertyData (muid, resource);
- break;
- }
-
- case ci::PropertySubscriptionCommand::end:
- {
- matchingDevice->subscribeIdForResource.erase (resource);
- break;
- }
-
- case ci::PropertySubscriptionCommand::start:
- jassertfalse;
- return;
- }
-
- devicesState = std::move (copiedDevices);
- }
-
- private:
- void updateProfilesForMuid (ci::MUID muid)
- {
- if (! demo.device.has_value())
- return;
-
- auto innerState = demo.appState[&Model::App::transient][&Model::Transient::devices];
- auto updated = *innerState;
-
- const auto iter = std::find_if (updated.items.begin(),
- updated.items.end(),
- [&] (const auto& d) { return d.muid == muid; });
-
- if (iter == updated.items.end())
- return;
-
- auto& profiles = iter->profiles;
-
- const auto lastSelectedProfile = [&]() -> std::optional<ci::Profile>
- {
- if (auto* ptr = profiles.profiles.getSelected())
- return *ptr;
-
- return {};
- }();
-
- profiles.profileMode = Model::ProfileMode::use;
- profiles.profiles.items = [&]
- {
- std::set<ci::Profile> uniqueProfiles;
- Utils::forAllChannelAddresses ([&] (auto address)
- {
- if (auto* state = demo.device->getProfileStateForMuid (muid, address))
- for (const auto& p : *state)
- uniqueProfiles.insert (p.profile);
- });
- return std::vector<ci::Profile> (uniqueProfiles.begin(), uniqueProfiles.end());
- }();
- profiles.profiles.selection = [&]
- {
- if (! lastSelectedProfile.has_value())
- return -1;
-
- const auto foundMuid = std::find (profiles.profiles.items.begin(),
- profiles.profiles.items.end(),
- *lastSelectedProfile);
-
- if (foundMuid == profiles.profiles.items.end())
- return -1;
-
- return (int) std::distance (profiles.profiles.items.begin(), foundMuid);
- }();
- profiles.channels = [&]
- {
- std::map<ci::ProfileAtAddress, ci::SupportedAndActive> result;
-
- Utils::forAllChannelAddresses ([&] (auto address)
- {
- if (auto* state = demo.device->getProfileStateForMuid (muid, address))
- for (const auto& p : *state)
- result[{ p.profile, address }] = p.state;
- });
-
- return result;
- }();
- profiles.selectedChannel = [&]() -> std::optional<ci::ChannelAddress>
- {
- if (profiles.profileMode == Model::ProfileMode::edit)
- return profiles.selectedChannel;
-
- const auto profileAtAddress = profiles.getSelectedProfileAtAddress();
-
- if (! profileAtAddress.has_value())
- return std::nullopt;
-
- const auto found = profiles.channels.find (*profileAtAddress);
-
- if (found == profiles.channels.end() || found->second.supported == 0)
- return std::nullopt;
-
- return profiles.selectedChannel;
- }();
-
- innerState = std::move (updated);
- }
-
- CapabilityInquiryDemo& demo;
- };
-
- class InputHandler : public ci::DeviceMessageHandler
- {
- public:
- explicit InputHandler (CapabilityInquiryDemo& d) : demo (d) {}
-
- void processMessage (ump::BytesOnGroup msg) override
- {
- demo.addLogEntry (msg, Model::MessageKind::incoming);
-
- if (demo.device.has_value())
- demo.device->processMessage (msg);
- }
-
- private:
- CapabilityInquiryDemo& demo;
- };
-
- class OutputHandler : public ci::DeviceMessageHandler
- {
- public:
- explicit OutputHandler (CapabilityInquiryDemo& d) : demo (d) {}
-
- void processMessage (ump::BytesOnGroup msg) override
- {
- SafePointer weak { &demo };
- std::vector<std::byte> bytes (msg.bytes.begin(), msg.bytes.end());
- auto group = msg.group;
- auto time = Time::getCurrentTime();
-
- MessageManager::callAsync ([weak, movedBytes = std::move (bytes), group, time]
- {
- // This call is async because we may send messages in direct response to model updates.
- if (weak != nullptr)
- weak->addLogEntry ({ group, movedBytes }, Model::MessageKind::outgoing, time);
- });
-
- if (auto* out = demo.output.get())
- out->sendMessageNow (MidiMessage::createSysExMessage (msg.bytes));
- }
-
- private:
- CapabilityInquiryDemo& demo;
- };
-
- class ProfileDelegate : public ci::ProfileDelegate
- {
- public:
- explicit ProfileDelegate (CapabilityInquiryDemo& d) : demo (d) {}
-
- void profileEnablementRequested (ci::MUID,
- ci::ProfileAtAddress profileAtAddress,
- int numChannels,
- bool enabled) override
- {
- auto state = demo.appState[&Model::App::saved][&Model::Saved::profiles];
- auto profiles = *state;
-
- if (auto* host = demo.getProfileHost())
- {
- if (enabled)
- host->enableProfile (profileAtAddress, numChannels);
- else
- host->disableProfile (profileAtAddress);
-
- profiles.channels[profileAtAddress].active = (uint16_t) numChannels;
-
- state = profiles;
- }
- }
-
- private:
- CapabilityInquiryDemo& demo;
- };
-
- class PropertyDelegate : public ci::PropertyDelegate
- {
- public:
- explicit PropertyDelegate (CapabilityInquiryDemo& d) : demo (d) {}
-
- uint8_t getNumSimultaneousRequestsSupported() const override
- {
- return demo.appState->saved.fundamentals.numPropertyExchangeTransactions;
- }
-
- ci::PropertyReplyData propertyGetDataRequested (ci::MUID,
- const ci::PropertyRequestHeader& header) override
- {
- auto allProperties = demo.appState->saved.properties.properties.items;
- allProperties.insert (allProperties.begin(), getDeviceInfo());
-
- if (header.resource == "ResourceList")
- return generateResourceListReply (allProperties);
-
- for (const auto& prop : allProperties)
- if (auto reply = generateReply (header, prop))
- return *reply;
-
- ci::PropertyReplyData result;
- result.header.status = 404;
- result.header.message = "Unable to locate resource " + header.resource;
- return result;
- }
-
- ci::PropertyReplyHeader propertySetDataRequested (ci::MUID,
- const ci::PropertyRequestData& request) override
- {
- const auto makeErrorHeader = [] (auto str, int code = 400)
- {
- ci::PropertyReplyHeader header;
- header.status = code;
- header.message = str;
- return header;
- };
-
- if (request.header.resource == "ResourceList")
- return makeErrorHeader ("Unable to set ResourceList");
-
- auto state = demo.appState[&Model::App::saved]
- [&Model::Saved::properties]
- [&Model::Properties::properties];
- auto props = *state;
-
- for (auto& prop : props.items)
- {
- if (request.header.resource != prop.name)
- continue;
-
- if (prop.canSet == Model::CanSet::none)
- return makeErrorHeader ("Unable to set resource " + prop.name);
-
- if (request.header.setPartial)
- {
- if (prop.canSet != Model::CanSet::partial)
- return makeErrorHeader ("Resource " + prop.name + " does not support setPartial");
-
- auto [updatedProp, error] = Utils::attemptSetPartial (std::move (prop), request);
- prop = std::move (updatedProp);
-
- if (error.isNotEmpty())
- return makeErrorHeader (error);
-
- state = props;
- return {};
- }
-
- prop.value.bytes = std::vector<std::byte> (request.body.begin(), request.body.end());
- prop.value.mediaType = request.header.mediaType;
-
- state = props;
-
- return {};
- }
-
- return makeErrorHeader ("Unable to locate resource " + request.header.resource, 404);
- }
-
- bool subscriptionStartRequested (ci::MUID, const ci::PropertySubscriptionHeader& header) override
- {
- const auto props = demo.appState->saved.properties.properties.items;
-
- return std::any_of (props.begin(), props.end(), [&] (const auto& p)
- {
- return p.name == header.resource;
- });
- }
-
- void subscriptionDidStart (ci::MUID initiator,
- const String& token,
- const ci::PropertySubscriptionHeader& header) override
- {
- auto transient = demo.appState[&Model::App::transient];
- auto updated = *transient;
- updated.subscribers[header.resource][initiator].insert (token);
- transient = updated;
- }
-
- void subscriptionWillEnd (ci::MUID initiator, const ci::Subscription& subscription) override
- {
- auto transient = demo.appState[&Model::App::transient];
- auto updated = *transient;
- updated.subscribers[subscription.resource][initiator].erase (subscription.subscribeId);
- transient = updated;
- }
-
- private:
- Model::Property getDeviceInfo() const
- {
- Model::Property result;
- result.name = "DeviceInfo";
-
- auto obj = std::make_unique<DynamicObject>();
-
- const auto varFromByteArray = [] (const auto& r)
- {
- return Model::toVarArray (r, [] (auto b) { return (int) b; });
- };
-
- obj->setProperty ("manufacturerId",
- varFromByteArray (demo.device->getOptions().getDeviceInfo().manufacturer));
- obj->setProperty ("manufacturer",
- "JUCE");
- obj->setProperty ("familyId",
- varFromByteArray (demo.device->getOptions().getDeviceInfo().family));
- obj->setProperty ("family",
- "MIDI Software");
- obj->setProperty ("modelId",
- varFromByteArray (demo.device->getOptions().getDeviceInfo().modelNumber));
- obj->setProperty ("model",
- "Capability Inquiry Demo");
- obj->setProperty ("versionId",
- varFromByteArray (demo.device->getOptions().getDeviceInfo().revision));
- obj->setProperty ("version",
- ProjectInfo::versionString);
-
- result.value.bytes = ci::Encodings::jsonTo7BitText (obj.release());
- return result;
- }
-
- ci::PropertyReplyData generateResourceListReply (Span<const Model::Property> allProperties) const
- {
- Array<var> resourceList;
-
- for (const auto& prop : allProperties)
- resourceList.add (prop.getResourceListEntry());
-
- return ci::PropertyReplyData { {},
- ci::Encodings::jsonTo7BitText (std::move (resourceList)) };
- }
-
- std::optional<ci::PropertyReplyData> generateReply (const ci::PropertyRequestHeader& header,
- const Model::Property& info) const
- {
- if (header.resource != info.name)
- return {};
-
- const auto encodingToUse = [&]() -> std::optional<ci::Encoding>
- {
- if (info.encodings.count (header.mutualEncoding) != 0)
- return header.mutualEncoding;
-
- if (! info.encodings.empty())
- return *info.encodings.begin();
-
- return {};
- }();
-
- if (! encodingToUse.has_value())
- {
- // If this is hit, we don't declare any supported encodings for this property!
- jassertfalse;
- return {};
- }
-
- const auto basicReplyHeader = [&]
- {
- ci::PropertyReplyHeader h;
- h.mutualEncoding = *encodingToUse;
- h.mediaType = info.value.mediaType;
- return h;
- }();
-
- const auto [replyHeader, unencoded] = [&]
- {
- if (info.name.endsWith ("List")
- && info.canPaginate
- && info.value.mediaType == "application/json")
- {
- const auto jsonToSend = ci::Encodings::jsonFrom7BitText (info.value.bytes);
-
- if (const auto* array = jsonToSend.getArray())
- {
- const auto updatedHeader = [&]
- {
- auto h = basicReplyHeader;
- h.extended["totalCount"] = array->size();
- return h;
- }();
-
- if (const auto pagination = header.pagination)
- {
- const auto realOffset = jlimit (0, array->size(), pagination->offset);
- const auto realLimit = jlimit (0,
- array->size() - realOffset,
- pagination->limit);
- const auto* data = array->data() + realOffset;
- const Array<var> slice (data, realLimit);
- return std::tuple (updatedHeader, ci::Encodings::jsonTo7BitText (slice));
- }
-
- return std::tuple (updatedHeader, info.value.bytes);
- }
- }
-
- return std::tuple (basicReplyHeader, info.value.bytes);
- }();
-
- return ci::PropertyReplyData { replyHeader, unencoded };
- }
-
- CapabilityInquiryDemo& demo;
- };
-
- ApplicationProperties applicationProperties;
-
- State<ci::MUID> ourMuid { ci::MUID::makeUnchecked (0) };
- State<Model::App> appState { Model::App{}, normalise };
-
- IOPickerLists lists { appState[&Model::App::saved][&Model::Saved::ioSelection] };
- LocalConfigurationPanel local { ourMuid, appState };
- DiscoveryPanel discovery { appState };
- LoggingPanel logging { appState };
- TabbedComponent tabs { TabbedButtonBar::Orientation::TabsAtTop };
- TextButton loadButton { "Load State..." }, saveButton { "Save State..."};
-
- MessageForwarder forwarder;
-
- std::unique_ptr<MidiInput> input;
- std::unique_ptr<MidiOutput> output;
- std::optional<ci::Device> device;
- std::list<ErasedScopeGuard> ongoingGetInquiries;
-
- FileChooser fileChooser { "Pick State JSON File", {}, "*.json", true, false, this };
-
- InputHandler inputHandler { *this };
- OutputHandler outputHandler { *this };
- DeviceListener deviceListener { *this };
- ProfileDelegate profileDelegate { *this };
- PropertyDelegate propertyDelegate { *this };
-
- std::vector<ErasedScopeGuard> listeners = Utils::makeVector
- (
- appState[&Model::App::saved][&Model::Saved::fundamentals].observe ([this] (auto)
- {
- Random random;
- random.setSeedRandomly();
-
- const auto fullState = *appState;
- const auto info = fullState.saved.fundamentals;
- const auto features = ci::DeviceFeatures()
- .withProfileConfigurationSupported (info.profilesSupported)
- .withPropertyExchangeSupported (info.propertiesSupported);
-
- const auto options = ci::DeviceOptions()
- .withOutputs ({ &outputHandler })
- .withDeviceInfo (info.deviceInfo)
- .withFeatures (features)
- .withMaxSysExSize (info.maxSysExSize)
- .withProductInstanceId (ci::DeviceOptions::makeProductInstanceId (random))
- .withPropertyDelegate (&propertyDelegate)
- .withProfileDelegate (&profileDelegate);
-
- ongoingGetInquiries.clear();
- device.emplace (options);
- device->addListener (deviceListener);
-
- for (const auto& [addressAndProfile, channels] : fullState.saved.profiles.channels)
- setDeviceProfileState (addressAndProfile, channels);
-
- ourMuid = device->getMuid();
- }),
- appState[&Model::App::saved]
- [&Model::Saved::profiles]
- [&Model::Profiles::channels].observe ([this] (auto)
- {
- if (auto* host = getProfileHost())
- {
- std::map<ci::ProfileAtAddress, ci::SupportedAndActive> deviceState;
-
- Utils::forAllChannelAddresses ([&] (auto address)
- {
- if (auto* state = host->getProfileStates().getStateForDestination (address))
- for (const auto& p : *state)
- deviceState[{ p.profile, address }] = p.state;
- });
-
- const auto requestedState = appState->saved.profiles.channels;
-
- std::vector<std::pair<ci::ProfileAtAddress, ci::SupportedAndActive>> removed;
- const auto compare = [] (const auto& a, const auto& b)
- {
- return a.first < b.first;
- };
- std::set_difference (deviceState.begin(),
- deviceState.end(),
- requestedState.begin(),
- requestedState.end(),
- std::back_inserter (removed),
- compare);
-
- for (const auto& p : removed)
- host->removeProfile (p.first);
-
- for (const auto& [addressAndProfile, channels] : requestedState)
- setDeviceProfileState (addressAndProfile, channels);
- }
- }),
- appState[&Model::App::saved]
- [&Model::Saved::ioSelection]
- [&Model::IOSelection::input].observe ([this] (auto)
- {
- if (input != nullptr)
- input->stop();
-
- input.reset();
-
- if (const auto selection = appState->saved.ioSelection.input)
- {
- input = MidiInput::openDevice (selection->identifier, &forwarder);
-
- if (input != nullptr)
- input->start();
- }
- }),
- appState[&Model::App::saved]
- [&Model::Saved::ioSelection]
- [&Model::IOSelection::output].observe ([this] (auto)
- {
- output.reset();
-
- if (const auto selection = appState->saved.ioSelection.output)
- output = MidiOutput::openDevice (selection->identifier);
- }),
- appState[&Model::App::saved].observe ([this] (auto)
- {
- if (auto* p = getPeer())
- p->setHasChangedSinceSaved (true);
- }),
- appState[&Model::App::transient]
- [&Model::Transient::subscribers].observe ([this] (const auto& oldSubscribers)
- {
- // Send a subscription end message for any subscribed properties that have been
- // completely removed
- const auto newSubscribers = appState->transient.subscribers;
-
- if (oldSubscribers == newSubscribers || ! device.has_value())
- return;
-
- std::set<String> removed;
- for (const auto& p : oldSubscribers)
- if (newSubscribers.count (p.first) == 0)
- removed.insert (p.first);
-
- if (auto* host = getPropertyHost())
- for (const auto& r : removed)
- for (const auto& [dest, subIds] : oldSubscribers.find (r)->second)
- for (const auto& id : subIds)
- host->terminateSubscription (dest, id);
- }),
- appState[&Model::App::saved]
- [&Model::Saved::properties]
- [&Model::Properties::properties].observe ([this] (const auto& oldProperties)
- {
- const auto makePropertyMap = [] (const auto& props)
- {
- std::map<String, Model::PropertyValue> result;
-
- for (const auto& p : props.items)
- result.emplace (p.name, p.value);
-
- return result;
- };
-
- const auto& newProperties = appState->saved.properties.properties;
- const auto oldMap = makePropertyMap (oldProperties);
- const auto newMap = makePropertyMap (newProperties);
-
- for (const auto& [name, value] : newMap)
- {
- const auto iter = oldMap.find (name);
-
- if (iter != oldMap.end() && iter->second != value)
- notifySubscribersForProperty (name);
- }
- })
- );
-
- MidiDeviceListConnection connection = MidiDeviceListConnection::make ([this]
- {
- if (! isStillConnected (input))
- {
- if (input != nullptr)
- input->stop();
-
- input.reset();
- }
-
- if (! isStillConnected (output))
- output.reset();
-
- appState[&Model::App::saved][&Model::Saved::ioSelection] = getIOSelectionFromDevices();
- });
- };
|