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.

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