/* ============================================================================== 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 class State { public: State() : State (Value{}) {} State (Value v, std::function n) : impl (std::make_unique (std::move (v), std::move (n))) {} explicit State (Value v) : State (std::move (v), [] (const Value&, Value newer) { return std::move (newer); }) {} template State (State p, Value Parent::* member) : impl (std::make_unique> (std::move (p), member)) {} template State (State p, Ind index) : impl (std::make_unique> (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 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 auto operator[] (T member) const -> State().*std::declval())>> { return { *this, std::move (member) }; } template auto operator[] (T index) const -> State()[std::declval()])>> { return { *this, std::move (index) }; } private: class Provider : public std::enable_shared_from_this { 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 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>; List list; }; class Root final : public Provider { public: explicit Root (Value v, std::function 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 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)); if (normalised != value) this->notify (std::exchange (value, std::move (normalised))); } Value value; std::function normalise; bool reentrant = false; }; template class Member final : public Provider { public: Member (State 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 void setImpl (T&& t) { auto updated = *parent; updated.*member = std::forward (t); parent = std::move (updated); } State parent; Value Parent::*member; ErasedScopeGuard listener = parent.observe ([this] (const auto& old) { if (old.*member != get()) this->notify (old.*member); }); }; template class Index final : public Provider { public: Index (State 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 void setImpl (T&& t) { auto updated = *parent; updated[index] = std::forward (t); parent = std::move (updated); } State 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 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 static Array toVarArray (Range&& range, Fn&& fn) { Array result; result.resize ((int) std::size (range)); std::transform (std::begin (range), std::end (range), result.begin(), std::forward (fn)); return result; } template static auto fromVarArray (var in, Fn&& fn, std::vector fallback) -> std::vector { auto* list = in.getArray(); if (list == nullptr) return fallback; std::vector 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 struct ListWithSelection { std::vector items; int selection = -1; static constexpr auto marshallingVersion = std::nullopt; template 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 static auto* get (This& t, Index index) { if (isPositiveAndBelow (index, t.items.size())) return &t.items[(size_t) index]; return static_cast (nullptr); } template 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 profiles; std::map channels; std::optional selectedChannel; ProfileMode profileMode{}; std::optional getSelectedProfileAtAddress() const { if (selectedChannel.has_value()) if (auto* item = profiles.getSelected()) return ci::ProfileAtAddress { *item, *selectedChannel }; return {}; } static constexpr auto marshallingVersion = 0; template 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 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 bytes; ///< decoded bytes String mediaType = "application/json"; static constexpr auto marshallingVersion = 0; template 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 encodings { ci::Encoding::ascii }; std::vector mediaTypes { "application/json" }; var columns; CanSet canSet = CanSet::none; bool canGet = true, canSubscribe = false, requireResId = false, canPaginate = false; PropertyValue value; [[nodiscard]] std::optional getBestCommonEncoding ( ci::Encoding firstChoice = ci::Encoding::ascii) const { if (encodings.count (firstChoice) != 0) return firstChoice; if (! encodings.empty()) return *encodings.begin(); return {}; } std::optional 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::set 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(); 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 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 properties; DataViewMode mode{}; std::optional getReadableDeviceInfo() const { for (const auto& prop : properties.items) if (auto info = prop.getReadableDeviceInfo()) return info; return {}; } static constexpr auto marshallingVersion = 0; template 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 input, output; static constexpr auto marshallingVersion = 0; template 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 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 filter; DataViewMode mode = DataViewMode::ascii; static constexpr auto marshallingVersion = 0; template 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 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 subscribeIdForResource; std::optional getSubscriptionId (const String& resource) const { const auto iter = subscribeIdForResource.find (resource); return iter != subscribeIdForResource.end() ? std::optional (iter->second) : std::nullopt; } template 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 message; uint8_t group{}; Time time; MessageKind kind; template 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>> subscribers; ListWithSelection devices; std::deque logEntries; template 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 currentProperties; for (auto& v : saved.properties.properties.items) currentProperties.insert (v.name); std::set 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 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 { static constexpr auto marshallingVersion = std::nullopt; template static auto serialise (Archive& archive, This& x) { archive (named ("name", x.name), named ("identifier", x.identifier)); } }; template <> struct juce::SerialisationTraits { static constexpr auto marshallingVersion = std::nullopt; template 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 static auto save (Archive& archive, const ci::ChannelAddress& x) { archive (named ("group", x.getGroup()), named ("channel", x.getChannel())); } }; template <> struct juce::SerialisationTraits { static constexpr auto marshallingVersion = std::nullopt; template static auto serialise (Archive& archive, This& x) { archive (named ("profile", x.profile), named ("address", x.address)); } }; template <> struct juce::SerialisationTraits { static constexpr auto marshallingVersion = std::nullopt; template 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 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 set (const String& str) { setText (str, dontSendNotification); } }; enum class Editable { no, yes, }; template class TextField; template <> class TextField : public MonospaceEditor { using MonospaceEditor::MonospaceEditor; }; template <> class TextField : public MonospaceLabel { using MonospaceLabel::MonospaceLabel; }; struct Utils { Utils() = delete; static constexpr auto padding = 10; static constexpr auto standardControlHeight = 30; template static auto makeVector (T&& t, Ts&&... ts) { std::vector result; result.reserve (1 + sizeof... (ts)); result.emplace_back (std::forward (t)); (result.emplace_back (std::forward (ts)), ...); return result; } static std::unique_ptr