The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5030 lines
174KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE examples.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. The code included in this file is provided under the terms of the ISC license
  6. http://www.isc.org/downloads/software-support-policy/isc-license. Permission
  7. To use, copy, modify, and/or distribute this software for any purpose with or
  8. without fee is hereby granted provided that the above copyright notice and
  9. this permission notice appear in all copies.
  10. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
  11. WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
  12. PURPOSE, ARE DISCLAIMED.
  13. ==============================================================================
  14. */
  15. /*******************************************************************************
  16. The block below describes the properties of this PIP. A PIP is a short snippet
  17. of code that can be read by the Projucer and used to generate a JUCE project.
  18. BEGIN_JUCE_PIP_METADATA
  19. name: CapabilityInquiryDemo
  20. version: 1.0.0
  21. vendor: JUCE
  22. website: http://juce.com
  23. description: Performs MIDI Capability Inquiry transactions
  24. dependencies: juce_audio_basics, juce_audio_devices, juce_core,
  25. juce_data_structures, juce_events, juce_graphics,
  26. juce_gui_basics, juce_midi_ci
  27. exporters: xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone
  28. moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
  29. type: Component
  30. mainClass: CapabilityInquiryDemo
  31. useLocalCopy: 1
  32. END_JUCE_PIP_METADATA
  33. *******************************************************************************/
  34. #pragma once
  35. /** This allows listening for changes to some data. Unlike ValueTree, it remembers the types
  36. of all the data that's stored, which makes it a bit nicer to use. Also unlike ValueTree,
  37. every mutation necessitates a deep copy of the model, so this isn't necessarily suitable
  38. for large models that change frequently.
  39. Use operator-> or operator* to find the current state (without copying!).
  40. Use operator= to assign new state.
  41. After assigning new state, the old and new states will be compared, and
  42. observers will be notified if the new state is different to the old state.
  43. Use operator[] to retrieve nested states.
  44. This is useful when a component only depends on a small part of a larger
  45. model. Assigning a new value to the nested state will also cause observers
  46. of the parent state to be notified. Similarly, assigning a new state to
  47. the parent state will cause observers of the nested state to be notified,
  48. if the nested state actually changes in value.
  49. */
  50. template <typename Value>
  51. class State
  52. {
  53. public:
  54. State() : State (Value{}) {}
  55. State (Value v, std::function<Value (const Value&, Value)> n)
  56. : impl (std::make_unique<Root> (std::move (v), std::move (n))) {}
  57. explicit State (Value v)
  58. : State (std::move (v), [] (const Value&, Value newer) { return std::move (newer); }) {}
  59. template <typename Parent>
  60. State (State<Parent> p, Value Parent::* member)
  61. : impl (std::make_unique<Member<Parent>> (std::move (p), member)) {}
  62. template <typename Parent, typename Ind>
  63. State (State<Parent> p, Ind index)
  64. : impl (std::make_unique<Index<Parent, Ind>> (std::move (p), std::move (index))) {}
  65. State& operator= (const Value& v)
  66. {
  67. impl->set (v);
  68. return *this;
  69. }
  70. State& operator= (Value&& v)
  71. {
  72. impl->set (std::move (v));
  73. return *this;
  74. }
  75. const Value& operator* () const { return impl->get(); }
  76. const Value* operator->() const { return &impl->get(); }
  77. ErasedScopeGuard observe (std::function<void (const Value&)> onChange)
  78. {
  79. // The idea is that observers want to know whenever the state changes, in order to repaint.
  80. // They'll also want to repaint upon first observing, so that painting is up-to-date.
  81. onChange (impl->get());
  82. // In the case that observe is called on a temporary, caching impl means that the
  83. // ErasedScopeGuard can still detach safely
  84. return impl->observe ([cached = *this, fn = std::move (onChange)] (const auto& x)
  85. {
  86. fn (x);
  87. });
  88. }
  89. template <typename T>
  90. auto operator[] (T member) const
  91. -> State<std::decay_t<decltype (std::declval<Value>().*std::declval<T>())>>
  92. {
  93. return { *this, std::move (member) };
  94. }
  95. template <typename T>
  96. auto operator[] (T index) const
  97. -> State<std::decay_t<decltype (std::declval<Value>()[std::declval<T>()])>>
  98. {
  99. return { *this, std::move (index) };
  100. }
  101. private:
  102. class Provider : public std::enable_shared_from_this<Provider>
  103. {
  104. public:
  105. virtual ~Provider() = default;
  106. virtual void set (const Value&) = 0;
  107. virtual void set (Value&&) = 0;
  108. virtual const Value& get() const = 0;
  109. ErasedScopeGuard observe (std::function<void (const Value&)> fn)
  110. {
  111. return ErasedScopeGuard { [t = this->shared_from_this(), it = list.insert (list.end(), std::move (fn))]
  112. {
  113. t->list.erase (it);
  114. } };
  115. }
  116. void notify (const Value& v) const
  117. {
  118. for (auto& callback : list)
  119. callback (v);
  120. }
  121. private:
  122. using List = std::list<std::function<void (const Value&)>>;
  123. List list;
  124. };
  125. class Root final : public Provider
  126. {
  127. public:
  128. explicit Root (Value v, std::function<Value (const Value&, Value&&)> n)
  129. : value (std::move (v)), normalise (std::move (n)) {}
  130. void set (const Value& m) override { setImpl (m); }
  131. void set (Value&& m) override { setImpl (std::move (m)); }
  132. const Value& get() const override { return value; }
  133. private:
  134. template <typename T>
  135. void setImpl (T&& t)
  136. {
  137. // If this fails, you're updating the model from inside an observer callback.
  138. // That's not very unidirectional-data-flow of you!
  139. jassert (! reentrant);
  140. const ScopedValueSetter scope { reentrant, true };
  141. auto normalised = normalise (value, std::forward<T> (t));
  142. if (normalised != value)
  143. this->notify (std::exchange (value, std::move (normalised)));
  144. }
  145. Value value;
  146. std::function<Value (const Value&, Value)> normalise;
  147. bool reentrant = false;
  148. };
  149. template <typename Parent>
  150. class Member final : public Provider
  151. {
  152. public:
  153. Member (State<Parent> p, Value Parent::* m) : parent (std::move (p)), member (m) {}
  154. void set (const Value& m) override { setImpl (m); }
  155. void set (Value&& m) override { setImpl (std::move (m)); }
  156. const Value& get() const override { return (*parent).*member; }
  157. private:
  158. template <typename T>
  159. void setImpl (T&& t)
  160. {
  161. auto updated = *parent;
  162. updated.*member = std::forward<T> (t);
  163. parent = std::move (updated);
  164. }
  165. State<Parent> parent;
  166. Value Parent::*member;
  167. ErasedScopeGuard listener = parent.observe ([this] (const auto& old)
  168. {
  169. if (old.*member != get())
  170. this->notify (old.*member);
  171. });
  172. };
  173. template <typename Parent, typename Ind>
  174. class Index final : public Provider
  175. {
  176. public:
  177. Index (State<Parent> p, Ind i) : parent (std::move (p)), index (i) {}
  178. void set (const Value& m) override { setImpl (m); }
  179. void set (Value&& m) override { setImpl (std::move (m)); }
  180. const Value& get() const override { return (*parent)[index]; }
  181. private:
  182. template <typename T>
  183. void setImpl (T&& t)
  184. {
  185. auto updated = *parent;
  186. updated[index] = std::forward<T> (t);
  187. parent = std::move (updated);
  188. }
  189. State<Parent> parent;
  190. Ind index;
  191. ErasedScopeGuard listener = parent.observe ([this] (const auto& old)
  192. {
  193. if (isPositiveAndBelow (index, std::size (*parent))
  194. && isPositiveAndBelow (index, std::size (old))
  195. && old[index] != get())
  196. {
  197. this->notify (old[index]);
  198. }
  199. });
  200. };
  201. std::shared_ptr<Provider> impl;
  202. };
  203. /**
  204. Data types used to represent the state of the application.
  205. These should all be types with value semantics, so there should not be pointers or references
  206. between different parts of the model.
  207. */
  208. struct Model
  209. {
  210. Model() = delete;
  211. template <typename Range, typename Fn>
  212. static Array<var> toVarArray (Range&& range, Fn&& fn)
  213. {
  214. Array<var> result;
  215. result.resize ((int) std::size (range));
  216. std::transform (std::begin (range),
  217. std::end (range),
  218. result.begin(),
  219. std::forward<Fn> (fn));
  220. return result;
  221. }
  222. template <typename Fn, typename T>
  223. static auto fromVarArray (var in, Fn&& fn, std::vector<T> fallback) -> std::vector<T>
  224. {
  225. auto* list = in.getArray();
  226. if (list == nullptr)
  227. return fallback;
  228. std::vector<T> result;
  229. for (auto& t : *list)
  230. result.push_back (fn (t));
  231. return result;
  232. }
  233. #define JUCE_TUPLE_RELATIONAL_OP(classname, op) \
  234. bool operator op (const classname& other) const \
  235. { \
  236. return tie() op other.tie(); \
  237. }
  238. #define JUCE_TUPLE_RELATIONAL_OPS(classname) \
  239. JUCE_TUPLE_RELATIONAL_OP(classname, ==) \
  240. JUCE_TUPLE_RELATIONAL_OP(classname, !=)
  241. template <typename T>
  242. struct ListWithSelection
  243. {
  244. std::vector<T> items;
  245. int selection = -1;
  246. static constexpr auto marshallingVersion = std::nullopt;
  247. template <typename Archive, typename This>
  248. static auto serialise (Archive& archive, This& t)
  249. {
  250. return archive (named ("items", t.items),
  251. named ("selection", t.selection));
  252. }
  253. auto tie() const { return std::tie (items, selection); }
  254. JUCE_TUPLE_RELATIONAL_OPS (ListWithSelection)
  255. private:
  256. template <typename This, typename Index>
  257. static auto* get (This& t, Index index)
  258. {
  259. if (isPositiveAndBelow (index, t.items.size()))
  260. return &t.items[(size_t) index];
  261. return static_cast<decltype (t.items.data())> (nullptr);
  262. }
  263. template <typename This>
  264. static auto* getSelected (This& t)
  265. {
  266. return get (t, t.selection);
  267. }
  268. public:
  269. auto* getSelected() { return getSelected (*this); }
  270. auto* getSelected() const { return getSelected (*this); }
  271. auto* get (int index) { return get (*this, index); }
  272. auto* get (int index) const { return get (*this, index); }
  273. };
  274. enum class ProfileMode
  275. {
  276. edit,
  277. use,
  278. };
  279. struct Profiles
  280. {
  281. ListWithSelection<ci::Profile> profiles;
  282. std::map<ci::ProfileAtAddress, ci::SupportedAndActive> channels;
  283. std::optional<ci::ChannelAddress> selectedChannel;
  284. ProfileMode profileMode{};
  285. std::optional<ci::ProfileAtAddress> getSelectedProfileAtAddress() const
  286. {
  287. if (selectedChannel.has_value())
  288. if (auto* item = profiles.getSelected())
  289. return ci::ProfileAtAddress { *item, *selectedChannel };
  290. return {};
  291. }
  292. static constexpr auto marshallingVersion = 0;
  293. template <typename Archive, typename This>
  294. static auto serialise (Archive& archive, This& t)
  295. {
  296. return archive (named ("profiles", t.profiles),
  297. named ("channels", t.channels),
  298. named ("selectedChannel", t.selectedChannel),
  299. named ("profileMode", t.profileMode));
  300. }
  301. auto tie() const { return std::tie (profiles, channels, selectedChannel, profileMode); }
  302. JUCE_TUPLE_RELATIONAL_OPS (Profiles)
  303. };
  304. enum class DataViewMode
  305. {
  306. ascii,
  307. hex,
  308. };
  309. #define JUCE_CAN_SET X(none) X(full) X(partial)
  310. enum class CanSet
  311. {
  312. #define X(name) name,
  313. JUCE_CAN_SET
  314. #undef X
  315. };
  316. struct CanSetUtils
  317. {
  318. CanSetUtils() = delete;
  319. static std::optional<CanSet> fromString (const char* str)
  320. {
  321. #define X(name) if (StringRef (str) == StringRef (#name)) return CanSet::name;
  322. JUCE_CAN_SET
  323. #undef X
  324. return std::nullopt;
  325. }
  326. static const char* toString (CanSet c)
  327. {
  328. switch (c)
  329. {
  330. #define X(name) case CanSet::name: return #name;
  331. JUCE_CAN_SET
  332. #undef X
  333. }
  334. return nullptr;
  335. }
  336. };
  337. #undef JUCE_CAN_SET
  338. struct PropertyValue
  339. {
  340. std::vector<std::byte> bytes; ///< decoded bytes
  341. String mediaType = "application/json";
  342. static constexpr auto marshallingVersion = 0;
  343. template <typename Archive, typename This>
  344. static auto serialise (Archive& archive, This& t)
  345. {
  346. return archive (named ("bytes", t.bytes),
  347. named ("mediaType", t.mediaType));
  348. }
  349. auto tie() const { return std::tie (bytes, mediaType); }
  350. JUCE_TUPLE_RELATIONAL_OPS (PropertyValue)
  351. };
  352. struct ReadableDeviceInfo
  353. {
  354. String manufacturer, family, model, version;
  355. };
  356. struct Property
  357. {
  358. String name;
  359. var schema;
  360. std::set<ci::Encoding> encodings { ci::Encoding::ascii };
  361. std::vector<String> mediaTypes { "application/json" };
  362. var columns;
  363. CanSet canSet = CanSet::none;
  364. bool canGet = true, canSubscribe = false, requireResId = false, canPaginate = false;
  365. PropertyValue value;
  366. [[nodiscard]] std::optional<ci::Encoding> getBestCommonEncoding (
  367. ci::Encoding firstChoice = ci::Encoding::ascii) const
  368. {
  369. if (encodings.count (firstChoice) != 0)
  370. return firstChoice;
  371. if (! encodings.empty())
  372. return *encodings.begin();
  373. return {};
  374. }
  375. std::optional<ReadableDeviceInfo> getReadableDeviceInfo() const
  376. {
  377. if (name != "DeviceInfo")
  378. return {};
  379. const auto parsed = ci::Encodings::jsonFrom7BitText (value.bytes);
  380. auto* object = parsed.getDynamicObject();
  381. if (object == nullptr)
  382. return {};
  383. if (! object->hasProperty ("manufacturer")
  384. || ! object->hasProperty ("family")
  385. || ! object->hasProperty ("model")
  386. || ! object->hasProperty ("version"))
  387. {
  388. return {};
  389. }
  390. return ReadableDeviceInfo { object->getProperty ("manufacturer"),
  391. object->getProperty ("family"),
  392. object->getProperty ("model"),
  393. object->getProperty ("version") };
  394. }
  395. static Property fromResourceListEntry (var entry)
  396. {
  397. Model::Property p;
  398. p.name = entry.getProperty ("resource", "");
  399. p.canGet = entry.getProperty ("canGet", p.canGet);
  400. const auto set = entry.getProperty ("canSet", {}).toString();
  401. p.canSet = Model::CanSetUtils::fromString (set.toRawUTF8()).value_or (p.canSet);
  402. p.canSubscribe = entry.getProperty ("canSubscribe", p.canSubscribe);
  403. p.requireResId = entry.getProperty ("requireResId", p.requireResId);
  404. p.mediaTypes = fromVarArray (entry.getProperty ("mediaTypes", {}),
  405. [] (String str) { return str; },
  406. p.mediaTypes);
  407. p.schema = entry.getProperty ("schema", p.schema);
  408. p.canPaginate = entry.getProperty ("canPaginate", p.canPaginate);
  409. p.columns = entry.getProperty ("columns", p.columns);
  410. const auto stringToEncoding = [] (String str)
  411. {
  412. return ci::EncodingUtils::toEncoding (str.toRawUTF8());
  413. };
  414. const auto parsedEncodings = fromVarArray (entry.getProperty ("encodings", {}),
  415. stringToEncoding,
  416. std::vector<std::optional<ci::Encoding>>{});
  417. std::set<ci::Encoding> encodingsSet;
  418. for (const auto& parsed : parsedEncodings)
  419. if (parsed.has_value())
  420. encodingsSet.insert (*parsed);
  421. p.encodings = encodingsSet.empty() ? p.encodings : encodingsSet;
  422. return p;
  423. }
  424. var getResourceListEntry() const
  425. {
  426. const Model::Property defaultInfo;
  427. auto obj = std::make_unique<DynamicObject>();
  428. obj->setProperty ("resource", name);
  429. if (canGet != defaultInfo.canGet)
  430. obj->setProperty ("canGet", canGet);
  431. if (canSet != defaultInfo.canSet)
  432. obj->setProperty ("canSet", Model::CanSetUtils::toString (canSet));
  433. if (canSubscribe != defaultInfo.canSubscribe)
  434. obj->setProperty ("canSubscribe", canSubscribe);
  435. if (requireResId != defaultInfo.requireResId)
  436. obj->setProperty ("requireResId", requireResId);
  437. if (mediaTypes != defaultInfo.mediaTypes)
  438. {
  439. obj->setProperty ("mediaTypes",
  440. toVarArray (mediaTypes, [] (auto str) { return str; }));
  441. }
  442. if (encodings != defaultInfo.encodings)
  443. {
  444. obj->setProperty ("encodings",
  445. toVarArray (encodings, ci::EncodingUtils::toString));
  446. }
  447. if (schema != defaultInfo.schema)
  448. obj->setProperty ("schema", schema);
  449. if (name.endsWith ("List"))
  450. {
  451. if (canPaginate != defaultInfo.canPaginate)
  452. obj->setProperty ("canPaginate", canPaginate);
  453. if (columns != defaultInfo.columns)
  454. obj->setProperty ("columns", columns);
  455. }
  456. return obj.release();
  457. }
  458. static constexpr auto marshallingVersion = 0;
  459. template <typename Archive, typename This>
  460. static auto serialise (Archive& archive, This& t)
  461. {
  462. return archive (named ("name", t.name),
  463. named ("schema", t.schema),
  464. named ("encodings", t.encodings),
  465. named ("mediaTypes", t.mediaTypes),
  466. named ("columns", t.columns),
  467. named ("canSet", t.canSet),
  468. named ("canGet", t.canGet),
  469. named ("canSubscribe", t.canSubscribe),
  470. named ("requireResId", t.requireResId),
  471. named ("canPaginate", t.canPaginate),
  472. named ("value", t.value));
  473. }
  474. auto tie() const
  475. {
  476. return std::tie (name,
  477. schema,
  478. encodings,
  479. mediaTypes,
  480. columns,
  481. canSet,
  482. canGet,
  483. canSubscribe,
  484. requireResId,
  485. canPaginate,
  486. value);
  487. }
  488. JUCE_TUPLE_RELATIONAL_OPS (Property)
  489. };
  490. struct Properties
  491. {
  492. ListWithSelection<Property> properties;
  493. DataViewMode mode{};
  494. std::optional<ReadableDeviceInfo> getReadableDeviceInfo() const
  495. {
  496. for (const auto& prop : properties.items)
  497. if (auto info = prop.getReadableDeviceInfo())
  498. return info;
  499. return {};
  500. }
  501. static constexpr auto marshallingVersion = 0;
  502. template <typename Archive, typename This>
  503. static auto serialise (Archive& archive, This& t)
  504. {
  505. return archive (named ("properties", t.properties),
  506. named ("mode", t.mode));
  507. }
  508. auto tie() const { return std::tie (properties, mode); }
  509. JUCE_TUPLE_RELATIONAL_OPS (Properties)
  510. };
  511. struct IOSelection
  512. {
  513. std::optional<MidiDeviceInfo> input, output;
  514. static constexpr auto marshallingVersion = 0;
  515. template <typename Archive, typename This>
  516. static auto serialise (Archive& archive, This& t)
  517. {
  518. return archive (named ("input", t.input),
  519. named ("output", t.output));
  520. }
  521. auto tie() const { return std::tie (input, output); }
  522. JUCE_TUPLE_RELATIONAL_OPS (IOSelection)
  523. };
  524. struct DeviceInfo
  525. {
  526. ump::DeviceInfo deviceInfo;
  527. size_t maxSysExSize { 512 };
  528. uint8_t numPropertyExchangeTransactions { 127 };
  529. bool profilesSupported { false }, propertiesSupported { false };
  530. static constexpr auto marshallingVersion = 0;
  531. template <typename Archive, typename This>
  532. static auto serialise (Archive& archive, This& t)
  533. {
  534. return archive (named ("deviceInfo", t.deviceInfo),
  535. named ("maxSysExSize", t.maxSysExSize),
  536. named ("numPropertyExchangeTransactions",
  537. t.numPropertyExchangeTransactions),
  538. named ("profilesSupported", t.profilesSupported),
  539. named ("propertiesSupported", t.propertiesSupported));
  540. }
  541. auto tie() const
  542. {
  543. return std::tie (deviceInfo,
  544. maxSysExSize,
  545. numPropertyExchangeTransactions,
  546. profilesSupported,
  547. propertiesSupported);
  548. }
  549. JUCE_TUPLE_RELATIONAL_OPS (DeviceInfo)
  550. };
  551. enum class MessageKind
  552. {
  553. incoming,
  554. outgoing,
  555. };
  556. struct LogView
  557. {
  558. std::optional<MessageKind> filter;
  559. DataViewMode mode = DataViewMode::ascii;
  560. static constexpr auto marshallingVersion = 0;
  561. template <typename Archive, typename This>
  562. static auto serialise (Archive& archive, This& t)
  563. {
  564. return archive (named ("filter", t.filter),
  565. named ("mode", t.mode));
  566. }
  567. auto tie() const { return std::tie (filter, mode); }
  568. JUCE_TUPLE_RELATIONAL_OPS (LogView)
  569. };
  570. /**
  571. The bits of the app state that we want to save and restore
  572. */
  573. struct Saved
  574. {
  575. DeviceInfo fundamentals;
  576. IOSelection ioSelection;
  577. Profiles profiles;
  578. Properties properties;
  579. LogView logView;
  580. static constexpr auto marshallingVersion = 0;
  581. template <typename Archive, typename This>
  582. static auto serialise (Archive& archive, This& t)
  583. {
  584. return archive (named ("fundamentals", t.fundamentals),
  585. named ("ioSelection", t.ioSelection),
  586. named ("profiles", t.profiles),
  587. named ("properties", t.properties),
  588. named ("logView", t.logView));
  589. }
  590. auto tie() const
  591. {
  592. return std::tie (fundamentals,
  593. ioSelection,
  594. profiles,
  595. properties,
  596. logView);
  597. }
  598. JUCE_TUPLE_RELATIONAL_OPS (Saved)
  599. };
  600. struct Device
  601. {
  602. ci::MUID muid = ci::MUID::makeUnchecked (0);
  603. DeviceInfo info;
  604. Profiles profiles;
  605. Properties properties;
  606. std::map<ci::SubscriptionKey, ci::Subscription> subscriptions;
  607. template <typename Archive, typename This>
  608. static auto serialise (Archive& archive, This& t)
  609. {
  610. return archive (named ("muid", t.muid),
  611. named ("info", t.info),
  612. named ("profiles", t.profiles),
  613. named ("properties", t.properties),
  614. named ("subscribeIdForResource", t.subscribeIdForResource));
  615. }
  616. auto tie() const
  617. {
  618. return std::tie (muid, info, profiles, properties, subscriptions);
  619. }
  620. JUCE_TUPLE_RELATIONAL_OPS (Device)
  621. };
  622. struct LogEntry
  623. {
  624. std::vector<std::byte> message;
  625. uint8_t group{};
  626. Time time;
  627. MessageKind kind;
  628. template <typename Archive, typename This>
  629. static auto serialise (Archive& archive, This& t)
  630. {
  631. return archive (named ("message", t.message),
  632. named ("group", t.group),
  633. named ("time", t.time),
  634. named ("kind", t.kind));
  635. }
  636. auto tie() const { return std::tie (message, group, time, kind); }
  637. JUCE_TUPLE_RELATIONAL_OPS (LogEntry)
  638. };
  639. /**
  640. App state that needs to be displayed somehow, but which shouldn't be saved or restored.
  641. */
  642. struct Transient
  643. {
  644. // property -> device -> subId
  645. std::map<String, std::map<ci::MUID, std::set<String>>> subscribers;
  646. ListWithSelection<Device> devices;
  647. std::deque<LogEntry> logEntries;
  648. template <typename Archive, typename This>
  649. static auto serialise (Archive& archive, This& t)
  650. {
  651. return archive (named ("subscribers", t.subscribers),
  652. named ("devices", t.devices),
  653. named ("logEntries", t.logEntries));
  654. }
  655. auto tie() const { return std::tie (subscribers, devices, logEntries); }
  656. JUCE_TUPLE_RELATIONAL_OPS (Transient)
  657. };
  658. struct App
  659. {
  660. Saved saved;
  661. Transient transient;
  662. /** Removes properties from Transient::subscribers that are no longer present in
  663. Properties::properties.
  664. */
  665. void syncSubscribers()
  666. {
  667. std::set<String> currentProperties;
  668. for (auto& v : saved.properties.properties.items)
  669. currentProperties.insert (v.name);
  670. std::set<String> removedProperties;
  671. for (const auto& oldSubscriber : transient.subscribers)
  672. if (currentProperties.find (oldSubscriber.first) == currentProperties.end())
  673. removedProperties.insert (oldSubscriber.first);
  674. for (const auto& removed : removedProperties)
  675. transient.subscribers.erase (removed);
  676. }
  677. template <typename Archive, typename This>
  678. static auto serialise (Archive& archive, This& t)
  679. {
  680. return archive (named ("saved", t.saved),
  681. named ("transient", t.transient));
  682. }
  683. auto tie() const { return std::tie (saved, transient); }
  684. JUCE_TUPLE_RELATIONAL_OPS (App)
  685. };
  686. #undef JUCE_TUPLE_RELATIONAL_OPS
  687. #undef JUCE_TUPLE_RELATIONAL_OP
  688. };
  689. template <>
  690. struct juce::SerialisationTraits<MidiDeviceInfo>
  691. {
  692. static constexpr auto marshallingVersion = std::nullopt;
  693. template <typename Archive, typename This>
  694. static auto serialise (Archive& archive, This& x)
  695. {
  696. archive (named ("name", x.name), named ("identifier", x.identifier));
  697. }
  698. };
  699. template <>
  700. struct juce::SerialisationTraits<ci::ChannelAddress>
  701. {
  702. static constexpr auto marshallingVersion = std::nullopt;
  703. template <typename Archive>
  704. static auto load (Archive& archive, ci::ChannelAddress& x)
  705. {
  706. auto group = x.getGroup();
  707. auto channel = x.getChannel();
  708. archive (named ("group", group),
  709. named ("channel", channel));
  710. x = ci::ChannelAddress{}.withGroup (group).withChannel (channel);
  711. }
  712. template <typename Archive>
  713. static auto save (Archive& archive, const ci::ChannelAddress& x)
  714. {
  715. archive (named ("group", x.getGroup()),
  716. named ("channel", x.getChannel()));
  717. }
  718. };
  719. template <>
  720. struct juce::SerialisationTraits<ci::ProfileAtAddress>
  721. {
  722. static constexpr auto marshallingVersion = std::nullopt;
  723. template <typename Archive, typename This>
  724. static auto serialise (Archive& archive, This& x)
  725. {
  726. archive (named ("profile", x.profile), named ("address", x.address));
  727. }
  728. };
  729. template <>
  730. struct juce::SerialisationTraits<ci::SupportedAndActive>
  731. {
  732. static constexpr auto marshallingVersion = std::nullopt;
  733. template <typename Archive, typename This>
  734. static auto serialise (Archive& archive, This& x)
  735. {
  736. archive (named ("supported", x.supported), named ("active", x.active));
  737. }
  738. };
  739. class MonospaceEditor : public TextEditor
  740. {
  741. public:
  742. MonospaceEditor()
  743. {
  744. setFont (Font { Font::getDefaultMonospacedFontName(), 12, 0 });
  745. }
  746. void onCommit (std::function<void()> fn)
  747. {
  748. onEscapeKey = onReturnKey = onFocusLost = std::move (fn);
  749. }
  750. void set (const String& str)
  751. {
  752. setText (str, false);
  753. }
  754. };
  755. class MonospaceLabel : public Label
  756. {
  757. public:
  758. MonospaceLabel()
  759. {
  760. setFont (Font { Font::getDefaultMonospacedFontName(), 12, 0 });
  761. setMinimumHorizontalScale (1.0f);
  762. setInterceptsMouseClicks (false, false);
  763. }
  764. void onCommit (std::function<void()>) {}
  765. void set (const String& str)
  766. {
  767. setText (str, dontSendNotification);
  768. }
  769. };
  770. enum class Editable
  771. {
  772. no,
  773. yes,
  774. };
  775. template <Editable>
  776. class TextField;
  777. template <>
  778. class TextField<Editable::yes> : public MonospaceEditor { using MonospaceEditor::MonospaceEditor; };
  779. template <>
  780. class TextField<Editable::no> : public MonospaceLabel { using MonospaceLabel::MonospaceLabel; };
  781. struct Utils
  782. {
  783. Utils() = delete;
  784. static constexpr auto padding = 10;
  785. static constexpr auto standardControlHeight = 30;
  786. template <typename T, typename... Ts>
  787. static auto makeVector (T&& t, Ts&&... ts)
  788. {
  789. std::vector<T> result;
  790. result.reserve (1 + sizeof... (ts));
  791. result.emplace_back (std::forward<T> (t));
  792. (result.emplace_back (std::forward<Ts> (ts)), ...);
  793. return result;
  794. }
  795. static std::unique_ptr<Label> makeListRowLabel (StringRef text, bool selected)
  796. {
  797. auto label = std::make_unique<TextField<Editable::no>>();
  798. label->set (text);
  799. if (selected)
  800. {
  801. const auto fg = label->findColour (TextEditor::ColourIds::highlightedTextColourId);
  802. label->setColour (Label::ColourIds::textColourId, fg);
  803. const auto bg = label->findColour (TextEditor::ColourIds::highlightColourId);
  804. label->setColour (Label::ColourIds::backgroundColourId, bg);
  805. }
  806. return label;
  807. }
  808. static SparseSet<int> setWithSingleIndex (int i)
  809. {
  810. SparseSet<int> result;
  811. result.addRange ({ i, i + 1 });
  812. return result;
  813. }
  814. template <typename... Items>
  815. static void doTwoColumnLayout (Rectangle<int> bounds, Items&&... items)
  816. {
  817. Grid grid;
  818. grid.autoFlow = Grid::AutoFlow::row;
  819. grid.autoColumns = grid.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
  820. grid.columnGap = grid.rowGap = Grid::Px { Utils::padding };
  821. grid.templateColumns = { Grid::TrackInfo { Grid::Px { 400 } },
  822. Grid::TrackInfo { Grid::Fr { 1 } } };
  823. grid.items = { GridItem { items }... };
  824. grid.performLayout (bounds);
  825. }
  826. template <typename... Items>
  827. static void doColumnLayout (Rectangle<int> bounds, Items&&... items)
  828. {
  829. Grid grid;
  830. grid.autoFlow = Grid::AutoFlow::column;
  831. grid.autoColumns = grid.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
  832. grid.columnGap = grid.rowGap = Grid::Px { Utils::padding };
  833. grid.items = { GridItem { items }... };
  834. grid.performLayout (bounds);
  835. }
  836. template <size_t N>
  837. static void stringToByteArray (const String& str, std::array<std::byte, N>& a)
  838. {
  839. const auto asInt = str.removeCharacters (" ").getHexValue64();
  840. for (size_t i = 0; i < N; ++i)
  841. a[i] = std::byte ((asInt >> (8 * (N - 1 - i))) & 0x7f);
  842. }
  843. template <typename Callback>
  844. static void forAllChannelAddresses (Callback&& callback)
  845. {
  846. for (uint8_t group = 0; group < 16; ++group)
  847. {
  848. for (uint8_t channel = 0; channel < 17; ++channel)
  849. {
  850. const auto channelKind = channel < 16 ? ci::ChannelInGroup (channel)
  851. : ci::ChannelInGroup::wholeGroup;
  852. callback (ci::ChannelAddress{}.withGroup (group).withChannel (channelKind));
  853. }
  854. }
  855. callback (ci::ChannelAddress{}.withChannel (ci::ChannelInGroup::wholeBlock));
  856. }
  857. template <typename Request>
  858. static std::tuple<Model::Property, String> attemptSetPartial (Model::Property prop,
  859. const Request& request)
  860. {
  861. if (request.header.mediaType != "application/json")
  862. {
  863. return std::tuple (std::move (prop),
  864. "Partial updates are only supported when the request body is JSON");
  865. }
  866. if (prop.mediaTypes != std::vector<String> { "application/json" })
  867. {
  868. return std::tuple (std::move (prop),
  869. "Partial updates are only supported when the target property is "
  870. "JSON");
  871. }
  872. const auto pointers = JSON::parse (String::fromUTF8 (
  873. reinterpret_cast<const char*> (request.body.data()),
  874. (int) request.body.size()));
  875. const auto* obj = pointers.getDynamicObject();
  876. if (obj == nullptr)
  877. {
  878. return std::tuple (std::move (prop),
  879. "Property data for a partial update must be a json object");
  880. }
  881. for (const auto& pair : obj->getProperties())
  882. {
  883. if (pair.value.isObject() || pair.value.isArray())
  884. {
  885. return std::tuple (std::move (prop),
  886. "Property data for a partial update must not contain nested "
  887. "arrays or objects");
  888. }
  889. }
  890. const auto updatedPropertyValue = [&]() -> std::optional<var>
  891. {
  892. std::optional result { JSON::parse (String::fromUTF8 (
  893. reinterpret_cast<const char*> (prop.value.bytes.data()),
  894. (int) prop.value.bytes.size())) };
  895. for (const auto& [key, value] : obj->getProperties())
  896. {
  897. if (! result.has_value())
  898. return {};
  899. result = JSONUtils::setPointer (*result, key.toString(), value);
  900. }
  901. return result;
  902. }();
  903. if (! updatedPropertyValue)
  904. {
  905. return std::tuple (std::move (prop),
  906. "Partial updates do not apply, perhaps the JSON pointer does not "
  907. "refer to an extant node");
  908. }
  909. prop.value.bytes = ci::Encodings::jsonTo7BitText (*updatedPropertyValue);
  910. return std::tuple (std::move (prop), String());
  911. }
  912. template <typename... Args>
  913. static auto forwardFunction (std::function<void (Args...)>& fn)
  914. {
  915. return [&fn] (auto&&... args)
  916. {
  917. NullCheckedInvocation::invoke (fn, std::forward<decltype (args)> (args)...);
  918. };
  919. }
  920. };
  921. template <typename Kind>
  922. class IOPickerModel : public ListBoxModel
  923. {
  924. public:
  925. explicit IOPickerModel (State<std::optional<MidiDeviceInfo>> s)
  926. : state (s) {}
  927. int getNumRows() override
  928. {
  929. return Kind::getAvailableDevices().size();
  930. }
  931. void paintListBoxItem (int, Graphics&, int, int, bool) override {}
  932. Component* refreshComponentForRow (int rowNumber,
  933. bool rowIsSelected,
  934. Component* existingComponentToUpdate) override
  935. {
  936. const auto toDelete = rawToUniquePtr (existingComponentToUpdate);
  937. const auto info = Kind::getAvailableDevices()[rowNumber];
  938. return Utils::makeListRowLabel (info.name, rowIsSelected).release();
  939. }
  940. void selectedRowsChanged (int newSelection) override
  941. {
  942. state = getInfoForRow (newSelection);
  943. }
  944. std::optional<MidiDeviceInfo> getInfoForRow (int row) const
  945. {
  946. return isPositiveAndBelow (row, Kind::getAvailableDevices().size())
  947. ? std::optional (Kind::getAvailableDevices()[row])
  948. : std::nullopt;
  949. }
  950. private:
  951. State<std::optional<MidiDeviceInfo>> state;
  952. };
  953. template <typename Kind>
  954. class IOPickerList : public Component
  955. {
  956. public:
  957. explicit IOPickerList (State<std::optional<MidiDeviceInfo>> s)
  958. : state (s),
  959. model (s)
  960. {
  961. addAndMakeVisible (label);
  962. label.setJustificationType (Justification::centred);
  963. addAndMakeVisible (list);
  964. list.setMultipleSelectionEnabled (false);
  965. list.setClickingTogglesRowSelection (true);
  966. list.updateContent();
  967. }
  968. void resized() override
  969. {
  970. FlexBox fb;
  971. fb.flexDirection = FlexBox::Direction::column;
  972. fb.items = { FlexItem { label }.withHeight (Utils::standardControlHeight),
  973. FlexItem{}.withHeight (Utils::padding),
  974. FlexItem { list }.withFlex (1) };
  975. fb.performLayout (getLocalBounds());
  976. }
  977. private:
  978. void setSelected (std::optional<MidiDeviceInfo> selected)
  979. {
  980. list.setSelectedRows ({}, dontSendNotification);
  981. list.updateContent();
  982. for (auto i = 0; i < model.getNumRows(); ++i)
  983. if (model.getInfoForRow (i) == selected)
  984. list.setSelectedRows (Utils::setWithSingleIndex (i), dontSendNotification);
  985. }
  986. static String getLabel (std::in_place_type_t<MidiInput>) { return "Input"; }
  987. static String getLabel (std::in_place_type_t<MidiOutput>) { return "Output"; }
  988. State<std::optional<MidiDeviceInfo>> state;
  989. Label label { "", getLabel (std::in_place_type<Kind>) };
  990. IOPickerModel<Kind> model;
  991. ListBox list { "Devices", &model };
  992. MidiDeviceListConnection connection = MidiDeviceListConnection::make ([this]
  993. {
  994. list.updateContent();
  995. setSelected (*state);
  996. });
  997. ErasedScopeGuard listener = state.observe ([this] (auto)
  998. {
  999. setSelected (*state);
  1000. });
  1001. };
  1002. class IOPickerLists : public Component
  1003. {
  1004. public:
  1005. explicit IOPickerLists (State<Model::IOSelection> selection)
  1006. : inputs (selection[&Model::IOSelection::input]),
  1007. outputs (selection[&Model::IOSelection::output])
  1008. {
  1009. addAndMakeVisible (inputs);
  1010. addAndMakeVisible (outputs);
  1011. }
  1012. void resized() override
  1013. {
  1014. Utils::doColumnLayout (getLocalBounds().reduced (Utils::padding), inputs, outputs);
  1015. }
  1016. private:
  1017. IOPickerList<MidiInput> inputs;
  1018. IOPickerList<MidiOutput> outputs;
  1019. };
  1020. class SectionHeader : public Component
  1021. {
  1022. public:
  1023. explicit SectionHeader (String text)
  1024. : label ("", text)
  1025. {
  1026. addAndMakeVisible (label);
  1027. setInterceptsMouseClicks (false, false);
  1028. setSize (1, Utils::standardControlHeight);
  1029. }
  1030. void resized() override
  1031. {
  1032. label.setBounds (getLocalBounds());
  1033. }
  1034. void paint (Graphics& g) override
  1035. {
  1036. g.setColour (findColour (TextEditor::ColourIds::highlightColourId));
  1037. g.fillRoundedRectangle (getLocalBounds().reduced (2).toFloat(), 2.0f);
  1038. }
  1039. void set (String str)
  1040. {
  1041. label.setText (str, dontSendNotification);
  1042. }
  1043. private:
  1044. Label label;
  1045. };
  1046. class ToggleSectionHeader : public Component
  1047. {
  1048. public:
  1049. ToggleSectionHeader (String text, State<bool> s)
  1050. : state (s),
  1051. button (text)
  1052. {
  1053. addAndMakeVisible (button);
  1054. setSize (1, Utils::standardControlHeight);
  1055. button.onClick = [this] { state = button.getToggleState(); };
  1056. }
  1057. void resized() override
  1058. {
  1059. button.setBounds (getLocalBounds());
  1060. }
  1061. void paint (Graphics& g) override
  1062. {
  1063. g.setColour (findColour (TextEditor::ColourIds::highlightColourId));
  1064. g.fillRoundedRectangle (getLocalBounds().reduced (2).toFloat(), 2.0f);
  1065. }
  1066. private:
  1067. State<bool> state;
  1068. ToggleButton button;
  1069. ErasedScopeGuard listener = state.observe ([this] (auto)
  1070. {
  1071. button.setToggleState (*state, dontSendNotification);
  1072. });
  1073. };
  1074. class PropertySectionHeader : public Component
  1075. {
  1076. public:
  1077. explicit PropertySectionHeader (State<Model::DeviceInfo> s)
  1078. : state (s)
  1079. {
  1080. addAndMakeVisible (toggle);
  1081. addAndMakeVisible (numConcurrentLabel);
  1082. addAndMakeVisible (numConcurrent);
  1083. numConcurrent.setJustification (Justification::centredLeft);
  1084. setSize (1, toggle.getHeight());
  1085. numConcurrent.onCommit ([this]
  1086. {
  1087. const auto clamped = (uint8_t) jlimit (1, 127, numConcurrent.getText().getIntValue());
  1088. state[&Model::DeviceInfo::numPropertyExchangeTransactions] = clamped;
  1089. refresh();
  1090. });
  1091. }
  1092. void resized() override
  1093. {
  1094. toggle.setBounds (getLocalBounds());
  1095. Utils::doTwoColumnLayout (getLocalBounds(), nullptr, numConcurrent);
  1096. Grid grid;
  1097. grid.autoFlow = Grid::AutoFlow::row;
  1098. grid.autoColumns = grid.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
  1099. grid.columnGap = grid.rowGap = Grid::Px { Utils::padding };
  1100. grid.templateColumns = { Grid::TrackInfo { Grid::Fr { 1 } },
  1101. Grid::TrackInfo { Grid::Fr { 1 } } };
  1102. grid.items = { GridItem { numConcurrentLabel }, GridItem { numConcurrent } };
  1103. grid.performLayout (numConcurrent.getBounds().reduced (Utils::padding, 4));
  1104. }
  1105. private:
  1106. void refresh()
  1107. {
  1108. numConcurrent.set (String (state->numPropertyExchangeTransactions));
  1109. }
  1110. State<Model::DeviceInfo> state;
  1111. ToggleSectionHeader toggle { "Properties", state[&Model::DeviceInfo::propertiesSupported] };
  1112. Label numConcurrentLabel { "", "Max Concurrent Streams" };
  1113. TextField<Editable::yes> numConcurrent;
  1114. std::vector<ErasedScopeGuard> listeners = Utils::makeVector
  1115. (
  1116. state[&Model::DeviceInfo::propertiesSupported].observe ([this] (auto)
  1117. {
  1118. const auto enabled = state->propertiesSupported;
  1119. numConcurrentLabel.setEnabled (enabled);
  1120. numConcurrent.setEnabled (enabled);
  1121. }),
  1122. state[&Model::DeviceInfo::numPropertyExchangeTransactions].observe ([this] (auto)
  1123. {
  1124. refresh();
  1125. })
  1126. );
  1127. };
  1128. class HeterogeneousListContents : public Component
  1129. {
  1130. public:
  1131. void addItem (Component& item)
  1132. {
  1133. contents.emplace_back (&item);
  1134. addAndMakeVisible (item);
  1135. }
  1136. void clear()
  1137. {
  1138. for (auto* c : contents)
  1139. removeChildComponent (c);
  1140. contents.clear();
  1141. }
  1142. void resized() override
  1143. {
  1144. auto y = 0;
  1145. for (auto* item : contents)
  1146. {
  1147. item->setBounds ({ 0, y, getWidth(), item->getHeight() });
  1148. y += item->getHeight();
  1149. }
  1150. }
  1151. int getIdealHeight() const
  1152. {
  1153. return std::accumulate (contents.begin(), contents.end(), 0, [] (auto acc, auto& item)
  1154. {
  1155. return acc + item->getHeight();
  1156. });
  1157. }
  1158. private:
  1159. std::vector<Component*> contents;
  1160. };
  1161. class HeterogeneousListView : public Component
  1162. {
  1163. public:
  1164. HeterogeneousListView()
  1165. {
  1166. addAndMakeVisible (viewport);
  1167. viewport.setViewedComponent (&contents, false);
  1168. viewport.setScrollBarsShown (true, false);
  1169. }
  1170. void resized() override
  1171. {
  1172. viewport.setBounds (getLocalBounds());
  1173. contents.setBounds ({ getWidth(), contents.getIdealHeight() });
  1174. }
  1175. void addItem (Component& item)
  1176. {
  1177. contents.addItem (item);
  1178. }
  1179. void clear()
  1180. {
  1181. contents.clear();
  1182. }
  1183. private:
  1184. HeterogeneousListContents contents;
  1185. Viewport viewport;
  1186. };
  1187. class ChannelStateButtonGrid : public Component
  1188. {
  1189. public:
  1190. static constexpr auto numChannelColumns = 17;
  1191. static constexpr auto numGroups = 16;
  1192. explicit ChannelStateButtonGrid (State<Model::Profiles> s)
  1193. : state (s)
  1194. {
  1195. auto index = 0;
  1196. for (auto& b : buttons)
  1197. {
  1198. const auto group = index / numChannelColumns;
  1199. const auto channel = index % numChannelColumns;
  1200. addAndMakeVisible (b);
  1201. b.setButtonText ("Group " + String (group) + " Channel " + String (channel));
  1202. b.onClick = [this, group, channel]
  1203. {
  1204. const auto channelKind = channel < 16 ? ci::ChannelInGroup (channel)
  1205. : ci::ChannelInGroup::wholeGroup;
  1206. const auto address = ci::ChannelAddress{}.withGroup (group)
  1207. .withChannel (channelKind);
  1208. auto selected = state[&Model::Profiles::selectedChannel];
  1209. if (*selected != address)
  1210. selected = address;
  1211. else
  1212. selected = std::nullopt;
  1213. };
  1214. ++index;
  1215. }
  1216. addAndMakeVisible (block);
  1217. block.setButtonText ("Function Block");
  1218. block.setDimensions ({ numChannelColumns, 1.0f });
  1219. block.onClick = [this]
  1220. {
  1221. const auto address = ci::ChannelAddress{}.withChannel (ci::ChannelInGroup::wholeBlock);
  1222. auto selected = state[&Model::Profiles::selectedChannel];
  1223. if (*selected != address)
  1224. selected = address;
  1225. else
  1226. selected = std::nullopt;
  1227. };
  1228. updateButtonVisibility();
  1229. }
  1230. void resized() override
  1231. {
  1232. const auto availableBounds = getLocalBounds().reduced (Utils::padding)
  1233. .withTrimmedLeft (labelWidth)
  1234. .withTrimmedTop (labelHeight);
  1235. const auto maxButtonSize = std::min (availableBounds.getWidth() / numChannelColumns,
  1236. availableBounds.getHeight() / (numGroups + 1));
  1237. Grid grid;
  1238. grid.autoFlow = Grid::AutoFlow::row;
  1239. grid.rowGap = grid.columnGap = Grid::Px { 1 };
  1240. grid.autoColumns = grid.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
  1241. grid.templateColumns = []
  1242. {
  1243. Array<Grid::TrackInfo> array;
  1244. for (auto i = 0; i < 17; ++i)
  1245. array.add (Grid::TrackInfo { Grid::Fr { 1 } });
  1246. return array;
  1247. }();
  1248. for (auto& b : buttons)
  1249. grid.items.add (GridItem { b });
  1250. grid.items.add (GridItem { block }.withArea ({}, GridItem::Span { numChannelColumns }));
  1251. const Rectangle<int> gridBounds { maxButtonSize * numChannelColumns,
  1252. maxButtonSize * (numGroups + 1) };
  1253. const RectanglePlacement placement(RectanglePlacement::yTop | RectanglePlacement::xMid);
  1254. grid.performLayout (placement.appliedTo (gridBounds, availableBounds));
  1255. block.setDimensions (block.getBounds().toFloat());
  1256. }
  1257. void paint (Graphics& g) override
  1258. {
  1259. g.setColour (findColour (Label::ColourIds::textColourId));
  1260. const auto groupWidth = 100;
  1261. GlyphArrangement groupArrangement;
  1262. groupArrangement.addJustifiedText ({},
  1263. "Group",
  1264. 0,
  1265. 0,
  1266. groupWidth,
  1267. Justification::horizontallyCentred);
  1268. Path groupPath;
  1269. groupArrangement.createPath (groupPath);
  1270. g.fillPath (groupPath,
  1271. AffineTransform().rotated (-MathConstants<float>::halfPi, groupWidth / 2, 0)
  1272. .translated (-groupWidth / 2, 0)
  1273. .translated (Point { buttons[0].getX() - 40,
  1274. buttons[8 * 17].getY() }.toFloat()));
  1275. for (auto i = 0; i < 16; ++i)
  1276. {
  1277. const auto bounds = buttons[(size_t) i * 17].getBounds();
  1278. g.drawSingleLineText (String (i + 1),
  1279. bounds.getX() - Utils::padding,
  1280. bounds.getBottom(),
  1281. Justification::right);
  1282. }
  1283. g.drawSingleLineText ("Block",
  1284. block.getX() - Utils::padding,
  1285. block.getBottom(),
  1286. Justification::right);
  1287. g.drawSingleLineText ("Channel",
  1288. buttons[8].getBounds().getCentreX(),
  1289. buttons[0].getY() - 40,
  1290. Justification::horizontallyCentred);
  1291. for (auto i = 0; i < 17; ++i)
  1292. {
  1293. const auto bounds = buttons[(size_t) i].getBounds();
  1294. GlyphArrangement channelArrangement;
  1295. channelArrangement.addJustifiedText ({},
  1296. i < 16 ? String (i + 1) : "All",
  1297. 0,
  1298. 0,
  1299. groupWidth,
  1300. Justification::left);
  1301. Path channelPath;
  1302. channelArrangement.createPath (channelPath);
  1303. const auto transform = AffineTransform()
  1304. .rotated (-MathConstants<float>::halfPi * 0.8, 0, 0)
  1305. .translated (Point { bounds.getRight(),
  1306. bounds.getY() - Utils::padding }
  1307. .toFloat());
  1308. g.fillPath (channelPath, transform);
  1309. }
  1310. if (auto selected = state->selectedChannel)
  1311. {
  1312. auto buttonBounds = getButtonForAddress (*selected).getBounds();
  1313. g.setColour (Colours::cyan);
  1314. g.drawRect (buttonBounds, 2);
  1315. }
  1316. }
  1317. private:
  1318. class Button : public ShapeButton
  1319. {
  1320. public:
  1321. Button()
  1322. : ShapeButton ("",
  1323. Colours::black.withAlpha (0.4f),
  1324. Colours::black.withAlpha (0.4f).brighter(),
  1325. Colours::black.withAlpha (0.4f).darker())
  1326. {
  1327. setDimensions ({ 1.0f, 1.0f });
  1328. const auto onColour = findColour (TextEditor::ColourIds::highlightColourId);
  1329. setOnColours (onColour, onColour.brighter(), onColour.darker());
  1330. shouldUseOnColours (true);
  1331. setClickingTogglesState (true);
  1332. }
  1333. void setDimensions (Rectangle<float> dimensions)
  1334. {
  1335. Path s;
  1336. s.addRectangle (dimensions);
  1337. setShape (s, false, true, false);
  1338. }
  1339. };
  1340. Button& getButtonForAddress (ci::ChannelAddress address)
  1341. {
  1342. if (address.isBlock())
  1343. return block;
  1344. const auto channelIndex = address.getChannel() != ci::ChannelInGroup::wholeGroup
  1345. ? (size_t) address.getChannel()
  1346. : 16;
  1347. const auto buttonIndex = (size_t) address.getGroup() * numChannelColumns + channelIndex;
  1348. return buttons[buttonIndex];
  1349. }
  1350. void updateButtonVisibility()
  1351. {
  1352. if (auto* profile = state->profiles.getSelected())
  1353. {
  1354. Utils::forAllChannelAddresses ([&] (auto address)
  1355. {
  1356. auto& button = getButtonForAddress (address);
  1357. const auto iter = state->channels.find ({ *profile, address });
  1358. const auto visible = state->profileMode == Model::ProfileMode::edit
  1359. || iter != state->channels.end();
  1360. button.setVisible (visible);
  1361. const auto lit = [&]
  1362. {
  1363. if (iter == state->channels.end())
  1364. return false;
  1365. if (state->profileMode == Model::ProfileMode::edit)
  1366. return iter->second.supported != 0;
  1367. return iter->second.active != 0;
  1368. }();
  1369. button.setToggleState (lit, dontSendNotification);
  1370. });
  1371. }
  1372. }
  1373. static constexpr auto labelWidth = 50;
  1374. static constexpr auto labelHeight = 50;
  1375. State<Model::Profiles> state;
  1376. std::array<Button, numGroups * numChannelColumns> buttons;
  1377. Button block;
  1378. ErasedScopeGuard listener = state.observe ([this] (const auto& old)
  1379. {
  1380. if (old.selectedChannel != state->selectedChannel)
  1381. repaint();
  1382. updateButtonVisibility();
  1383. });
  1384. };
  1385. class ProfileListModel : public ListBoxModel
  1386. {
  1387. public:
  1388. explicit ProfileListModel (State<Model::Profiles> s)
  1389. : state (s) {}
  1390. int getNumRows() override
  1391. {
  1392. return (int) state->profiles.items.size();
  1393. }
  1394. void paintListBoxItem (int, Graphics&, int, int, bool) override {}
  1395. Component* refreshComponentForRow (int rowNumber,
  1396. bool rowIsSelected,
  1397. Component* existingComponentToUpdate) override
  1398. {
  1399. const auto toDelete = rawToUniquePtr (existingComponentToUpdate);
  1400. const auto currentState = *state;
  1401. if (auto* item = currentState.profiles.get (rowNumber))
  1402. {
  1403. const auto name = String::toHexString (item->data(), (int) item->size());
  1404. return Utils::makeListRowLabel (name, rowIsSelected).release();
  1405. }
  1406. return nullptr;
  1407. }
  1408. void selectedRowsChanged (int newSelection) override
  1409. {
  1410. state[&Model::Profiles::profiles]
  1411. [&Model::ListWithSelection<ci::Profile>::selection] = newSelection;
  1412. state[&Model::Profiles::selectedChannel] = std::nullopt;
  1413. }
  1414. private:
  1415. State<Model::Profiles> state;
  1416. };
  1417. template <Editable editable>
  1418. class ProfileList : public Component
  1419. {
  1420. public:
  1421. explicit ProfileList (State<Model::Profiles> s)
  1422. : state (s)
  1423. {
  1424. addAndMakeVisible (list);
  1425. if constexpr (editable == Editable::yes)
  1426. {
  1427. addAndMakeVisible (add);
  1428. add.onClick = [this]
  1429. {
  1430. auto updated = *state;
  1431. updated.profiles.items.emplace_back();
  1432. updated.profiles.selection = (int) updated.profiles.items.size() - 1;
  1433. updated.selectedChannel = std::nullopt;
  1434. state = std::move (updated);
  1435. };
  1436. addAndMakeVisible (remove);
  1437. remove.onClick = [this]
  1438. {
  1439. auto updated = *state;
  1440. if (auto* item = updated.profiles.getSelected())
  1441. {
  1442. const auto toErase = *item;
  1443. Utils::forAllChannelAddresses ([&] (auto address)
  1444. {
  1445. updated.channels.erase (ci::ProfileAtAddress { toErase, address });
  1446. });
  1447. const auto erase = updated.profiles.items.begin() + updated.profiles.selection;
  1448. updated.profiles.items.erase (erase);
  1449. updated.profiles.selection = -1;
  1450. state = std::move (updated);
  1451. }
  1452. };
  1453. }
  1454. }
  1455. void resized() override
  1456. {
  1457. if constexpr (editable == Editable::yes)
  1458. {
  1459. FlexBox fb;
  1460. fb.flexDirection = FlexBox::Direction::column;
  1461. fb.items = { FlexItem { list }.withFlex (1),
  1462. FlexItem{}.withHeight (Utils::padding),
  1463. FlexItem{}.withHeight (Utils::standardControlHeight) };
  1464. fb.performLayout (getLocalBounds());
  1465. Utils::doColumnLayout (fb.items.getLast().currentBounds.getSmallestIntegerContainer(),
  1466. add,
  1467. remove);
  1468. }
  1469. else
  1470. {
  1471. list.setBounds (getLocalBounds());
  1472. }
  1473. }
  1474. private:
  1475. State<Model::Profiles> state;
  1476. ProfileListModel model { state };
  1477. ListBox list { "Profiles", &model };
  1478. TextButton add { "+" }, remove { "-" };
  1479. ErasedScopeGuard listener = state.observe ([this] (auto)
  1480. {
  1481. list.setSelectedRows ({}, dontSendNotification);
  1482. list.updateContent();
  1483. const auto& profs = state->profiles;
  1484. auto* item = profs.getSelected();
  1485. remove.setEnabled (item != nullptr);
  1486. if (item != nullptr)
  1487. {
  1488. list.setSelectedRows (Utils::setWithSingleIndex (profs.selection),
  1489. dontSendNotification);
  1490. }
  1491. });
  1492. };
  1493. class ProfileNamePane : public Component
  1494. {
  1495. public:
  1496. explicit ProfileNamePane (State<Model::Profiles> s)
  1497. : state (s)
  1498. {
  1499. addAndMakeVisible (label);
  1500. addAndMakeVisible (field);
  1501. field.onCommit ([this]
  1502. {
  1503. auto updated = *state;
  1504. if (auto* item = updated.profiles.getSelected())
  1505. {
  1506. Utils::stringToByteArray (field.getText(), *item);
  1507. state = std::move (updated);
  1508. }
  1509. refresh();
  1510. });
  1511. }
  1512. void resized() override
  1513. {
  1514. Utils::doColumnLayout (getLocalBounds(), label, field);
  1515. }
  1516. private:
  1517. void refresh()
  1518. {
  1519. if (auto* item = state->profiles.getSelected())
  1520. field.set (String::toHexString (item->data(), (int) item->size()));
  1521. }
  1522. State<Model::Profiles> state;
  1523. Label label { "", "Profile Identifier" };
  1524. TextField<Editable::yes> field;
  1525. ErasedScopeGuard listener = state.observe ([this] (auto) { refresh(); });
  1526. };
  1527. class NumChannelsPane : public Component
  1528. {
  1529. public:
  1530. explicit NumChannelsPane (State<Model::Profiles> s)
  1531. : state (s)
  1532. {
  1533. addAndMakeVisible (label);
  1534. addAndMakeVisible (field);
  1535. field.onCommit ([this]
  1536. {
  1537. NullCheckedInvocation::invoke (onChannelsRequested,
  1538. (uint16_t) field.getText().getIntValue());
  1539. refresh();
  1540. });
  1541. }
  1542. void resized() override
  1543. {
  1544. Utils::doColumnLayout (getLocalBounds(), label, field);
  1545. }
  1546. std::function<void (uint16_t)> onChannelsRequested;
  1547. private:
  1548. void refresh()
  1549. {
  1550. const auto attributes = [&]() -> ci::SupportedAndActive
  1551. {
  1552. const auto selected = state->getSelectedProfileAtAddress();
  1553. if (! selected.has_value())
  1554. return {};
  1555. const auto iter = state->channels.find (*selected);
  1556. if (iter == state->channels.end())
  1557. return {};
  1558. return iter->second;
  1559. }();
  1560. const auto numToShow = state->profileMode == Model::ProfileMode::edit
  1561. ? attributes.supported
  1562. : attributes.active;
  1563. field.set (String (numToShow));
  1564. }
  1565. State<Model::Profiles> state;
  1566. Label label;
  1567. TextField<Editable::yes> field;
  1568. ErasedScopeGuard listener = state.observe ([this] (auto)
  1569. {
  1570. const auto text = state->profileMode == Model::ProfileMode::edit
  1571. ? "Num Supported Channels"
  1572. : "Num Active Channels";
  1573. label.setText (text, dontSendNotification);
  1574. refresh();
  1575. });
  1576. };
  1577. template <Editable editable>
  1578. class ProfileDetailsPane : public Component
  1579. {
  1580. public:
  1581. explicit ProfileDetailsPane (State<Model::Profiles> s)
  1582. : state (s)
  1583. {
  1584. if constexpr (editable == Editable::yes)
  1585. {
  1586. addAndMakeVisible (name);
  1587. addAndMakeVisible (edit);
  1588. edit.setClickingTogglesState (false);
  1589. edit.onClick = [this]
  1590. {
  1591. state[&Model::Profiles::profileMode] = Model::ProfileMode::edit;
  1592. };
  1593. addAndMakeVisible (use);
  1594. use.setClickingTogglesState (false);
  1595. use.onClick = [this]
  1596. {
  1597. state[&Model::Profiles::profileMode] = Model::ProfileMode::use;
  1598. };
  1599. channels.onChannelsRequested = [this] (auto numChannels)
  1600. {
  1601. auto updated = *state;
  1602. const auto selected = updated.getSelectedProfileAtAddress();
  1603. if (! selected.has_value())
  1604. return;
  1605. auto& value = updated.channels[*selected];
  1606. if (updated.profileMode == Model::ProfileMode::edit)
  1607. value.supported = jmax ((uint16_t) 0, numChannels);
  1608. else
  1609. value.active = jlimit ((uint16_t) 0, value.supported, numChannels);
  1610. state = std::move (updated);
  1611. };
  1612. invert.onClick = [this]
  1613. {
  1614. if (const auto selected = state->getSelectedProfileAtAddress())
  1615. {
  1616. auto updated = *state;
  1617. auto& value = updated.channels[*selected];
  1618. auto& toInvert = updated.profileMode == Model::ProfileMode::edit
  1619. ? value.supported
  1620. : value.active;
  1621. toInvert = toInvert == 0 ? 1 : 0;
  1622. state = std::move (updated);
  1623. }
  1624. };
  1625. }
  1626. else
  1627. {
  1628. channels.onChannelsRequested = Utils::forwardFunction (onChannelsRequested);
  1629. invert.onClick = [this]
  1630. {
  1631. if (const auto selected = state->getSelectedProfileAtAddress())
  1632. {
  1633. const auto iter = state->channels.find (*selected);
  1634. if (iter == state->channels.end())
  1635. return;
  1636. auto& toInvert = state->profileMode == Model::ProfileMode::edit
  1637. ? iter->second.supported
  1638. : iter->second.active;
  1639. NullCheckedInvocation::invoke (onChannelsRequested,
  1640. (uint16) (toInvert == 0 ? 1 : 0));
  1641. }
  1642. };
  1643. }
  1644. addAndMakeVisible (grid);
  1645. addAndMakeVisible (channels);
  1646. addAndMakeVisible (invert);
  1647. }
  1648. void resized() override
  1649. {
  1650. Grid g;
  1651. g.autoFlow = Grid::AutoFlow::row;
  1652. g.autoColumns = g.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
  1653. g.columnGap = g.rowGap = Grid::Px { Utils::padding };
  1654. g.templateColumns = { Grid::TrackInfo { Grid::Fr { 1 } },
  1655. Grid::TrackInfo { Grid::Fr { 1 } } };
  1656. if constexpr (editable == Editable::yes)
  1657. {
  1658. g.templateRows = { Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
  1659. Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
  1660. Grid::TrackInfo { Grid::Fr { 1 } },
  1661. Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
  1662. Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } } };
  1663. g.items = { GridItem { name }.withArea ({}, GridItem::Span { 2 }),
  1664. GridItem { edit }, GridItem { use },
  1665. GridItem { grid }.withArea ({}, GridItem::Span { 2 }),
  1666. GridItem { invert }.withArea ({}, GridItem::Span { 2 }),
  1667. GridItem { channels }.withArea ({}, GridItem::Span { 2 }) };
  1668. }
  1669. else
  1670. {
  1671. g.templateRows = { Grid::TrackInfo { Grid::Fr { 1 } },
  1672. Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
  1673. Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } } };
  1674. g.items = { GridItem { grid }.withArea ({}, GridItem::Span { 2 }),
  1675. GridItem { invert }.withArea ({}, GridItem::Span { 2 }),
  1676. GridItem { channels }.withArea ({}, GridItem::Span { 2 }) };
  1677. }
  1678. g.performLayout (getLocalBounds());
  1679. }
  1680. std::function<void (uint16_t)> onChannelsRequested;
  1681. private:
  1682. State<Model::Profiles> state;
  1683. ProfileNamePane name { state };
  1684. ToggleButton edit { "Show Supported Channels" }, use { "Show Active Channels" };
  1685. TextButton invert { "Toggle Member Channels" };
  1686. ChannelStateButtonGrid grid { state };
  1687. NumChannelsPane channels { state };
  1688. std::vector<ErasedScopeGuard> listeners = Utils::makeVector
  1689. (
  1690. state[&Model::Profiles::profileMode].observe ([this] (auto)
  1691. {
  1692. edit.setToggleState (state->profileMode == Model::ProfileMode::edit,
  1693. dontSendNotification);
  1694. use.setToggleState (state->profileMode == Model::ProfileMode::use,
  1695. dontSendNotification);
  1696. }),
  1697. state.observe ([this] (auto)
  1698. {
  1699. invert.setVisible (state->getSelectedProfileAtAddress().has_value());
  1700. channels.setVisible (state->getSelectedProfileAtAddress().has_value());
  1701. })
  1702. );
  1703. };
  1704. template <Editable editable>
  1705. class ProfileConfigPanel : public Component
  1706. {
  1707. public:
  1708. explicit ProfileConfigPanel (State<Model::Profiles> s)
  1709. : state (s)
  1710. {
  1711. setSize (1, 500);
  1712. addAndMakeVisible (list);
  1713. }
  1714. void resized() override
  1715. {
  1716. auto* d = details.has_value() ? &*details : nullptr;
  1717. Utils::doTwoColumnLayout (getLocalBounds().reduced (Utils::padding), list, d);
  1718. }
  1719. std::function<void (uint16_t)> onChannelsRequested;
  1720. private:
  1721. State<Model::Profiles> state;
  1722. ProfileList<editable> list { state };
  1723. std::optional<ProfileDetailsPane<editable>> details;
  1724. ErasedScopeGuard listener = state.observe ([this] (auto)
  1725. {
  1726. if (state->profiles.getSelected() != nullptr)
  1727. {
  1728. if (! details.has_value())
  1729. {
  1730. addAndMakeVisible (details.emplace (state));
  1731. details->onChannelsRequested = Utils::forwardFunction (onChannelsRequested);
  1732. resized();
  1733. }
  1734. }
  1735. else
  1736. {
  1737. details.reset();
  1738. }
  1739. });
  1740. };
  1741. template <Editable editable>
  1742. class DiscoveryInfoPanel : public Component
  1743. {
  1744. public:
  1745. DiscoveryInfoPanel (State<ci::MUID> m, State<Model::DeviceInfo> s)
  1746. : muidState (m), state (s)
  1747. {
  1748. [&] (auto&&... item)
  1749. {
  1750. (addAndMakeVisible (item), ...);
  1751. ((item.onCommit ([this] { setStateFromUI(); })), ...);
  1752. } (manufacturer, family, modelNumber, revision, maxSysExSize);
  1753. [&] (auto&&... item)
  1754. {
  1755. (addAndMakeVisible (item), ...);
  1756. } (muid,
  1757. muidLabel,
  1758. manufacturerLabel,
  1759. familyLabel,
  1760. modelNumberLabel,
  1761. revisionLabel,
  1762. maxSysExSizeLabel);
  1763. setSize (1, 6 * Utils::standardControlHeight + 7 * Utils::padding);
  1764. }
  1765. void resized() override
  1766. {
  1767. Utils::doTwoColumnLayout (getLocalBounds().reduced (Utils::padding),
  1768. muidLabel, muid,
  1769. maxSysExSizeLabel, maxSysExSize,
  1770. manufacturerLabel, manufacturer,
  1771. familyLabel, family,
  1772. modelNumberLabel, modelNumber,
  1773. revisionLabel, revision);
  1774. }
  1775. private:
  1776. void setStateFromUI()
  1777. {
  1778. auto updated = *state;
  1779. Utils::stringToByteArray (manufacturer.getText(), updated.deviceInfo.manufacturer);
  1780. Utils::stringToByteArray (family .getText(), updated.deviceInfo.family);
  1781. Utils::stringToByteArray (modelNumber .getText(), updated.deviceInfo.modelNumber);
  1782. Utils::stringToByteArray (revision .getText(), updated.deviceInfo.revision);
  1783. updated.maxSysExSize = (size_t) maxSysExSize.getText().getIntValue();
  1784. state = std::move (updated);
  1785. refresh();
  1786. }
  1787. void refresh()
  1788. {
  1789. maxSysExSize.set (String (state->maxSysExSize));
  1790. auto& info = state->deviceInfo;
  1791. manufacturer.set (byteArrayToString (info.manufacturer));
  1792. family .set (byteArrayToString (info.family));
  1793. modelNumber .set (byteArrayToString (info.modelNumber));
  1794. revision .set (byteArrayToString (info.revision));
  1795. }
  1796. static String byteArrayToString (Span<const std::byte> bytes)
  1797. {
  1798. return String::toHexString (bytes.data(), (int) bytes.size());
  1799. }
  1800. State<ci::MUID> muidState;
  1801. State<Model::DeviceInfo> state;
  1802. Label muidLabel { "", "MUID (hex)" },
  1803. maxSysExSizeLabel { "", "Maximum SysEx size (decimal)" },
  1804. manufacturerLabel { "", "Manufacturer (hex, 3 bytes)" },
  1805. familyLabel { "", "Family (hex, 2 bytes)" },
  1806. modelNumberLabel { "", "Model (hex, 2 bytes)" },
  1807. revisionLabel { "", "Revision (hex, 4 bytes)" };
  1808. TextField<Editable::no> muid;
  1809. TextField<editable> maxSysExSize, manufacturer, family, modelNumber, revision;
  1810. std::vector<ErasedScopeGuard> listeners = Utils::makeVector
  1811. (
  1812. muidState.observe ([this] (auto)
  1813. {
  1814. muid.set (String::toHexString (muidState->get()));
  1815. }),
  1816. state.observe ([this] (auto) { refresh(); })
  1817. );
  1818. };
  1819. class PropertyListModel : public ListBoxModel
  1820. {
  1821. public:
  1822. explicit PropertyListModel (State<Model::Properties> s)
  1823. : state (s) {}
  1824. int getNumRows() override
  1825. {
  1826. return (int) state->properties.items.size();
  1827. }
  1828. void paintListBoxItem (int, Graphics&, int, int, bool) override {}
  1829. Component* refreshComponentForRow (int rowNumber,
  1830. bool rowIsSelected,
  1831. Component* existingComponentToUpdate) override
  1832. {
  1833. const auto toDelete = rawToUniquePtr (existingComponentToUpdate);
  1834. const auto currentState = *state;
  1835. if (auto* item = currentState.properties.get (rowNumber))
  1836. {
  1837. const auto name = item->name;
  1838. return Utils::makeListRowLabel (name, rowIsSelected).release();
  1839. }
  1840. return nullptr;
  1841. }
  1842. void selectedRowsChanged (int newSelection) override
  1843. {
  1844. state[&Model::Properties::properties]
  1845. [&Model::ListWithSelection<Model::Property>::selection] = newSelection;
  1846. }
  1847. private:
  1848. State<Model::Properties> state;
  1849. };
  1850. template <Editable editable>
  1851. class PropertyList : public Component
  1852. {
  1853. public:
  1854. explicit PropertyList (State<Model::Properties> s)
  1855. : state (s)
  1856. {
  1857. addAndMakeVisible (list);
  1858. if constexpr (editable == Editable::yes)
  1859. {
  1860. addAndMakeVisible (add);
  1861. add.onClick = [this]
  1862. {
  1863. auto updated = *state;
  1864. updated.properties.items.emplace_back();
  1865. updated.properties.selection = (int) updated.properties.items.size() - 1;
  1866. state = std::move (updated);
  1867. };
  1868. addAndMakeVisible (remove);
  1869. remove.onClick = [this]
  1870. {
  1871. auto updated = *state;
  1872. auto& props = updated.properties;
  1873. if (auto* item = props.getSelected())
  1874. {
  1875. const auto toErase = props.items.begin() + props.selection;
  1876. props.items.erase (toErase);
  1877. props.selection = -1;
  1878. state = std::move (updated);
  1879. }
  1880. };
  1881. }
  1882. }
  1883. void resized() override
  1884. {
  1885. if constexpr (editable == Editable::yes)
  1886. {
  1887. FlexBox fb;
  1888. fb.flexDirection = FlexBox::Direction::column;
  1889. fb.items = { FlexItem { list }.withFlex (1),
  1890. FlexItem{}.withHeight (Utils::padding),
  1891. FlexItem{}.withHeight (Utils::standardControlHeight) };
  1892. fb.performLayout (getLocalBounds());
  1893. Utils::doColumnLayout (fb.items.getLast().currentBounds.getSmallestIntegerContainer(),
  1894. add,
  1895. remove);
  1896. }
  1897. else
  1898. {
  1899. list.setBounds (getLocalBounds());
  1900. }
  1901. }
  1902. private:
  1903. State<Model::Properties> state;
  1904. PropertyListModel model { state };
  1905. ListBox list { "Profiles", &model };
  1906. TextButton add { "+" }, remove { "-" };
  1907. ErasedScopeGuard listener = state.observe ([this] (auto)
  1908. {
  1909. list.setSelectedRows ({}, dontSendNotification);
  1910. list.updateContent();
  1911. const auto& props = state->properties;
  1912. auto* item = props.getSelected();
  1913. remove.setEnabled (item != nullptr);
  1914. if (item != nullptr)
  1915. {
  1916. list.setSelectedRows (Utils::setWithSingleIndex (props.selection),
  1917. dontSendNotification);
  1918. }
  1919. });
  1920. };
  1921. class PropertySubscribersModel : public ListBoxModel
  1922. {
  1923. public:
  1924. explicit PropertySubscribersModel (State<Model::App> s) : state (s) {}
  1925. int getNumRows() override { return (int) entries.size(); }
  1926. void paintListBoxItem (int, Graphics&, int, int, bool) override {}
  1927. Component* refreshComponentForRow (int rowNumber,
  1928. bool,
  1929. Component* existingComponentToUpdate) override
  1930. {
  1931. const auto toDelete = rawToUniquePtr (existingComponentToUpdate);
  1932. if (! isPositiveAndBelow (rowNumber, entries.size()))
  1933. return nullptr;
  1934. const auto info = entries[(size_t) rowNumber];
  1935. return Utils::makeListRowLabel (info, false).release();
  1936. }
  1937. void refresh()
  1938. {
  1939. entries = [&]() -> std::vector<String>
  1940. {
  1941. const auto& props = state->saved.properties;
  1942. if (auto* item = props.properties.getSelected())
  1943. {
  1944. const auto selected = item->name;
  1945. const auto& subs = state->transient.subscribers;
  1946. const auto iter = subs.find (selected);
  1947. if (iter == subs.end())
  1948. return {};
  1949. std::vector<String> result;
  1950. for (const auto& [device, subscriptions] : iter->second)
  1951. for (const auto& s : subscriptions)
  1952. result.push_back (String::toHexString (device.get()) + " (" + s + ")");
  1953. return result;
  1954. }
  1955. return {};
  1956. }();
  1957. }
  1958. private:
  1959. State<Model::App> state;
  1960. std::vector<String> entries;
  1961. };
  1962. template <Editable editable>
  1963. class PropertySubscribersPanel : public Component
  1964. {
  1965. public:
  1966. explicit PropertySubscribersPanel (State<Model::App> s)
  1967. : state (s)
  1968. {
  1969. addAndMakeVisible (list);
  1970. list.setMultipleSelectionEnabled (false);
  1971. list.setClickingTogglesRowSelection (false);
  1972. }
  1973. void resized() override
  1974. {
  1975. list.setBounds (getLocalBounds().reduced (Utils::padding));
  1976. }
  1977. private:
  1978. State<Model::App> state;
  1979. PropertySubscribersModel model { state };
  1980. ListBox list { "Subscribers", &model };
  1981. ErasedScopeGuard listener = state.observe ([this] (auto)
  1982. {
  1983. model.refresh();
  1984. list.updateContent();
  1985. });
  1986. };
  1987. template <Editable editable>
  1988. class PropertyValuePanel : public Component
  1989. {
  1990. enum class Kind { full, partial };
  1991. auto getFileChooserCallback (Kind kind)
  1992. {
  1993. return [this, kind] (const auto&)
  1994. {
  1995. if (propertyFileChooser.getResults().isEmpty())
  1996. return;
  1997. auto updated = *state;
  1998. if (auto* item = updated.properties.getSelected())
  1999. {
  2000. MemoryBlock block;
  2001. propertyFileChooser.getResult().loadFileAsData (block);
  2002. const auto* data = static_cast<const std::byte*> (block.getData());
  2003. if constexpr (editable == Editable::yes)
  2004. {
  2005. std::vector<std::byte> asByteVec (data, data + block.getSize());
  2006. if (kind == Kind::full)
  2007. {
  2008. item->value.bytes = std::move (asByteVec);
  2009. }
  2010. else
  2011. {
  2012. struct Header
  2013. {
  2014. String mediaType;
  2015. ci::Encoding mutualEncoding{};
  2016. };
  2017. struct Request
  2018. {
  2019. Header header;
  2020. std::vector<std::byte> body;
  2021. };
  2022. Request request { { "application/json", ci::Encoding::ascii },
  2023. std::move (asByteVec) };
  2024. auto [newItem, error] = Utils::attemptSetPartial (std::move (*item),
  2025. std::move (request));
  2026. *item = std::move (newItem);
  2027. jassert (error.isEmpty()); // Inspect error to find out what went wrong
  2028. }
  2029. state = std::move (updated);
  2030. }
  2031. else
  2032. {
  2033. NullCheckedInvocation::invoke (kind == Kind::full ? onSetFullRequested
  2034. : onSetPartialRequested,
  2035. Span (data, block.getSize()));
  2036. }
  2037. }
  2038. };
  2039. }
  2040. public:
  2041. explicit PropertyValuePanel (State<Model::Properties> s)
  2042. : PropertyValuePanel (s, {}) {}
  2043. PropertyValuePanel (State<Model::Properties> s, State<std::map<ci::SubscriptionKey, ci::Subscription>> subState)
  2044. : state (s), subscriptions (subState)
  2045. {
  2046. addAndMakeVisible (value);
  2047. value.setReadOnly (true);
  2048. value.setMultiLine (true);
  2049. addAndMakeVisible (formatLabel);
  2050. addAndMakeVisible (format);
  2051. format.onCommit ([this]
  2052. {
  2053. auto adjusted = *state;
  2054. if (auto* item = adjusted.properties.getSelected())
  2055. {
  2056. item->value.mediaType = format.getText();
  2057. state = adjusted;
  2058. }
  2059. refresh();
  2060. });
  2061. addAndMakeVisible (viewAsHex);
  2062. viewAsHex.onClick = [this]
  2063. {
  2064. state[&Model::Properties::mode] = Model::DataViewMode::hex;
  2065. };
  2066. addAndMakeVisible (viewAsAscii);
  2067. viewAsAscii.onClick = [this]
  2068. {
  2069. state[&Model::Properties::mode] = Model::DataViewMode::ascii;
  2070. };
  2071. addAndMakeVisible (setFull);
  2072. setFull.onClick = [this]
  2073. {
  2074. propertyFileChooser.launchAsync (FileBrowserComponent::canSelectFiles
  2075. | FileBrowserComponent::openMode,
  2076. getFileChooserCallback (Kind::full));
  2077. };
  2078. addAndMakeVisible (setPartial);
  2079. setPartial.onClick = [this]
  2080. {
  2081. propertyFileChooser.launchAsync (FileBrowserComponent::canSelectFiles
  2082. | FileBrowserComponent::openMode,
  2083. getFileChooserCallback (Kind::partial));
  2084. };
  2085. if constexpr (editable == Editable::no)
  2086. {
  2087. addAndMakeVisible (get);
  2088. get.onClick = Utils::forwardFunction (onGetRequested);
  2089. addAndMakeVisible (subscribe);
  2090. subscribe.onClick = Utils::forwardFunction (onSubscribeRequested);
  2091. }
  2092. }
  2093. void resized() override
  2094. {
  2095. Grid grid;
  2096. grid.autoFlow = Grid::AutoFlow::row;
  2097. grid.autoColumns = grid.autoRows = Grid::TrackInfo { Grid::Fr { 1 } };
  2098. grid.columnGap = grid.rowGap = Grid::Px { Utils::padding };
  2099. grid.templateColumns = { Grid::TrackInfo { Grid::Fr { 1 } },
  2100. Grid::TrackInfo { Grid::Fr { 1 } } };
  2101. if constexpr (editable == Editable::yes)
  2102. {
  2103. grid.templateRows = { Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
  2104. Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
  2105. Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
  2106. Grid::TrackInfo { Grid::Fr { 1 } } };
  2107. grid.items = { GridItem { setFull }, GridItem { setPartial },
  2108. GridItem { formatLabel }, GridItem { format },
  2109. GridItem { viewAsHex }, GridItem { viewAsAscii },
  2110. GridItem { value }.withArea ({}, GridItem::Span { 2 }) };
  2111. }
  2112. else
  2113. {
  2114. grid.templateRows = { Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
  2115. Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
  2116. Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
  2117. Grid::TrackInfo { Grid::Px { Utils::standardControlHeight } },
  2118. Grid::TrackInfo { Grid::Fr { 1 } } };
  2119. grid.items = { GridItem { setFull }, GridItem { setPartial },
  2120. GridItem { get }, GridItem { subscribe },
  2121. GridItem { formatLabel }, GridItem { format },
  2122. GridItem { viewAsHex }, GridItem { viewAsAscii },
  2123. GridItem { value }.withArea ({}, GridItem::Span { 2 }) };
  2124. }
  2125. grid.performLayout (getLocalBounds().reduced (Utils::padding));
  2126. }
  2127. std::function<void()> onGetRequested, onSubscribeRequested;
  2128. std::function<void (Span<const std::byte>)> onSetFullRequested, onSetPartialRequested;
  2129. private:
  2130. void refresh()
  2131. {
  2132. const auto& currentState = *state;
  2133. if (auto* item = currentState.properties.getSelected())
  2134. format.set (item->value.mediaType);
  2135. const auto mode = state->mode;
  2136. viewAsHex.setToggleState (mode == Model::DataViewMode::hex, dontSendNotification);
  2137. viewAsAscii.setToggleState (mode == Model::DataViewMode::ascii, dontSendNotification);
  2138. }
  2139. void updateSubButtonText()
  2140. {
  2141. const auto& sub = *subscriptions;
  2142. if (const auto* selectedProp = state->properties.getSelected())
  2143. {
  2144. const auto text = std::any_of (sub.begin(), sub.end(), [&] (const auto& p) { return p.second.resource == selectedProp->name; }) ? "Unsubscribe" : "Subscribe";
  2145. subscribe.setButtonText (text);
  2146. }
  2147. }
  2148. State<Model::Properties> state;
  2149. State<std::map<ci::SubscriptionKey, ci::Subscription>> subscriptions;
  2150. MonospaceEditor value;
  2151. TextField<editable> format;
  2152. Label formatLabel { "", "Media Type" };
  2153. ToggleButton viewAsHex { "Hex" };
  2154. ToggleButton viewAsAscii { "ASCII" };
  2155. TextButton setFull { "Set Full..." },
  2156. setPartial { "Set Partial..." },
  2157. get { "Get" },
  2158. subscribe { "Subscribe" };
  2159. FileChooser propertyFileChooser { "Property Data", {}, "*", true, false, this };
  2160. std::vector<ErasedScopeGuard> listeners = Utils::makeVector
  2161. (
  2162. state.observe ([this] (const auto& old)
  2163. {
  2164. updateSubButtonText();
  2165. refresh();
  2166. const auto& currentState = *state;
  2167. if (auto* item = currentState.properties.getSelected())
  2168. {
  2169. const auto mode = currentState.mode;
  2170. if (mode == old.mode
  2171. && old.properties.getSelected() != nullptr
  2172. && *old.properties.getSelected() == *item)
  2173. {
  2174. return;
  2175. }
  2176. const auto canSetFull = item->canSet != Model::CanSet::none
  2177. || editable == Editable::yes;
  2178. setFull.setEnabled (canSetFull);
  2179. setPartial.setEnabled (item->canSet == Model::CanSet::partial);
  2180. get.setEnabled (item->canGet);
  2181. auto* oldSelection = old.properties.getSelected();
  2182. const auto needsValueUpdate = old.mode != mode
  2183. || oldSelection == nullptr
  2184. || oldSelection->value.bytes != item->value.bytes;
  2185. if (! needsValueUpdate)
  2186. return;
  2187. value.set ("");
  2188. switch (mode)
  2189. {
  2190. case Model::DataViewMode::hex:
  2191. value.setColour (TextEditor::ColourIds::textColourId,
  2192. findColour (TextEditor::ColourIds::textColourId));
  2193. value.setText (String::toHexString (item->value.bytes.data(),
  2194. (int) item->value.bytes.size()),
  2195. false);
  2196. break;
  2197. case Model::DataViewMode::ascii:
  2198. String toShow;
  2199. for (auto& b : item->value.bytes)
  2200. {
  2201. const char ascii[] { (char) b, 0 };
  2202. toShow << (b < std::byte { 0x80 } ? ascii : "\xef\xbf\xbd");
  2203. }
  2204. value.set (toShow);
  2205. break;
  2206. }
  2207. }
  2208. }),
  2209. subscriptions.observe ([this] (const auto&)
  2210. {
  2211. updateSubButtonText();
  2212. })
  2213. );
  2214. };
  2215. template <Editable editable>
  2216. class PropertyInfoPanel : public Component
  2217. {
  2218. public:
  2219. explicit PropertyInfoPanel (State<Model::Properties> s)
  2220. : state (s)
  2221. {
  2222. if constexpr (editable == Editable::yes)
  2223. {
  2224. addAndMakeVisible (canSet);
  2225. canSet.addItemList ({ "None", "Full", "Partial" }, 1);
  2226. canSet.onChange = [this] { updateStateFromUI(); };
  2227. }
  2228. else
  2229. {
  2230. addAndMakeVisible (canSetField);
  2231. }
  2232. [&] (auto&&... args)
  2233. {
  2234. (addAndMakeVisible (args), ...);
  2235. (args.setClickingTogglesState (editable == Editable::yes), ...);
  2236. ((args.onClick = [this] { updateStateFromUI(); }), ...);
  2237. } (canGet,
  2238. canSubscribe,
  2239. canPaginate,
  2240. requireResId,
  2241. canAsciiEncode,
  2242. canMcoded7Encode,
  2243. canZipMcoded7Encode);
  2244. [&] (auto&&... args)
  2245. {
  2246. (addAndMakeVisible (args), ...);
  2247. } (nameLabel, schemaLabel, mediaTypesLabel, columnsLabel, canSetLabel, listLabel);
  2248. [&] (auto&&... args)
  2249. {
  2250. (addAndMakeVisible (args), ...);
  2251. (args.setReadOnly (editable == Editable::no), ...);
  2252. (args.setMultiLine (true), ...);
  2253. ((args.onReturnKey = args.onEscapeKey
  2254. = args.onFocusLost
  2255. = [this] { updateStateFromUI(); }), ...);
  2256. } (schema, mediaTypes, columns);
  2257. addAndMakeVisible (name);
  2258. name.onCommit ([this]
  2259. {
  2260. updateStateFromUI();
  2261. refresh();
  2262. });
  2263. updateUIFromState();
  2264. }
  2265. void resized() override
  2266. {
  2267. Grid grid;
  2268. grid.autoFlow = Grid::AutoFlow::row;
  2269. grid.autoColumns = Grid::TrackInfo { Grid::Fr { 1 } };
  2270. const Grid::TrackInfo tallRow { Grid::Px { 100 } },
  2271. shortRow { Grid::Px { Utils::standardControlHeight } };
  2272. grid.columnGap = grid.rowGap = Grid::Px { Utils::padding };
  2273. grid.templateColumns = { Grid::TrackInfo { Grid::Fr { 1 } },
  2274. Grid::TrackInfo { Grid::Fr { 1 } } };
  2275. grid.templateRows = { shortRow,
  2276. tallRow,
  2277. tallRow,
  2278. shortRow,
  2279. shortRow,
  2280. shortRow,
  2281. shortRow,
  2282. shortRow,
  2283. shortRow,
  2284. tallRow };
  2285. auto* canSetControl = editable == Editable::yes ? static_cast<Component*> (&canSet)
  2286. : static_cast<Component*> (&canSetField);
  2287. grid.items = { GridItem { nameLabel }, GridItem { name },
  2288. GridItem { mediaTypesLabel }, GridItem { mediaTypes },
  2289. GridItem { schemaLabel }, GridItem { schema },
  2290. GridItem { canSetLabel }, GridItem { canSetControl },
  2291. GridItem { canGet }, GridItem { canSubscribe },
  2292. GridItem { requireResId }, GridItem { canAsciiEncode },
  2293. GridItem { canMcoded7Encode }, GridItem { canZipMcoded7Encode },
  2294. GridItem { listLabel }.withArea ({}, GridItem::Span { 2 }),
  2295. GridItem { canPaginate }.withArea ({}, GridItem::Span { 2 }),
  2296. GridItem { columnsLabel }, GridItem { columns } };
  2297. grid.performLayout (getLocalBounds().reduced (Utils::padding));
  2298. }
  2299. private:
  2300. void refresh()
  2301. {
  2302. const auto currentState = *state;
  2303. if (auto* item = state->properties.getSelected())
  2304. name.set (item->name);
  2305. }
  2306. void updateStateFromUI()
  2307. {
  2308. if constexpr (editable == Editable::yes)
  2309. {
  2310. auto updated = *state;
  2311. auto& props = updated.properties;
  2312. if (auto* item = props.getSelected())
  2313. {
  2314. auto cachedData = item->value;
  2315. *item = getInfoFromUI();
  2316. item->value = cachedData;
  2317. state = std::move (updated);
  2318. }
  2319. }
  2320. }
  2321. Model::Property getInfoFromUI() const
  2322. {
  2323. Model::Property result;
  2324. result.name = name.getText();
  2325. result.schema = JSON::fromString (schema.getText());
  2326. auto lines = StringArray::fromLines (mediaTypes.getText() + "\n");
  2327. lines.removeEmptyStrings();
  2328. result.mediaTypes = std::vector<String> (lines.begin(), lines.end());
  2329. result.columns = JSON::fromString (columns.getText());
  2330. result.encodings = [&]
  2331. {
  2332. std::set<ci::Encoding> encodings;
  2333. if (canAsciiEncode.getToggleState())
  2334. encodings.insert (ci::Encoding::ascii);
  2335. if (canMcoded7Encode.getToggleState())
  2336. encodings.insert (ci::Encoding::mcoded7);
  2337. if (canZipMcoded7Encode.getToggleState())
  2338. encodings.insert (ci::Encoding::zlibAndMcoded7);
  2339. return encodings;
  2340. }();
  2341. result.canSet = (Model::CanSet) canSet.getSelectedItemIndex();
  2342. result.canGet = canGet.getToggleState();
  2343. result.canSubscribe = canSubscribe.getToggleState();
  2344. result.requireResId = requireResId.getToggleState();
  2345. result.canPaginate = canPaginate.getToggleState();
  2346. return result;
  2347. }
  2348. void updateUIFromState()
  2349. {
  2350. refresh();
  2351. const auto currentState = *state;
  2352. if (auto* item = state->properties.getSelected())
  2353. {
  2354. schema.setText (JSON::toString (item->schema), false);
  2355. const auto pairs = { std::tuple (&canAsciiEncode, ci::Encoding::ascii),
  2356. std::tuple (&canMcoded7Encode, ci::Encoding::mcoded7),
  2357. std::tuple (&canZipMcoded7Encode, ci::Encoding::zlibAndMcoded7) };
  2358. for (const auto& [button, encoding] : pairs)
  2359. {
  2360. button->setToggleState (item->encodings.count (encoding) != 0,
  2361. dontSendNotification);
  2362. }
  2363. mediaTypes.setText (StringArray (item->mediaTypes.data(),
  2364. (int) item->mediaTypes.size()).joinIntoString ("\n"),
  2365. false);
  2366. columns.setText (JSON::toString (item->columns), false);
  2367. canSetField.set (Model::CanSetUtils::toString (item->canSet));
  2368. canSet.setSelectedItemIndex ((int) item->canSet, dontSendNotification);
  2369. canGet.setToggleState (item->canGet, dontSendNotification);
  2370. canSubscribe.setToggleState (item->canSubscribe, dontSendNotification);
  2371. requireResId.setToggleState (item->requireResId, dontSendNotification);
  2372. canPaginate.setToggleState (item->canPaginate, dontSendNotification);
  2373. const auto list = item->name.endsWith ("List");
  2374. canPaginate.setEnabled (list);
  2375. columnsLabel.setEnabled (list);
  2376. columns.setEnabled (list);
  2377. }
  2378. }
  2379. State<Model::Properties> state;
  2380. Label nameLabel { "", "Name" },
  2381. schemaLabel { "", "Schema" },
  2382. mediaTypesLabel { "", "Media Types" },
  2383. columnsLabel { "", "Columns (json array)" },
  2384. canSetLabel { "", "Can Set" },
  2385. listLabel { "", "List Properties (only valid when Name ends with \"List\")" };
  2386. TextField<editable> name, canSetField;
  2387. MonospaceEditor schema, mediaTypes, columns;
  2388. ComboBox canSet;
  2389. ToggleButton canGet { "Can Get" },
  2390. canSubscribe { "Can Subscribe" },
  2391. canPaginate { "Can Paginate" },
  2392. requireResId { "Require Res ID" },
  2393. canAsciiEncode { "Can ASCII Encode" },
  2394. canMcoded7Encode { "Can Mcoded7 Encode" },
  2395. canZipMcoded7Encode { "Can zlib+Mcoded7 Encode" };
  2396. ErasedScopeGuard listener = state.observe ([this] (auto)
  2397. {
  2398. updateUIFromState();
  2399. });
  2400. };
  2401. template <Editable>
  2402. class PropertyEditPanel;
  2403. template <>
  2404. class PropertyEditPanel<Editable::yes> : public Component
  2405. {
  2406. public:
  2407. explicit PropertyEditPanel (State<Model::App> s)
  2408. : state (s)
  2409. {
  2410. addAndMakeVisible (tabs);
  2411. tabs.addTab ("Info",
  2412. findColour (DocumentWindow::backgroundColourId),
  2413. &info,
  2414. false);
  2415. tabs.addTab ("Value",
  2416. findColour (DocumentWindow::backgroundColourId),
  2417. &value,
  2418. false);
  2419. tabs.addTab ("Subscribers",
  2420. findColour (DocumentWindow::backgroundColourId),
  2421. &subscribers,
  2422. false);
  2423. }
  2424. void resized() override
  2425. {
  2426. tabs.setBounds (getLocalBounds());
  2427. }
  2428. private:
  2429. State<Model::App> state;
  2430. PropertyInfoPanel<Editable::yes> info { state[&Model::App::saved][&Model::Saved::properties] };
  2431. PropertyValuePanel<Editable::yes> value { state[&Model::App::saved]
  2432. [&Model::Saved::properties] };
  2433. PropertySubscribersPanel<Editable::yes> subscribers { state };
  2434. TabbedComponent tabs { TabbedButtonBar::Orientation::TabsAtTop };
  2435. };
  2436. template <>
  2437. class PropertyEditPanel<Editable::no> : public Component
  2438. {
  2439. public:
  2440. explicit PropertyEditPanel (State<Model::App> s)
  2441. : state (s)
  2442. {
  2443. addAndMakeVisible (tabs);
  2444. tabs.addTab ("Info",
  2445. findColour (DocumentWindow::backgroundColourId),
  2446. &info,
  2447. false);
  2448. tabs.addTab ("Value",
  2449. findColour (DocumentWindow::backgroundColourId),
  2450. &value,
  2451. false);
  2452. value.onSetFullRequested = Utils::forwardFunction (onSetFullRequested);
  2453. value.onSetPartialRequested = Utils::forwardFunction (onSetPartialRequested);
  2454. value.onGetRequested = Utils::forwardFunction (onGetRequested);
  2455. value.onSubscribeRequested = Utils::forwardFunction (onSubscribeRequested);
  2456. }
  2457. void resized() override
  2458. {
  2459. tabs.setBounds (getLocalBounds());
  2460. }
  2461. std::function<void()> onGetRequested, onSubscribeRequested;
  2462. std::function<void (Span<const std::byte>)> onSetFullRequested, onSetPartialRequested;
  2463. private:
  2464. State<Model::Device> getDeviceState() const
  2465. {
  2466. const auto selected = (size_t) state->transient.devices.selection;
  2467. return state[&Model::App::transient]
  2468. [&Model::Transient::devices]
  2469. [&Model::ListWithSelection<Model::Device>::items]
  2470. [selected];
  2471. }
  2472. State<Model::App> state;
  2473. PropertyInfoPanel<Editable::no> info { getDeviceState()[&Model::Device::properties] };
  2474. PropertyValuePanel<Editable::no> value
  2475. {
  2476. getDeviceState()[&Model::Device::properties],
  2477. getDeviceState()[&Model::Device::subscriptions]
  2478. };
  2479. TabbedComponent tabs { TabbedButtonBar::Orientation::TabsAtTop };
  2480. };
  2481. template <Editable editable>
  2482. class PropertyConfigPanel : public Component
  2483. {
  2484. public:
  2485. explicit PropertyConfigPanel (State<Model::App> s)
  2486. : state (s)
  2487. {
  2488. setSize (1, 700);
  2489. addAndMakeVisible (list);
  2490. }
  2491. void resized() override
  2492. {
  2493. Utils::doTwoColumnLayout (getLocalBounds().reduced (Utils::padding),
  2494. list,
  2495. GridItem { info.has_value() ? &*info : nullptr });
  2496. }
  2497. std::function<void()> onGetRequested, onSubscribeRequested;
  2498. std::function<void (Span<const std::byte>)> onSetFullRequested, onSetPartialRequested;
  2499. private:
  2500. State<Model::Properties> getPropertyState() const
  2501. {
  2502. if constexpr (editable == Editable::yes)
  2503. {
  2504. return state[&Model::App::saved][&Model::Saved::properties];
  2505. }
  2506. else
  2507. {
  2508. const auto selected = (size_t) state->transient.devices.selection;
  2509. return state[&Model::App::transient]
  2510. [&Model::Transient::devices]
  2511. [&Model::ListWithSelection<Model::Device>::items]
  2512. [selected]
  2513. [&Model::Device::properties];
  2514. }
  2515. }
  2516. State<Model::App> state;
  2517. PropertyList<editable> list { getPropertyState() };
  2518. std::optional<PropertyEditPanel<editable>> info;
  2519. ErasedScopeGuard listener = state.observe ([this] (auto)
  2520. {
  2521. if (getPropertyState()->properties.getSelected() != nullptr)
  2522. {
  2523. if (! info.has_value())
  2524. {
  2525. addAndMakeVisible (info.emplace (state));
  2526. if constexpr (editable == Editable::no)
  2527. {
  2528. info->onSetFullRequested = Utils::forwardFunction (onSetFullRequested);
  2529. info->onSetPartialRequested = Utils::forwardFunction (onSetPartialRequested);
  2530. info->onGetRequested = Utils::forwardFunction (onGetRequested);
  2531. info->onSubscribeRequested = Utils::forwardFunction (onSubscribeRequested);
  2532. }
  2533. resized();
  2534. }
  2535. }
  2536. else
  2537. {
  2538. info.reset();
  2539. }
  2540. });
  2541. };
  2542. class LocalConfigurationPanel : public Component
  2543. {
  2544. public:
  2545. LocalConfigurationPanel (State<ci::MUID> m, State<Model::App> s)
  2546. : muidState (m), state (s)
  2547. {
  2548. addAndMakeVisible (list);
  2549. list.addItem (basicsHeader);
  2550. list.addItem (discovery);
  2551. list.addItem (profilesHeader);
  2552. list.addItem (profiles);
  2553. list.addItem (propertiesHeader);
  2554. list.addItem (properties);
  2555. }
  2556. void resized() override
  2557. {
  2558. list.setBounds (getLocalBounds());
  2559. }
  2560. private:
  2561. State<ci::MUID> muidState;
  2562. State<Model::App> state;
  2563. SectionHeader basicsHeader { "Basics" };
  2564. ToggleSectionHeader profilesHeader { "Profiles",
  2565. state[&Model::App::saved]
  2566. [&Model::Saved::fundamentals]
  2567. [&Model::DeviceInfo::profilesSupported] };
  2568. PropertySectionHeader propertiesHeader { state[&Model::App::saved]
  2569. [&Model::Saved::fundamentals] };
  2570. DiscoveryInfoPanel<Editable::yes> discovery { muidState,
  2571. state[&Model::App::saved]
  2572. [&Model::Saved::fundamentals] };
  2573. ProfileConfigPanel<Editable::yes> profiles { state[&Model::App::saved]
  2574. [&Model::Saved::profiles] };
  2575. PropertyConfigPanel<Editable::yes> properties { state };
  2576. HeterogeneousListView list;
  2577. ErasedScopeGuard listener = state[&Model::App::saved]
  2578. [&Model::Saved::fundamentals].observe ([this] (auto)
  2579. {
  2580. const auto current = state->saved.fundamentals;
  2581. profiles.setEnabled (current.profilesSupported);
  2582. properties.setEnabled (current.propertiesSupported);
  2583. });
  2584. };
  2585. class RemoteConfigurationPanel : public Component
  2586. {
  2587. public:
  2588. explicit RemoteConfigurationPanel (State<Model::App> s)
  2589. : state (s)
  2590. {
  2591. addAndMakeVisible (list);
  2592. profiles.onChannelsRequested = Utils::forwardFunction (onChannelsRequested);
  2593. properties.onSetFullRequested = Utils::forwardFunction (onPropertySetFullRequested);
  2594. properties.onSetPartialRequested = Utils::forwardFunction (onPropertySetPartialRequested);
  2595. properties.onGetRequested = Utils::forwardFunction (onPropertyGetRequested);
  2596. properties.onSubscribeRequested = Utils::forwardFunction (onPropertySubscribeRequested);
  2597. rebuildList();
  2598. }
  2599. void resized() override
  2600. {
  2601. list.setBounds (getLocalBounds());
  2602. }
  2603. std::function<void (uint16_t)> onChannelsRequested;
  2604. std::function<void()> onPropertyGetRequested, onPropertySubscribeRequested;
  2605. std::function<void (Span<const std::byte>)> onPropertySetFullRequested,
  2606. onPropertySetPartialRequested;
  2607. private:
  2608. void rebuildList()
  2609. {
  2610. list.clear();
  2611. list.addItem (basicsHeader);
  2612. list.addItem (discovery);
  2613. if (getDeviceState()->info.profilesSupported)
  2614. {
  2615. list.addItem (profilesHeader);
  2616. list.addItem (profiles);
  2617. }
  2618. if (getDeviceState()->info.propertiesSupported)
  2619. {
  2620. list.addItem (propertiesHeader);
  2621. list.addItem (properties);
  2622. }
  2623. list.resized();
  2624. }
  2625. State<Model::Device> getDeviceState() const
  2626. {
  2627. const auto selected = (size_t) state->transient.devices.selection;
  2628. return state[&Model::App::transient]
  2629. [&Model::Transient::devices]
  2630. [&Model::ListWithSelection<Model::Device>::items]
  2631. [selected];
  2632. }
  2633. State<Model::App> state;
  2634. SectionHeader basicsHeader { "Basics" };
  2635. DiscoveryInfoPanel<Editable::no> discovery { getDeviceState()[&Model::Device::muid],
  2636. getDeviceState()[&Model::Device::info] };
  2637. SectionHeader profilesHeader { "Profiles" }, propertiesHeader { "Properties" };
  2638. ProfileConfigPanel<Editable::no> profiles { getDeviceState()[&Model::Device::profiles] };
  2639. PropertyConfigPanel<Editable::no> properties { state };
  2640. HeterogeneousListView list;
  2641. ErasedScopeGuard listener = getDeviceState().observe ([this] (const auto& old)
  2642. {
  2643. const auto transactions = (int) getDeviceState()->info.numPropertyExchangeTransactions;
  2644. const auto plural = transactions == 1 ? "transaction" : "transactions";
  2645. propertiesHeader.set ("Properties ("
  2646. + String (transactions)
  2647. + " simultaneous "
  2648. + String (plural)
  2649. + " supported)");
  2650. const auto& newInfo = getDeviceState()->info;
  2651. if (std::tie (old.info.propertiesSupported, old.info.profilesSupported)
  2652. != std::tie (newInfo.propertiesSupported, newInfo.profilesSupported))
  2653. {
  2654. rebuildList();
  2655. }
  2656. });
  2657. };
  2658. class DiscoveryPanel : public Component
  2659. {
  2660. public:
  2661. explicit DiscoveryPanel (State<Model::App> s)
  2662. : state (s)
  2663. {
  2664. addAndMakeVisible (discoveryButton);
  2665. discoveryButton.onClick = Utils::forwardFunction (onDiscover);
  2666. addAndMakeVisible (deviceCombo);
  2667. deviceCombo.setTextWhenNoChoicesAvailable ("Press Discover Devices to find new devices");
  2668. deviceCombo.setTextWhenNothingSelected ("No device selected");
  2669. }
  2670. void resized() override
  2671. {
  2672. const auto headerHeight = Utils::standardControlHeight + 2 * Utils::padding;
  2673. auto b = getLocalBounds();
  2674. Utils::doTwoColumnLayout (b.removeFromTop (headerHeight).reduced (Utils::padding),
  2675. discoveryButton,
  2676. deviceCombo);
  2677. if (configPanel.has_value())
  2678. configPanel->setBounds (b);
  2679. }
  2680. std::function<void (uint16_t)> onChannelsRequested;
  2681. std::function<void()> onDiscover, onPropertyGetRequested, onPropertySubscribeRequested;
  2682. std::function<void (Span<const std::byte>)> onPropertySetFullRequested,
  2683. onPropertySetPartialRequested;
  2684. private:
  2685. State<Model::App> state;
  2686. TextButton discoveryButton { "Discover Devices" };
  2687. ComboBox deviceCombo;
  2688. std::optional<RemoteConfigurationPanel> configPanel;
  2689. ErasedScopeGuard listener = state[&Model::App::transient]
  2690. [&Model::Transient::devices].observe ([this] (const auto& old)
  2691. {
  2692. if (old.items != state->transient.devices.items)
  2693. {
  2694. deviceCombo.getRootMenu()->clear();
  2695. auto indexState = state[&Model::App::transient]
  2696. [&Model::Transient::devices]
  2697. [&Model::ListWithSelection<Model::Device>::selection];
  2698. auto index = 0;
  2699. for (auto& dev : state->transient.devices.items)
  2700. {
  2701. const auto suffix = [&]() -> String
  2702. {
  2703. if (const auto readable = dev.properties.getReadableDeviceInfo())
  2704. {
  2705. String result;
  2706. if (readable->model.isNotEmpty())
  2707. result << " " << readable->model;
  2708. if (readable->manufacturer.isNotEmpty())
  2709. result << " (" << readable->manufacturer << ")";
  2710. return result;
  2711. }
  2712. return {};
  2713. }();
  2714. PopupMenu::Item item;
  2715. item.action = [indexState, i = index++]() mutable { indexState = i; };
  2716. item.text = String::toHexString (dev.muid.get()) + suffix;
  2717. item.itemID = index;
  2718. deviceCombo.getRootMenu()->addItem (std::move (item));
  2719. }
  2720. }
  2721. deviceCombo.setSelectedItemIndex (state->transient.devices.selection, dontSendNotification);
  2722. if (state->transient.devices.getSelected() != nullptr)
  2723. {
  2724. const auto newIndex = state->transient.devices.selection;
  2725. if (old.selection != newIndex)
  2726. {
  2727. addAndMakeVisible (configPanel.emplace (state));
  2728. configPanel->onChannelsRequested =
  2729. Utils::forwardFunction (onChannelsRequested);
  2730. configPanel->onPropertySetFullRequested =
  2731. Utils::forwardFunction (onPropertySetFullRequested);
  2732. configPanel->onPropertySetPartialRequested =
  2733. Utils::forwardFunction (onPropertySetPartialRequested);
  2734. configPanel->onPropertyGetRequested =
  2735. Utils::forwardFunction (onPropertyGetRequested);
  2736. configPanel->onPropertySubscribeRequested =
  2737. Utils::forwardFunction (onPropertySubscribeRequested);
  2738. }
  2739. }
  2740. else
  2741. {
  2742. configPanel.reset();
  2743. }
  2744. resized();
  2745. });
  2746. };
  2747. /** Accumulates incoming MIDI messages on the MIDI thread, and passes these messages off to other
  2748. Consumers on the main thread.
  2749. This is useful because the ci::Device is only intended for single-threaded use. It is an error
  2750. to call ci::Device::receiveMessage concurrently with any other member function on a particular
  2751. instance of Device.
  2752. This implementation uses a mutex to protect access to the accumulated packets, and an
  2753. AsyncUpdater to signal the main thread to wake up and process the packets. Alternative
  2754. approaches (e.g. using notify_one and wait from std::atomic_flag, or putting messages into a
  2755. queue and polling periodically on the main thread) may be more suitable in production apps.
  2756. */
  2757. class MessageForwarder : public MidiInputCallback,
  2758. private AsyncUpdater
  2759. {
  2760. public:
  2761. void handleIncomingMidiMessage (MidiInput*, const MidiMessage& message) override
  2762. {
  2763. if (! message.isSysEx())
  2764. return;
  2765. {
  2766. const std::scoped_lock lock { mutex };
  2767. messages.push_back (message);
  2768. }
  2769. triggerAsyncUpdate();
  2770. }
  2771. [[nodiscard]] ErasedScopeGuard addConsumer (ci::DeviceMessageHandler& c)
  2772. {
  2773. return ErasedScopeGuard { [this, it = consumers.insert (&c).first] { consumers.erase (it); } };
  2774. }
  2775. private:
  2776. void handleAsyncUpdate() override
  2777. {
  2778. const std::scoped_lock lock { mutex };
  2779. for (auto* c : consumers)
  2780. for (const auto& message : messages)
  2781. c->processMessage ({ 0, message.getSysExDataSpan() });
  2782. messages.clear();
  2783. }
  2784. std::set<ci::DeviceMessageHandler*> consumers;
  2785. std::mutex mutex;
  2786. std::vector<MidiMessage> messages;
  2787. };
  2788. class LoggingModel : public TableListBoxModel
  2789. {
  2790. public:
  2791. enum Columns
  2792. {
  2793. messageTime = 1,
  2794. group,
  2795. direction,
  2796. from,
  2797. to,
  2798. version,
  2799. channel,
  2800. description,
  2801. };
  2802. explicit LoggingModel (State<Model::App> s) : state (s) {}
  2803. Component* refreshComponentForCell (int rowNumber,
  2804. int columnId,
  2805. bool,
  2806. Component* existingComponentToUpdate) override
  2807. {
  2808. auto owned = rawToUniquePtr (existingComponentToUpdate);
  2809. const auto filtered = getFiltered();
  2810. if (! isPositiveAndBelow (rowNumber, filtered.size()))
  2811. return nullptr;
  2812. auto ownedLabel = [&owned]
  2813. {
  2814. auto* label = dynamic_cast<Label*> (owned.get());
  2815. if (label == nullptr)
  2816. return Utils::makeListRowLabel ("", false);
  2817. owned.release();
  2818. return rawToUniquePtr (label);
  2819. }();
  2820. const auto& row = filtered[(size_t) rowNumber];
  2821. const auto text = [&]() -> String
  2822. {
  2823. const auto parsed = ci::Parser::parse (row.message);
  2824. if (! parsed.has_value())
  2825. return {};
  2826. switch (columnId)
  2827. {
  2828. case messageTime:
  2829. return row.time.toString (false, true, true, true);
  2830. case group:
  2831. return String (row.group);
  2832. case direction:
  2833. return row.kind == Model::MessageKind::incoming ? "in" : "out";
  2834. case from:
  2835. return String::toHexString (parsed->header.source.get());
  2836. case to:
  2837. return String::toHexString (parsed->header.destination.get());
  2838. case version:
  2839. return String::toHexString (parsed->header.version);
  2840. case channel:
  2841. return ci::ChannelInGroupUtils::toString (parsed->header.deviceID);
  2842. case description:
  2843. return state->saved.logView.mode == Model::DataViewMode::ascii
  2844. ? ci::Parser::getMessageDescription (*parsed)
  2845. : String::toHexString (row.message.data(), (int) row.message.size());
  2846. }
  2847. return {};
  2848. }();
  2849. ownedLabel->setText (text, dontSendNotification);
  2850. return ownedLabel.release();
  2851. }
  2852. int getNumRows() override { return (int) getFiltered().size(); }
  2853. void paintRowBackground (Graphics&, int, int, int, bool) override {}
  2854. void paintCell (Graphics&, int, int, int, int, bool) override {}
  2855. private:
  2856. std::deque<Model::LogEntry> getFiltered() const
  2857. {
  2858. const auto& filter = state->saved.logView.filter;
  2859. auto copy = state->transient.logEntries;
  2860. if (! filter.has_value())
  2861. return copy;
  2862. const auto iter = std::remove_if (copy.begin(),
  2863. copy.end(),
  2864. [&] (const auto& e) { return e.kind != *filter; });
  2865. copy.erase (iter, copy.end());
  2866. return copy;
  2867. }
  2868. State<Model::App> state;
  2869. };
  2870. class LoggingList : public Component,
  2871. private Timer
  2872. {
  2873. public:
  2874. explicit LoggingList (State<Model::App> s)
  2875. : state (s)
  2876. {
  2877. addAndMakeVisible (list);
  2878. auto& header = list.getHeader();
  2879. header.addColumn ("Time",
  2880. LoggingModel::Columns::messageTime,
  2881. 100,
  2882. 100);
  2883. header.addColumn ("Group",
  2884. LoggingModel::Columns::group,
  2885. 50,
  2886. 50);
  2887. header.addColumn ("IO",
  2888. LoggingModel::Columns::direction,
  2889. 50,
  2890. 50);
  2891. header.addColumn ("From",
  2892. LoggingModel::Columns::from,
  2893. 60,
  2894. 50);
  2895. header.addColumn ("To",
  2896. LoggingModel::Columns::to,
  2897. 60,
  2898. 50);
  2899. header.addColumn ("Version",
  2900. LoggingModel::Columns::version,
  2901. 50,
  2902. 50);
  2903. header.addColumn ("Channel",
  2904. LoggingModel::Columns::channel,
  2905. 100,
  2906. 50);
  2907. header.addColumn ("Description",
  2908. LoggingModel::Columns::description,
  2909. 300,
  2910. 50);
  2911. }
  2912. void resized() override
  2913. {
  2914. list.setBounds (getLocalBounds());
  2915. }
  2916. void updateContent()
  2917. {
  2918. // Using a timer here means that we only repaint the UI after there haven't been any
  2919. // new messages for a while, which avoids doing redundant expensive list-layouts.
  2920. startTimer (16);
  2921. }
  2922. private:
  2923. void timerCallback() override
  2924. {
  2925. const auto& vbar = list.getVerticalScrollBar();
  2926. const auto endShowing = vbar.getCurrentRange().getEnd() >= vbar.getMaximumRangeLimit();
  2927. stopTimer();
  2928. list.updateContent();
  2929. if (endShowing)
  2930. list.scrollToEnsureRowIsOnscreen (list.getNumRows() - 1);
  2931. }
  2932. State<Model::App> state;
  2933. LoggingModel model { state };
  2934. TableListBox list { "Logs", &model };
  2935. ErasedScopeGuard listener = state[&Model::App::transient]
  2936. [&Model::Transient::logEntries].observe ([this] (auto)
  2937. {
  2938. updateContent();
  2939. });
  2940. };
  2941. class LoggingPanel : public Component
  2942. {
  2943. public:
  2944. explicit LoggingPanel (State<Model::App> stateIn)
  2945. : state (stateIn)
  2946. {
  2947. addAndMakeVisible (list);
  2948. addAndMakeVisible (onlyIncoming);
  2949. onlyIncoming.onClick = [this]
  2950. {
  2951. auto s = state[&Model::App::saved][&Model::Saved::logView][&Model::LogView::filter];
  2952. s = *s == Model::MessageKind::incoming ? std::nullopt
  2953. : std::optional (Model::MessageKind::incoming);
  2954. };
  2955. addAndMakeVisible (onlyOutgoing);
  2956. onlyOutgoing.onClick = [this]
  2957. {
  2958. auto s = state[&Model::App::saved][&Model::Saved::logView][&Model::LogView::filter];
  2959. s = *s == Model::MessageKind::outgoing ? std::nullopt
  2960. : std::optional (Model::MessageKind::outgoing);
  2961. };
  2962. addAndMakeVisible (readable);
  2963. readable.onClick = [this]
  2964. {
  2965. auto s = state[&Model::App::saved][&Model::Saved::logView][&Model::LogView::mode];
  2966. s = readable.getToggleState() ? Model::DataViewMode::ascii : Model::DataViewMode::hex;
  2967. };
  2968. addAndMakeVisible (clearButton);
  2969. clearButton.onClick = [this]
  2970. {
  2971. state[&Model::App::transient]
  2972. [&Model::Transient::logEntries] = std::deque<Model::LogEntry>();
  2973. };
  2974. }
  2975. void resized() override
  2976. {
  2977. FlexBox fb;
  2978. fb.flexDirection = FlexBox::Direction::column;
  2979. fb.items = { FlexItem{}.withHeight (Utils::standardControlHeight),
  2980. FlexItem{}.withHeight (Utils::padding),
  2981. FlexItem { list }.withFlex (1) };
  2982. fb.performLayout (getLocalBounds().reduced (Utils::padding));
  2983. Utils::doColumnLayout (fb.items.getFirst().currentBounds.getSmallestIntegerContainer(),
  2984. onlyIncoming,
  2985. onlyOutgoing,
  2986. readable,
  2987. clearButton);
  2988. }
  2989. private:
  2990. State<Model::App> state;
  2991. LoggingList list { state };
  2992. ToggleButton onlyIncoming { "Only Incoming" }, onlyOutgoing { "Only Outgoing" };
  2993. ToggleButton readable { "Human-Readable" };
  2994. TextButton clearButton { "Clear" };
  2995. ErasedScopeGuard listener = state[&Model::App::saved]
  2996. [&Model::Saved::logView].observe ([this] (auto)
  2997. {
  2998. list.updateContent();
  2999. onlyIncoming.setToggleState (state->saved.logView.filter == Model::MessageKind::incoming,
  3000. dontSendNotification);
  3001. onlyOutgoing.setToggleState (state->saved.logView.filter == Model::MessageKind::outgoing,
  3002. dontSendNotification);
  3003. readable.setToggleState (state->saved.logView.mode == Model::DataViewMode::ascii,
  3004. dontSendNotification);
  3005. });
  3006. };
  3007. class CapabilityInquiryDemo : public Component,
  3008. private Timer
  3009. {
  3010. public:
  3011. CapabilityInquiryDemo()
  3012. {
  3013. PropertiesFile::Options options;
  3014. options.applicationName = "CapabilityInquiryDemo";
  3015. options.filenameSuffix = "settings";
  3016. options.osxLibrarySubFolder = "Application Support";
  3017. applicationProperties.setStorageParameters (options);
  3018. if (auto* userSettings = applicationProperties.getUserSettings())
  3019. setSavedState (JSON::parse (userSettings->getValue ("savedState")));
  3020. forwarder.addConsumer (inputHandler).release();
  3021. setSize (800, 800);
  3022. addAndMakeVisible (tabs);
  3023. tabs.addTab ("MIDI IO",
  3024. findColour (DocumentWindow::backgroundColourId),
  3025. &lists,
  3026. false);
  3027. tabs.addTab ("Local Configuration",
  3028. findColour (DocumentWindow::backgroundColourId),
  3029. &local,
  3030. false);
  3031. tabs.addTab ("Discovery",
  3032. findColour (DocumentWindow::backgroundColourId),
  3033. &discovery,
  3034. false);
  3035. tabs.addTab ("Logging",
  3036. findColour (DocumentWindow::backgroundColourId),
  3037. &logging,
  3038. false);
  3039. addAndMakeVisible (loadButton);
  3040. loadButton.onClick = [this] { loadState(); };
  3041. addAndMakeVisible (saveButton);
  3042. saveButton.onClick = [this] { saveState(); };
  3043. discovery.onDiscover = [this] { discoverDevices(); };
  3044. discovery.onPropertyGetRequested = [this] { getProperty(); };
  3045. discovery.onPropertySubscribeRequested = [this] { subscribeToProperty(); };
  3046. discovery.onChannelsRequested = [this] (auto channels) { setProfileChannels (channels); };
  3047. discovery.onPropertySetFullRequested = [this] (Span<const std::byte> bytes)
  3048. {
  3049. setPropertyFull (bytes);
  3050. };
  3051. discovery.onPropertySetPartialRequested = [this] (Span<const std::byte> bytes)
  3052. {
  3053. setPropertyPartial (bytes);
  3054. };
  3055. startTimer (2'000);
  3056. }
  3057. ~CapabilityInquiryDemo() override
  3058. {
  3059. stopTimer();
  3060. // In a production app, it'd be a bit risky to write to a file from a destructor as it's
  3061. // bad karma to throw an exception inside a destructor!
  3062. if (auto* userSettings = applicationProperties.getUserSettings())
  3063. userSettings->setValue ("savedState", JSON::toString (getSavedState()));
  3064. if (auto* p = getPeer())
  3065. p->setHasChangedSinceSaved (false);
  3066. }
  3067. void resized() override
  3068. {
  3069. tabs.setBounds (getLocalBounds());
  3070. const auto buttonBounds = getLocalBounds().removeFromTop (tabs.getTabBarDepth())
  3071. .removeFromRight (300)
  3072. .reduced (2);
  3073. Utils::doColumnLayout (buttonBounds, loadButton, saveButton);
  3074. }
  3075. private:
  3076. void timerCallback() override
  3077. {
  3078. if (device.has_value())
  3079. device->sendPendingMessages();
  3080. }
  3081. std::optional<std::tuple<ci::MUID, String>> getPropertyRequestInfo() const
  3082. {
  3083. auto* selectedDevice = appState->transient.devices.getSelected();
  3084. if (selectedDevice == nullptr)
  3085. return {};
  3086. auto* selectedProperty = selectedDevice->properties.properties.getSelected();
  3087. if (selectedProperty == nullptr)
  3088. return {};
  3089. return std::tuple (selectedDevice->muid, selectedProperty->name);
  3090. }
  3091. void discoverDevices()
  3092. {
  3093. if (device.has_value())
  3094. device->sendDiscovery();
  3095. }
  3096. void loadState()
  3097. {
  3098. fileChooser.launchAsync (FileBrowserComponent::canSelectFiles | FileBrowserComponent::openMode,
  3099. [this] (const auto& fc)
  3100. {
  3101. if (fc.getResults().isEmpty())
  3102. return;
  3103. auto stream = fc.getResult().createInputStream();
  3104. if (stream == nullptr)
  3105. return;
  3106. setSavedState (JSON::parse (*stream));
  3107. });
  3108. }
  3109. void saveState()
  3110. {
  3111. const auto toWrite = JSON::toString (getSavedState());
  3112. fileChooser.launchAsync (FileBrowserComponent::canSelectFiles | FileBrowserComponent::saveMode,
  3113. [this, toWrite] (const auto& fc)
  3114. {
  3115. if (! fc.getResults().isEmpty())
  3116. {
  3117. fc.getResult().replaceWithText (toWrite);
  3118. if (auto* p = getPeer())
  3119. p->setHasChangedSinceSaved (false);
  3120. }
  3121. });
  3122. }
  3123. void setProfileChannels (uint16_t numChannels)
  3124. {
  3125. if (! device.has_value())
  3126. return;
  3127. if (auto* selectedDevice = appState->transient.devices.getSelected())
  3128. {
  3129. if (const auto selected = selectedDevice->profiles.getSelectedProfileAtAddress())
  3130. {
  3131. device->sendProfileEnablement (selectedDevice->muid,
  3132. selected->address.getChannel(),
  3133. selected->profile,
  3134. numChannels);
  3135. }
  3136. }
  3137. }
  3138. void setPropertyFull (Span<const std::byte> bytes)
  3139. {
  3140. if (! device.has_value())
  3141. return;
  3142. if (const auto details = getPropertyRequestInfo())
  3143. {
  3144. const auto& [muid, propName] = *details;
  3145. const auto encodingToUse = [&, muidCopy = muid, propNameCopy = propName]() -> std::optional<ci::Encoding>
  3146. {
  3147. if (auto* deviceResource = findDeviceResource (appState->transient, muidCopy, propNameCopy))
  3148. return deviceResource->getBestCommonEncoding();
  3149. return {};
  3150. }();
  3151. if (! encodingToUse.has_value())
  3152. {
  3153. // We can't set property data because we don't have any encodings in common with the other device.
  3154. jassertfalse;
  3155. return;
  3156. }
  3157. ci::PropertyRequestHeader header;
  3158. header.resource = propName;
  3159. header.mutualEncoding = *encodingToUse;
  3160. device->sendPropertySetInquiry (muid, header, bytes, [] (const auto&)
  3161. {
  3162. // Could do error handling here, e.g. retry the request if the responder is busy
  3163. });
  3164. }
  3165. }
  3166. void setPropertyPartial (Span<const std::byte> bytes)
  3167. {
  3168. if (! device.has_value())
  3169. return;
  3170. if (const auto details = getPropertyRequestInfo())
  3171. {
  3172. const auto& [muid, propName] = *details;
  3173. const auto encodingToUse = [&, muidCopy = muid, propNameCopy = propName]() -> std::optional<ci::Encoding>
  3174. {
  3175. if (auto* deviceResource = findDeviceResource (appState->transient, muidCopy, propNameCopy))
  3176. return deviceResource->getBestCommonEncoding();
  3177. return {};
  3178. }();
  3179. if (! encodingToUse.has_value())
  3180. {
  3181. // We can't set property data because we don't have any encodings in common with the other device.
  3182. jassertfalse;
  3183. return;
  3184. }
  3185. ci::PropertyRequestHeader header;
  3186. header.resource = propName;
  3187. header.mutualEncoding = *encodingToUse;
  3188. header.setPartial = true;
  3189. device->sendPropertySetInquiry (muid, header, bytes, [] (const auto&)
  3190. {
  3191. // Could do error handling here, e.g. retry the request if the responder is busy
  3192. });
  3193. }
  3194. }
  3195. void getProperty()
  3196. {
  3197. if (! device.has_value())
  3198. return;
  3199. if (const auto details = getPropertyRequestInfo())
  3200. {
  3201. const auto& [muid, propName] = *details;
  3202. requestPropertyData (muid, propName);
  3203. }
  3204. }
  3205. void subscribeToProperty()
  3206. {
  3207. if (! device.has_value())
  3208. return;
  3209. auto* selectedDevice = appState->transient.devices.getSelected();
  3210. if (selectedDevice == nullptr)
  3211. return;
  3212. const auto details = getPropertyRequestInfo();
  3213. if (! details.has_value())
  3214. return;
  3215. const auto& [muid, propName] = *details;
  3216. // Find the subscription for this resource, if any
  3217. const auto existingToken = [&, propNameCopy = propName]() -> std::optional<ci::SubscriptionKey>
  3218. {
  3219. const auto ongoing = device->getOngoingSubscriptions();
  3220. for (const auto& o : ongoing)
  3221. if (propNameCopy == device->getResourceForKey (o))
  3222. return o;
  3223. return std::nullopt;
  3224. }();
  3225. // If we're already subscribed, end that subscription.
  3226. // Otherwise, begin a new subscription to this resource.
  3227. const auto changedToken = [this,
  3228. propNameCopy = propName,
  3229. muidCopy = muid,
  3230. existingTokenCopy = existingToken]() -> std::optional<ci::SubscriptionKey>
  3231. {
  3232. // We're not subscribed, so begin a new subscription
  3233. if (! existingTokenCopy.has_value())
  3234. {
  3235. ci::PropertySubscriptionHeader header;
  3236. header.resource = propNameCopy;
  3237. header.command = ci::PropertySubscriptionCommand::start;
  3238. return device->beginSubscription (muidCopy, header);
  3239. }
  3240. device->endSubscription (*existingTokenCopy);
  3241. return existingTokenCopy;
  3242. }();
  3243. if (changedToken.has_value())
  3244. deviceListener.propertySubscriptionChanged (*changedToken);
  3245. }
  3246. template <typename Transient>
  3247. static auto findDeviceResourceImpl (Transient& transient, ci::MUID device, String resource)
  3248. -> decltype (transient.devices.items.front().properties.properties.items.data())
  3249. {
  3250. auto& knownDevices = transient.devices.items;
  3251. const auto deviceIter = std::find_if (knownDevices.begin(),
  3252. knownDevices.end(),
  3253. [&] (const auto& d) { return d.muid == device; });
  3254. if (deviceIter == knownDevices.end())
  3255. return nullptr;
  3256. auto& props = deviceIter->properties.properties.items;
  3257. const auto propIter = std::find_if (props.begin(), props.end(), [&] (const auto& prop)
  3258. {
  3259. return prop.name == resource;
  3260. });
  3261. if (propIter == props.end())
  3262. return nullptr;
  3263. return &*propIter;
  3264. }
  3265. static Model::Property* findDeviceResource (Model::Transient& transient,
  3266. ci::MUID device,
  3267. String resource)
  3268. {
  3269. return findDeviceResourceImpl (transient, device, resource);
  3270. }
  3271. static const Model::Property* findDeviceResource (const Model::Transient& transient,
  3272. ci::MUID device,
  3273. String resource)
  3274. {
  3275. return findDeviceResourceImpl (transient, device, resource);
  3276. }
  3277. void requestPropertyData (ci::MUID target, String propertyName)
  3278. {
  3279. const auto encodingToUse = [&]() -> std::optional<ci::Encoding>
  3280. {
  3281. if (auto* deviceResource = findDeviceResource (appState->transient, target, propertyName))
  3282. return deviceResource->getBestCommonEncoding();
  3283. return {};
  3284. }();
  3285. if (! encodingToUse.has_value())
  3286. {
  3287. // We can't request property data because we don't have any encodings in common with the other device.
  3288. jassertfalse;
  3289. return;
  3290. }
  3291. ci::PropertyRequestHeader header;
  3292. header.resource = propertyName;
  3293. header.mutualEncoding = *encodingToUse;
  3294. device->sendPropertyGetInquiry (target, header, [this, target, propertyName] (const auto& response)
  3295. {
  3296. if (response.getError().has_value())
  3297. return;
  3298. auto updated = *appState;
  3299. if (auto* deviceResource = findDeviceResource (updated.transient, target, propertyName))
  3300. {
  3301. deviceResource->value.bytes = std::vector<std::byte> (response.getBody().begin(),
  3302. response.getBody().end());
  3303. appState = std::move (updated);
  3304. }
  3305. });
  3306. }
  3307. void setSavedState (var json)
  3308. {
  3309. if (auto newState = FromVar::convert<Model::Saved> (json))
  3310. appState[&Model::App::saved] = std::move (*newState);
  3311. if (auto* p = getPeer())
  3312. p->setHasChangedSinceSaved (false);
  3313. }
  3314. var getSavedState() const
  3315. {
  3316. if (auto json = ToVar::convert (appState->saved))
  3317. return *json;
  3318. return {};
  3319. }
  3320. ci::ProfileHost* getProfileHost()
  3321. {
  3322. if (device.has_value())
  3323. return device->getProfileHost();
  3324. return nullptr;
  3325. }
  3326. ci::PropertyHost* getPropertyHost()
  3327. {
  3328. if (device.has_value())
  3329. return device->getPropertyHost();
  3330. return nullptr;
  3331. }
  3332. [[nodiscard]] std::optional<MidiDeviceInfo> getInputInfo() const
  3333. {
  3334. if (input != nullptr)
  3335. return input->getDeviceInfo();
  3336. return std::nullopt;
  3337. }
  3338. [[nodiscard]] std::optional<MidiDeviceInfo> getOutputInfo() const
  3339. {
  3340. if (output != nullptr)
  3341. return output->getDeviceInfo();
  3342. return std::nullopt;
  3343. }
  3344. [[nodiscard]] Model::IOSelection getIOSelectionFromDevices() const
  3345. {
  3346. return { getInputInfo(), getOutputInfo() };
  3347. }
  3348. static bool isStillConnected (const MidiInput& x)
  3349. {
  3350. const auto devices = MidiInput::getAvailableDevices();
  3351. return std::any_of (devices.begin(),
  3352. devices.end(),
  3353. [info = x.getDeviceInfo()] (const auto& d) { return d == info; });
  3354. }
  3355. static bool isStillConnected (const MidiOutput& x)
  3356. {
  3357. const auto devices = MidiOutput::getAvailableDevices();
  3358. return std::any_of (devices.begin(),
  3359. devices.end(),
  3360. [info = x.getDeviceInfo()] (const auto& d) { return d == info; });
  3361. }
  3362. [[nodiscard]] static bool isStillConnected (const std::unique_ptr<MidiInput>& x)
  3363. {
  3364. if (x == nullptr)
  3365. return false;
  3366. return isStillConnected (*x);
  3367. }
  3368. [[nodiscard]] static bool isStillConnected (const std::unique_ptr<MidiOutput>& x)
  3369. {
  3370. if (x == nullptr)
  3371. return false;
  3372. return isStillConnected (*x);
  3373. }
  3374. void setDeviceProfileState (ci::ProfileAtAddress profileAtAddress,
  3375. ci::SupportedAndActive state)
  3376. {
  3377. if (auto* h = getProfileHost())
  3378. {
  3379. if (state.supported == 0)
  3380. h->removeProfile (profileAtAddress);
  3381. else
  3382. h->addProfile (profileAtAddress, state.supported);
  3383. h->setProfileEnablement (profileAtAddress, state.active);
  3384. }
  3385. }
  3386. void notifySubscribersForProperty (StringRef propertyName)
  3387. {
  3388. if (! device.has_value())
  3389. return;
  3390. const auto& subscribers = appState->transient.subscribers;
  3391. const auto iter = subscribers.find (propertyName);
  3392. if (iter == subscribers.end())
  3393. return;
  3394. for (const auto& [receiver, subscriptions] : iter->second)
  3395. {
  3396. for (const auto& subId : subscriptions)
  3397. {
  3398. if (auto* host = getPropertyHost())
  3399. {
  3400. ci::PropertySubscriptionHeader header;
  3401. header.command = ci::PropertySubscriptionCommand::notify;
  3402. header.subscribeId = subId;
  3403. header.resource = propertyName;
  3404. host->sendSubscriptionUpdate (receiver, header, {}, {});
  3405. }
  3406. }
  3407. }
  3408. }
  3409. void addLogEntry (ump::BytesOnGroup entry,
  3410. Model::MessageKind kind,
  3411. Time time = Time::getCurrentTime())
  3412. {
  3413. static constexpr size_t maxNum = 1000;
  3414. auto entries = appState[&Model::App::transient][&Model::Transient::logEntries];
  3415. auto updated = *entries;
  3416. updated.emplace (updated.end(), Model::LogEntry { { entry.bytes.begin(),
  3417. entry.bytes.end() },
  3418. entry.group,
  3419. time,
  3420. kind });
  3421. while (updated.size() > maxNum)
  3422. updated.pop_front();
  3423. entries = std::move (updated);
  3424. }
  3425. static Model::App normalise (const Model::App& older, Model::App&& newer)
  3426. {
  3427. auto modified = std::move (newer);
  3428. if (older.saved.fundamentals != modified.saved.fundamentals)
  3429. {
  3430. modified.transient.devices.items.clear();
  3431. modified.transient.devices.selection = -1;
  3432. modified.transient.subscribers.clear();
  3433. }
  3434. modified.syncSubscribers();
  3435. return modified;
  3436. }
  3437. class DeviceListener : public ci::DeviceListener
  3438. {
  3439. public:
  3440. explicit DeviceListener (CapabilityInquiryDemo& d) : demo (d) {}
  3441. void deviceAdded (ci::MUID added) override
  3442. {
  3443. auto updated = *demo.appState;
  3444. auto& devices = updated.transient.devices;
  3445. auto& deviceVec = devices.items;
  3446. const auto iterForMuid = [&] (auto m)
  3447. {
  3448. return std::find_if (deviceVec.begin(),
  3449. deviceVec.end(),
  3450. [&] (const auto& d) { return d.muid == m; });
  3451. };
  3452. const auto iter = iterForMuid (added);
  3453. auto& toUpdate = iter != deviceVec.end() ? *iter : deviceVec.emplace_back();
  3454. toUpdate.muid = added;
  3455. if (devices.getSelected() == nullptr)
  3456. devices.selection = (int) std::distance (deviceVec.begin(), iterForMuid (added));
  3457. const auto response = demo.device.has_value() ? demo.device->getDiscoveryInfoForMuid (added)
  3458. : std::nullopt;
  3459. if (! response.has_value())
  3460. return;
  3461. const ci::DeviceFeatures features { response->capabilities };
  3462. toUpdate.info.profilesSupported = features.isProfileConfigurationSupported();
  3463. toUpdate.info.propertiesSupported = features.isPropertyExchangeSupported();
  3464. toUpdate.info.deviceInfo = response->device;
  3465. toUpdate.info.maxSysExSize = response->maximumSysexSize;
  3466. toUpdate.info.numPropertyExchangeTransactions = 0;
  3467. demo.appState = updated;
  3468. if (! demo.device.has_value())
  3469. return;
  3470. if (features.isProfileConfigurationSupported())
  3471. demo.device->sendProfileInquiry (added, ci::ChannelInGroup::wholeBlock);
  3472. if (features.isPropertyExchangeSupported())
  3473. demo.device->sendPropertyCapabilitiesInquiry (added);
  3474. }
  3475. void deviceRemoved (ci::MUID gone) override
  3476. {
  3477. auto updated = *demo.appState;
  3478. auto& subs = updated.transient.subscribers;
  3479. for (auto& u : subs)
  3480. u.second.erase (gone);
  3481. auto& devices = updated.transient.devices;
  3482. const auto selectedMuid = [&]() -> std::optional<ci::MUID>
  3483. {
  3484. if (auto* item = devices.getSelected())
  3485. return item->muid;
  3486. return {};
  3487. }();
  3488. devices.items.erase (std::remove_if (devices.items.begin(),
  3489. devices.items.end(),
  3490. [&] (const auto& d) { return d.muid == gone; }),
  3491. devices.items.end());
  3492. const auto iter = std::find_if (devices.items.begin(),
  3493. devices.items.end(),
  3494. [&] (const auto& d) { return d.muid == selectedMuid; });
  3495. devices.selection = iter != devices.items.end()
  3496. ? (int) std::distance (devices.items.begin(), iter)
  3497. : -1;
  3498. demo.appState = updated;
  3499. }
  3500. void endpointReceived (ci::MUID, ci::Message::EndpointInquiryResponse) override
  3501. {
  3502. // No special handling
  3503. }
  3504. void messageNotAcknowledged (ci::MUID, ci::Message::NAK) override
  3505. {
  3506. // No special handling
  3507. }
  3508. void profileStateReceived (ci::MUID muid,
  3509. ci::ChannelInGroup) override
  3510. {
  3511. updateProfilesForMuid (muid);
  3512. }
  3513. void profilePresenceChanged (ci::MUID muid,
  3514. ci::ChannelInGroup,
  3515. ci::Profile,
  3516. bool) override
  3517. {
  3518. updateProfilesForMuid (muid);
  3519. }
  3520. void profileEnablementChanged (ci::MUID muid,
  3521. ci::ChannelInGroup,
  3522. ci::Profile,
  3523. int) override
  3524. {
  3525. updateProfilesForMuid (muid);
  3526. }
  3527. void profileDetailsReceived (ci::MUID,
  3528. ci::ChannelInGroup,
  3529. ci::Profile,
  3530. std::byte,
  3531. Span<const std::byte>) override
  3532. {
  3533. // No special handling
  3534. }
  3535. void profileSpecificDataReceived (ci::MUID,
  3536. ci::ChannelInGroup,
  3537. ci::Profile,
  3538. Span<const std::byte>) override
  3539. {
  3540. // No special handling
  3541. }
  3542. void propertyExchangeCapabilitiesReceived (ci::MUID muid) override
  3543. {
  3544. auto updated = *demo.appState;
  3545. auto& devices = updated.transient.devices;
  3546. const auto iter = std::find_if (devices.items.begin(),
  3547. devices.items.end(),
  3548. [&] (const auto& d) { return d.muid == muid; });
  3549. if (iter == devices.items.end() || ! demo.device.has_value())
  3550. return;
  3551. const auto transactions = demo.device->getNumPropertyExchangeRequestsSupportedForMuid (muid);
  3552. if (! transactions.has_value())
  3553. return;
  3554. iter->info.numPropertyExchangeTransactions = (uint8_t) *transactions;
  3555. if (const auto resourceList = demo.device->getResourceListForMuid (muid); resourceList != var{})
  3556. {
  3557. if (auto* list = resourceList.getArray())
  3558. {
  3559. auto& items = updated.transient.devices.items;
  3560. const auto found = std::find_if (items.begin(),
  3561. items.end(),
  3562. [&] (const auto& dev) { return dev.muid == muid; });
  3563. if (found != updated.transient.devices.items.end())
  3564. {
  3565. found->properties.properties = {};
  3566. auto& propItems = found->properties.properties.items;
  3567. Model::Property resourceListProp;
  3568. resourceListProp.name = "ResourceList";
  3569. resourceListProp.canSet = Model::CanSet::none;
  3570. resourceListProp.value.bytes = ci::Encodings::jsonTo7BitText (resourceList);
  3571. propItems.push_back (resourceListProp);
  3572. for (auto& entry : *list)
  3573. propItems.push_back (Model::Property::fromResourceListEntry (entry));
  3574. }
  3575. }
  3576. }
  3577. if (const auto deviceInfo = demo.device->getDeviceInfoForMuid (muid); deviceInfo != var{})
  3578. {
  3579. if (auto* deviceResource = findDeviceResource (updated.transient, muid, "DeviceInfo"))
  3580. {
  3581. deviceResource->value.bytes = ci::Encodings::jsonTo7BitText (deviceInfo);
  3582. }
  3583. }
  3584. demo.appState = std::move (updated);
  3585. }
  3586. void propertySubscriptionDataReceived (ci::MUID muid,
  3587. const ci::PropertySubscriptionData& subscription) override
  3588. {
  3589. const auto resource = [&]
  3590. {
  3591. const auto ongoing = demo.device->getOngoingSubscriptions();
  3592. for (const auto& o : ongoing)
  3593. {
  3594. if (subscription.header.subscribeId == demo.device->getSubscribeIdForKey (o))
  3595. return demo.device->getResourceForKey (o).value_or (String{});
  3596. }
  3597. return String{};
  3598. }();
  3599. if (resource.isEmpty())
  3600. {
  3601. // Got a subscription message for a subscription that's no longer ongoing
  3602. jassertfalse;
  3603. return;
  3604. }
  3605. auto devicesState = demo.appState[&Model::App::transient]
  3606. [&Model::Transient::devices]
  3607. [&Model::ListWithSelection<Model::Device>::items];
  3608. auto copiedDevices = *devicesState;
  3609. const auto matchingDevice = [&]
  3610. {
  3611. const auto iter = std::find_if (copiedDevices.begin(),
  3612. copiedDevices.end(),
  3613. [&] (const auto& d) { return d.muid == muid; });
  3614. return iter != copiedDevices.end() ? &*iter : nullptr;
  3615. }();
  3616. if (matchingDevice == nullptr)
  3617. {
  3618. // Got a subscription message for a device that we haven't recorded
  3619. jassertfalse;
  3620. return;
  3621. }
  3622. auto& propertyList = matchingDevice->properties.properties.items;
  3623. const auto matchingProperty = [&]
  3624. {
  3625. const auto iter = std::find_if (propertyList.begin(),
  3626. propertyList.end(),
  3627. [&] (const auto& p) { return p.name == resource; });
  3628. return iter != propertyList.end() ? &*iter : nullptr;
  3629. }();
  3630. if (matchingProperty == nullptr)
  3631. {
  3632. // Got a subscription message for a property that we haven't recorded
  3633. jassertfalse;
  3634. return;
  3635. }
  3636. switch (subscription.header.command)
  3637. {
  3638. case ci::PropertySubscriptionCommand::partial:
  3639. {
  3640. auto [updated, error] = Utils::attemptSetPartial (std::move (*matchingProperty),
  3641. subscription);
  3642. *matchingProperty = std::move (updated);
  3643. jassert (error.isEmpty()); // Inspect 'error' to see what went wrong
  3644. break;
  3645. }
  3646. case ci::PropertySubscriptionCommand::full:
  3647. {
  3648. matchingProperty->value.bytes = std::vector<std::byte> (subscription.body.begin(),
  3649. subscription.body.end());
  3650. matchingProperty->value.mediaType = subscription.header.mediaType;
  3651. break;
  3652. }
  3653. case ci::PropertySubscriptionCommand::notify:
  3654. {
  3655. demo.requestPropertyData (muid, resource);
  3656. break;
  3657. }
  3658. case ci::PropertySubscriptionCommand::end:
  3659. break;
  3660. case ci::PropertySubscriptionCommand::start:
  3661. jassertfalse;
  3662. return;
  3663. }
  3664. devicesState = std::move (copiedDevices);
  3665. }
  3666. void propertySubscriptionChanged (ci::SubscriptionKey key, const std::optional<String>&) override
  3667. {
  3668. propertySubscriptionChanged (key);
  3669. }
  3670. void propertySubscriptionChanged (ci::SubscriptionKey key)
  3671. {
  3672. auto updated = *demo.appState;
  3673. auto& knownDevices = updated.transient.devices.items;
  3674. const auto deviceIter = std::find_if (knownDevices.begin(),
  3675. knownDevices.end(),
  3676. [target = key.getMuid()] (const auto& d) { return d.muid == target; });
  3677. if (deviceIter == knownDevices.end())
  3678. {
  3679. // The device has gone away?
  3680. jassertfalse;
  3681. return;
  3682. }
  3683. if (const auto resource = demo.device->getResourceForKey (key))
  3684. deviceIter->subscriptions.emplace (key, ci::Subscription { demo.device->getSubscribeIdForKey (key).value_or (String{}), *resource });
  3685. else
  3686. deviceIter->subscriptions.erase (key);
  3687. demo.appState = std::move (updated);
  3688. }
  3689. private:
  3690. void updateProfilesForMuid (ci::MUID muid)
  3691. {
  3692. if (! demo.device.has_value())
  3693. return;
  3694. auto innerState = demo.appState[&Model::App::transient][&Model::Transient::devices];
  3695. auto updated = *innerState;
  3696. const auto iter = std::find_if (updated.items.begin(),
  3697. updated.items.end(),
  3698. [&] (const auto& d) { return d.muid == muid; });
  3699. if (iter == updated.items.end())
  3700. return;
  3701. auto& profiles = iter->profiles;
  3702. const auto lastSelectedProfile = [&]() -> std::optional<ci::Profile>
  3703. {
  3704. if (auto* ptr = profiles.profiles.getSelected())
  3705. return *ptr;
  3706. return {};
  3707. }();
  3708. profiles.profileMode = Model::ProfileMode::use;
  3709. profiles.profiles.items = [&]
  3710. {
  3711. std::set<ci::Profile> uniqueProfiles;
  3712. Utils::forAllChannelAddresses ([&] (auto address)
  3713. {
  3714. if (auto* state = demo.device->getProfileStateForMuid (muid, address))
  3715. for (const auto& p : *state)
  3716. uniqueProfiles.insert (p.profile);
  3717. });
  3718. return std::vector<ci::Profile> (uniqueProfiles.begin(), uniqueProfiles.end());
  3719. }();
  3720. profiles.profiles.selection = [&]
  3721. {
  3722. if (! lastSelectedProfile.has_value())
  3723. return -1;
  3724. const auto foundMuid = std::find (profiles.profiles.items.begin(),
  3725. profiles.profiles.items.end(),
  3726. *lastSelectedProfile);
  3727. if (foundMuid == profiles.profiles.items.end())
  3728. return -1;
  3729. return (int) std::distance (profiles.profiles.items.begin(), foundMuid);
  3730. }();
  3731. profiles.channels = [&]
  3732. {
  3733. std::map<ci::ProfileAtAddress, ci::SupportedAndActive> result;
  3734. Utils::forAllChannelAddresses ([&] (auto address)
  3735. {
  3736. if (auto* state = demo.device->getProfileStateForMuid (muid, address))
  3737. for (const auto& p : *state)
  3738. result[{ p.profile, address }] = p.state;
  3739. });
  3740. return result;
  3741. }();
  3742. profiles.selectedChannel = [&]() -> std::optional<ci::ChannelAddress>
  3743. {
  3744. if (profiles.profileMode == Model::ProfileMode::edit)
  3745. return profiles.selectedChannel;
  3746. const auto profileAtAddress = profiles.getSelectedProfileAtAddress();
  3747. if (! profileAtAddress.has_value())
  3748. return std::nullopt;
  3749. const auto found = profiles.channels.find (*profileAtAddress);
  3750. if (found == profiles.channels.end() || found->second.supported == 0)
  3751. return std::nullopt;
  3752. return profiles.selectedChannel;
  3753. }();
  3754. innerState = std::move (updated);
  3755. }
  3756. CapabilityInquiryDemo& demo;
  3757. };
  3758. class InputHandler : public ci::DeviceMessageHandler
  3759. {
  3760. public:
  3761. explicit InputHandler (CapabilityInquiryDemo& d) : demo (d) {}
  3762. void processMessage (ump::BytesOnGroup msg) override
  3763. {
  3764. demo.addLogEntry (msg, Model::MessageKind::incoming);
  3765. if (demo.device.has_value())
  3766. demo.device->processMessage (msg);
  3767. }
  3768. private:
  3769. CapabilityInquiryDemo& demo;
  3770. };
  3771. class OutputHandler : public ci::DeviceMessageHandler
  3772. {
  3773. public:
  3774. explicit OutputHandler (CapabilityInquiryDemo& d) : demo (d) {}
  3775. void processMessage (ump::BytesOnGroup msg) override
  3776. {
  3777. SafePointer weak { &demo };
  3778. std::vector<std::byte> bytes (msg.bytes.begin(), msg.bytes.end());
  3779. auto group = msg.group;
  3780. auto time = Time::getCurrentTime();
  3781. MessageManager::callAsync ([weak, movedBytes = std::move (bytes), group, time]
  3782. {
  3783. // This call is async because we may send messages in direct response to model updates.
  3784. if (weak != nullptr)
  3785. weak->addLogEntry ({ group, movedBytes }, Model::MessageKind::outgoing, time);
  3786. });
  3787. if (auto* out = demo.output.get())
  3788. out->sendMessageNow (MidiMessage::createSysExMessage (msg.bytes));
  3789. }
  3790. private:
  3791. CapabilityInquiryDemo& demo;
  3792. };
  3793. class ProfileDelegate : public ci::ProfileDelegate
  3794. {
  3795. public:
  3796. explicit ProfileDelegate (CapabilityInquiryDemo& d) : demo (d) {}
  3797. void profileEnablementRequested (ci::MUID,
  3798. ci::ProfileAtAddress profileAtAddress,
  3799. int numChannels,
  3800. bool enabled) override
  3801. {
  3802. auto state = demo.appState[&Model::App::saved][&Model::Saved::profiles];
  3803. auto profiles = *state;
  3804. if (auto* host = demo.getProfileHost())
  3805. {
  3806. const auto count = enabled ? jmax (1, numChannels) : 0;
  3807. host->setProfileEnablement (profileAtAddress, count);
  3808. profiles.channels[profileAtAddress].active = (uint16_t) count;
  3809. state = profiles;
  3810. }
  3811. }
  3812. private:
  3813. CapabilityInquiryDemo& demo;
  3814. };
  3815. class PropertyDelegate : public ci::PropertyDelegate
  3816. {
  3817. public:
  3818. explicit PropertyDelegate (CapabilityInquiryDemo& d) : demo (d) {}
  3819. uint8_t getNumSimultaneousRequestsSupported() const override
  3820. {
  3821. return demo.appState->saved.fundamentals.numPropertyExchangeTransactions;
  3822. }
  3823. ci::PropertyReplyData propertyGetDataRequested (ci::MUID,
  3824. const ci::PropertyRequestHeader& header) override
  3825. {
  3826. auto allProperties = demo.appState->saved.properties.properties.items;
  3827. allProperties.insert (allProperties.begin(), getDeviceInfo());
  3828. if (header.resource == "ResourceList")
  3829. return generateResourceListReply (allProperties);
  3830. for (const auto& prop : allProperties)
  3831. if (auto reply = generateReply (header, prop))
  3832. return *reply;
  3833. ci::PropertyReplyData result;
  3834. result.header.status = 404;
  3835. result.header.message = "Unable to locate resource " + header.resource;
  3836. return result;
  3837. }
  3838. ci::PropertyReplyHeader propertySetDataRequested (ci::MUID,
  3839. const ci::PropertyRequestData& request) override
  3840. {
  3841. const auto makeErrorHeader = [] (auto str, int code = 400)
  3842. {
  3843. ci::PropertyReplyHeader header;
  3844. header.status = code;
  3845. header.message = str;
  3846. return header;
  3847. };
  3848. if (request.header.resource == "ResourceList")
  3849. return makeErrorHeader ("Unable to set ResourceList");
  3850. auto state = demo.appState[&Model::App::saved]
  3851. [&Model::Saved::properties]
  3852. [&Model::Properties::properties];
  3853. auto props = *state;
  3854. for (auto& prop : props.items)
  3855. {
  3856. if (request.header.resource != prop.name)
  3857. continue;
  3858. if (prop.canSet == Model::CanSet::none)
  3859. return makeErrorHeader ("Unable to set resource " + prop.name);
  3860. if (request.header.setPartial)
  3861. {
  3862. if (prop.canSet != Model::CanSet::partial)
  3863. return makeErrorHeader ("Resource " + prop.name + " does not support setPartial");
  3864. auto [updatedProp, error] = Utils::attemptSetPartial (std::move (prop), request);
  3865. prop = std::move (updatedProp);
  3866. if (error.isNotEmpty())
  3867. return makeErrorHeader (error);
  3868. state = props;
  3869. return {};
  3870. }
  3871. prop.value.bytes = std::vector<std::byte> (request.body.begin(), request.body.end());
  3872. prop.value.mediaType = request.header.mediaType;
  3873. state = props;
  3874. return {};
  3875. }
  3876. return makeErrorHeader ("Unable to locate resource " + request.header.resource, 404);
  3877. }
  3878. bool subscriptionStartRequested (ci::MUID, const ci::PropertySubscriptionHeader& header) override
  3879. {
  3880. const auto props = demo.appState->saved.properties.properties.items;
  3881. return std::any_of (props.begin(), props.end(), [&] (const auto& p)
  3882. {
  3883. return p.name == header.resource;
  3884. });
  3885. }
  3886. void subscriptionDidStart (ci::MUID initiator,
  3887. const String& token,
  3888. const ci::PropertySubscriptionHeader& header) override
  3889. {
  3890. auto transient = demo.appState[&Model::App::transient];
  3891. auto updated = *transient;
  3892. updated.subscribers[header.resource][initiator].insert (token);
  3893. transient = updated;
  3894. }
  3895. void subscriptionWillEnd (ci::MUID initiator, const ci::Subscription& subscription) override
  3896. {
  3897. auto transient = demo.appState[&Model::App::transient];
  3898. auto updated = *transient;
  3899. updated.subscribers[subscription.resource][initiator].erase (subscription.subscribeId);
  3900. transient = updated;
  3901. }
  3902. private:
  3903. Model::Property getDeviceInfo() const
  3904. {
  3905. Model::Property result;
  3906. result.name = "DeviceInfo";
  3907. auto obj = std::make_unique<DynamicObject>();
  3908. const auto varFromByteArray = [] (const auto& r)
  3909. {
  3910. return Model::toVarArray (r, [] (auto b) { return (int) b; });
  3911. };
  3912. obj->setProperty ("manufacturerId",
  3913. varFromByteArray (demo.device->getOptions().getDeviceInfo().manufacturer));
  3914. obj->setProperty ("manufacturer",
  3915. "JUCE");
  3916. obj->setProperty ("familyId",
  3917. varFromByteArray (demo.device->getOptions().getDeviceInfo().family));
  3918. obj->setProperty ("family",
  3919. "MIDI Software");
  3920. obj->setProperty ("modelId",
  3921. varFromByteArray (demo.device->getOptions().getDeviceInfo().modelNumber));
  3922. obj->setProperty ("model",
  3923. "Capability Inquiry Demo");
  3924. obj->setProperty ("versionId",
  3925. varFromByteArray (demo.device->getOptions().getDeviceInfo().revision));
  3926. obj->setProperty ("version",
  3927. ProjectInfo::versionString);
  3928. result.value.bytes = ci::Encodings::jsonTo7BitText (obj.release());
  3929. return result;
  3930. }
  3931. ci::PropertyReplyData generateResourceListReply (Span<const Model::Property> allProperties) const
  3932. {
  3933. Array<var> resourceList;
  3934. for (const auto& prop : allProperties)
  3935. resourceList.add (prop.getResourceListEntry());
  3936. return ci::PropertyReplyData { {},
  3937. ci::Encodings::jsonTo7BitText (std::move (resourceList)) };
  3938. }
  3939. std::optional<ci::PropertyReplyData> generateReply (const ci::PropertyRequestHeader& header,
  3940. const Model::Property& info) const
  3941. {
  3942. if (header.resource != info.name)
  3943. return {};
  3944. const auto encodingToUse = [&]() -> std::optional<ci::Encoding>
  3945. {
  3946. if (info.encodings.count (header.mutualEncoding) != 0)
  3947. return header.mutualEncoding;
  3948. if (! info.encodings.empty())
  3949. return *info.encodings.begin();
  3950. return {};
  3951. }();
  3952. if (! encodingToUse.has_value())
  3953. {
  3954. // If this is hit, we don't declare any supported encodings for this property!
  3955. jassertfalse;
  3956. return {};
  3957. }
  3958. const auto basicReplyHeader = [&]
  3959. {
  3960. ci::PropertyReplyHeader h;
  3961. h.mutualEncoding = *encodingToUse;
  3962. h.mediaType = info.value.mediaType;
  3963. return h;
  3964. }();
  3965. const auto [replyHeader, unencoded] = [&]
  3966. {
  3967. if (info.name.endsWith ("List")
  3968. && info.canPaginate
  3969. && info.value.mediaType == "application/json")
  3970. {
  3971. const auto jsonToSend = ci::Encodings::jsonFrom7BitText (info.value.bytes);
  3972. if (const auto* array = jsonToSend.getArray())
  3973. {
  3974. const auto updatedHeader = [&]
  3975. {
  3976. auto h = basicReplyHeader;
  3977. h.extended["totalCount"] = array->size();
  3978. return h;
  3979. }();
  3980. if (const auto pagination = header.pagination)
  3981. {
  3982. const auto realOffset = jlimit (0, array->size(), pagination->offset);
  3983. const auto realLimit = jlimit (0,
  3984. array->size() - realOffset,
  3985. pagination->limit);
  3986. const auto* data = array->data() + realOffset;
  3987. const Array<var> slice (data, realLimit);
  3988. return std::tuple (updatedHeader, ci::Encodings::jsonTo7BitText (slice));
  3989. }
  3990. return std::tuple (updatedHeader, info.value.bytes);
  3991. }
  3992. }
  3993. return std::tuple (basicReplyHeader, info.value.bytes);
  3994. }();
  3995. return ci::PropertyReplyData { replyHeader, unencoded };
  3996. }
  3997. CapabilityInquiryDemo& demo;
  3998. };
  3999. ApplicationProperties applicationProperties;
  4000. State<ci::MUID> ourMuid { ci::MUID::makeUnchecked (0) };
  4001. State<Model::App> appState { Model::App{}, normalise };
  4002. IOPickerLists lists { appState[&Model::App::saved][&Model::Saved::ioSelection] };
  4003. LocalConfigurationPanel local { ourMuid, appState };
  4004. DiscoveryPanel discovery { appState };
  4005. LoggingPanel logging { appState };
  4006. TabbedComponent tabs { TabbedButtonBar::Orientation::TabsAtTop };
  4007. TextButton loadButton { "Load State..." }, saveButton { "Save State..."};
  4008. MessageForwarder forwarder;
  4009. std::unique_ptr<MidiInput> input;
  4010. std::unique_ptr<MidiOutput> output;
  4011. std::optional<ci::Device> device;
  4012. FileChooser fileChooser { "Pick State JSON File", {}, "*.json", true, false, this };
  4013. InputHandler inputHandler { *this };
  4014. OutputHandler outputHandler { *this };
  4015. DeviceListener deviceListener { *this };
  4016. ProfileDelegate profileDelegate { *this };
  4017. PropertyDelegate propertyDelegate { *this };
  4018. std::vector<ErasedScopeGuard> listeners = Utils::makeVector
  4019. (
  4020. appState[&Model::App::saved][&Model::Saved::fundamentals].observe ([this] (auto)
  4021. {
  4022. Random random;
  4023. random.setSeedRandomly();
  4024. const auto fullState = *appState;
  4025. const auto info = fullState.saved.fundamentals;
  4026. const auto features = ci::DeviceFeatures()
  4027. .withProfileConfigurationSupported (info.profilesSupported)
  4028. .withPropertyExchangeSupported (info.propertiesSupported);
  4029. const auto options = ci::DeviceOptions()
  4030. .withOutputs ({ &outputHandler })
  4031. .withDeviceInfo (info.deviceInfo)
  4032. .withFeatures (features)
  4033. .withMaxSysExSize (info.maxSysExSize)
  4034. .withProductInstanceId (ci::DeviceOptions::makeProductInstanceId (random))
  4035. .withPropertyDelegate (&propertyDelegate)
  4036. .withProfileDelegate (&profileDelegate);
  4037. device.emplace (options);
  4038. device->addListener (deviceListener);
  4039. for (const auto& [addressAndProfile, channels] : fullState.saved.profiles.channels)
  4040. setDeviceProfileState (addressAndProfile, channels);
  4041. ourMuid = device->getMuid();
  4042. }),
  4043. appState[&Model::App::saved]
  4044. [&Model::Saved::profiles]
  4045. [&Model::Profiles::channels].observe ([this] (auto)
  4046. {
  4047. if (auto* host = getProfileHost())
  4048. {
  4049. std::map<ci::ProfileAtAddress, ci::SupportedAndActive> deviceState;
  4050. Utils::forAllChannelAddresses ([&] (auto address)
  4051. {
  4052. if (auto* state = host->getProfileStates().getStateForDestination (address))
  4053. for (const auto& p : *state)
  4054. deviceState[{ p.profile, address }] = p.state;
  4055. });
  4056. const auto requestedState = appState->saved.profiles.channels;
  4057. std::vector<std::pair<ci::ProfileAtAddress, ci::SupportedAndActive>> removed;
  4058. const auto compare = [] (const auto& a, const auto& b)
  4059. {
  4060. return a.first < b.first;
  4061. };
  4062. std::set_difference (deviceState.begin(),
  4063. deviceState.end(),
  4064. requestedState.begin(),
  4065. requestedState.end(),
  4066. std::back_inserter (removed),
  4067. compare);
  4068. for (const auto& p : removed)
  4069. host->removeProfile (p.first);
  4070. for (const auto& [addressAndProfile, channels] : requestedState)
  4071. setDeviceProfileState (addressAndProfile, channels);
  4072. }
  4073. }),
  4074. appState[&Model::App::saved]
  4075. [&Model::Saved::ioSelection]
  4076. [&Model::IOSelection::input].observe ([this] (auto)
  4077. {
  4078. if (input != nullptr)
  4079. input->stop();
  4080. input.reset();
  4081. if (const auto selection = appState->saved.ioSelection.input)
  4082. {
  4083. input = MidiInput::openDevice (selection->identifier, &forwarder);
  4084. if (input != nullptr)
  4085. input->start();
  4086. }
  4087. }),
  4088. appState[&Model::App::saved]
  4089. [&Model::Saved::ioSelection]
  4090. [&Model::IOSelection::output].observe ([this] (auto)
  4091. {
  4092. output.reset();
  4093. if (const auto selection = appState->saved.ioSelection.output)
  4094. output = MidiOutput::openDevice (selection->identifier);
  4095. }),
  4096. appState[&Model::App::saved].observe ([this] (auto)
  4097. {
  4098. if (auto* p = getPeer())
  4099. p->setHasChangedSinceSaved (true);
  4100. }),
  4101. appState[&Model::App::transient]
  4102. [&Model::Transient::subscribers].observe ([this] (const auto& oldSubscribers)
  4103. {
  4104. // Send a subscription end message for any subscribed properties that have been
  4105. // completely removed
  4106. const auto newSubscribers = appState->transient.subscribers;
  4107. if (oldSubscribers == newSubscribers || ! device.has_value())
  4108. return;
  4109. std::set<String> removed;
  4110. for (const auto& p : oldSubscribers)
  4111. if (newSubscribers.count (p.first) == 0)
  4112. removed.insert (p.first);
  4113. if (auto* host = getPropertyHost())
  4114. for (const auto& r : removed)
  4115. for (const auto& [dest, subIds] : oldSubscribers.find (r)->second)
  4116. for (const auto& id : subIds)
  4117. host->terminateSubscription (dest, id);
  4118. }),
  4119. appState[&Model::App::saved]
  4120. [&Model::Saved::properties]
  4121. [&Model::Properties::properties].observe ([this] (const auto& oldProperties)
  4122. {
  4123. const auto makePropertyMap = [] (const auto& props)
  4124. {
  4125. std::map<String, Model::PropertyValue> result;
  4126. for (const auto& p : props.items)
  4127. result.emplace (p.name, p.value);
  4128. return result;
  4129. };
  4130. const auto& newProperties = appState->saved.properties.properties;
  4131. const auto oldMap = makePropertyMap (oldProperties);
  4132. const auto newMap = makePropertyMap (newProperties);
  4133. for (const auto& [name, value] : newMap)
  4134. {
  4135. const auto iter = oldMap.find (name);
  4136. if (iter != oldMap.end() && iter->second != value)
  4137. notifySubscribersForProperty (name);
  4138. }
  4139. })
  4140. );
  4141. MidiDeviceListConnection connection = MidiDeviceListConnection::make ([this]
  4142. {
  4143. if (! isStillConnected (input))
  4144. {
  4145. if (input != nullptr)
  4146. input->stop();
  4147. input.reset();
  4148. }
  4149. if (! isStillConnected (output))
  4150. output.reset();
  4151. appState[&Model::App::saved][&Model::Saved::ioSelection] = getIOSelectionFromDevices();
  4152. });
  4153. };