| 
							- /*
 -   ==============================================================================
 - 
 -    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();
 -     });
 - };
 
 
  |