From 2dd21f5baa8540d7eb1742f1d2884363a3958d79 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 24 Jan 2023 17:56:54 -0500 Subject: [PATCH] Accessibility Menu; Patch to Text (#6819) A couple of accessibility features 1. Add an "Accessibility" menu 2. Add a "set to recommended accessibility settings" item to it that sets the four settings which folks recommend. 3. Add a very early draft of patch->text for sharing a description of the patch for feedback on if it is is even useful to consider developing Addresses #6811 --- src/common/SurgeStorage.h | 3 + src/surge-xt/gui/SurgeGUIEditor.cpp | 110 +++++++--- src/surge-xt/gui/SurgeGUIEditor.h | 5 + .../gui/SurgeGUIEditorHtmlGenerators.cpp | 192 ++++++++++++++++++ 4 files changed, 279 insertions(+), 31 deletions(-) diff --git a/src/common/SurgeStorage.h b/src/common/SurgeStorage.h index 6b96e2be98a..08528634808 100644 --- a/src/common/SurgeStorage.h +++ b/src/common/SurgeStorage.h @@ -620,6 +620,9 @@ struct FxStorage struct SurgeSceneStorage { OscillatorStorage osc[n_oscs]; + + // If you change the start and end points of this param list the iteration in the + // dump to HTML will break. Parameter pitch, octave; Parameter fm_depth, fm_switch; Parameter drift, noise_colour, keytrack_root; diff --git a/src/surge-xt/gui/SurgeGUIEditor.cpp b/src/surge-xt/gui/SurgeGUIEditor.cpp index fc97a64673f..6ec428d1f77 100644 --- a/src/surge-xt/gui/SurgeGUIEditor.cpp +++ b/src/surge-xt/gui/SurgeGUIEditor.cpp @@ -2928,11 +2928,14 @@ void SurgeGUIEditor::showSettingsMenu(const juce::Point &where, settingsMenu.addSubMenu(Surge::GUI::toOSCase("Mouse Behavior"), mouseMenu); auto patchDefMenu = makePatchDefaultsMenu(where); - settingsMenu.addSubMenu(Surge::GUI::toOSCase("Patch Defaults"), patchDefMenu); + settingsMenu.addSubMenu(Surge::GUI::toOSCase("Patch Settings"), patchDefMenu); auto wfMenu = makeWorkflowMenu(where); settingsMenu.addSubMenu(Surge::GUI::toOSCase("Workflow"), wfMenu); + auto accMenu = makeAccesibilityMenu(where); + settingsMenu.addSubMenu(Surge::GUI::toOSCase("Accessibility"), accMenu); + settingsMenu.addSeparator(); auto mpeSubMenu = makeMpeMenu(where, false); @@ -3966,6 +3969,12 @@ juce::PopupMenu SurgeGUIEditor::makePatchDefaultsMenu(const juce::Point &wh patchDefMenu.addSubMenu(Surge::GUI::toOSCase("Tuning on Patch Load"), tuningOnLoadMenu); + patchDefMenu.addSeparator(); + patchDefMenu.addItem(Surge::GUI::toOSCase("Export Patch as Text (Non-Default Values)"), true, + false, [this]() { showHTML(patchToHtml()); }); + patchDefMenu.addItem(Surge::GUI::toOSCase("Export Patch as Text (All Values)"), true, false, + [this]() { showHTML(patchToHtml(true)); }); + return patchDefMenu; } @@ -4160,21 +4169,44 @@ juce::PopupMenu SurgeGUIEditor::makeWorkflowMenu(const juce::Point &where) wfMenu.addSeparator(); + bool showVirtualKeyboard = getShowVirtualKeyboard(); + + Surge::GUI::addMenuWithShortcut(wfMenu, Surge::GUI::toOSCase("Show Virtual Keyboard"), + showShortcutDescription("Alt + K", u8"\U00002325K"), true, + showVirtualKeyboard, [this]() { toggleVirtualKeyboard(); }); + + bool showOscilloscope = isAnyOverlayPresent(OSCILLOSCOPE); + + Surge::GUI::addMenuWithShortcut(wfMenu, Surge::GUI::toOSCase("Open Oscilloscope"), + showShortcutDescription("Alt + O", u8"\U00002325O"), true, + showOscilloscope, [this]() { toggleOverlay(OSCILLOSCOPE); }); + + return wfMenu; +} + +juce::PopupMenu SurgeGUIEditor::makeAccesibilityMenu(const juce::Point &rect) +{ + auto accMenu = juce::PopupMenu(); + + accMenu.addItem(Surge::GUI::toOSCase("Set All Recommended Accessibility Options"), true, false, + [this]() { setRecommendedAccessibility(); }); + accMenu.addSeparator(); + bool doAccAnn = Surge::Storage::getUserDefaultValue( &(this->synth->storage), Surge::Storage::UseNarratorAnnouncements, false); - wfMenu.addItem(Surge::GUI::toOSCase("Send Additional Accessibility Announcements"), true, - doAccAnn, [this, doAccAnn]() { - Surge::Storage::updateUserDefaultValue( - &(this->synth->storage), Surge::Storage::UseNarratorAnnouncements, - !doAccAnn); - }); + accMenu.addItem(Surge::GUI::toOSCase("Send Additional Accessibility Announcements"), true, + doAccAnn, [this, doAccAnn]() { + Surge::Storage::updateUserDefaultValue( + &(this->synth->storage), Surge::Storage::UseNarratorAnnouncements, + !doAccAnn); + }); #if WINDOWS bool doAccAnnPatch = Surge::Storage::getUserDefaultValue( &(this->synth->storage), Surge::Storage::UseNarratorAnnouncementsForPatchTypeahead, true); - wfMenu.addItem("Announce Patch Browser entries", true, doAccAnnPatch, [this, doAccAnnPatch]() { + accMenu.addItem("Announce Patch Browser entries", true, doAccAnnPatch, [this, doAccAnnPatch]() { Surge::Storage::updateUserDefaultValue( &(this->synth->storage), Surge::Storage::UseNarratorAnnouncementsForPatchTypeahead, !doAccAnnPatch); @@ -4184,39 +4216,55 @@ juce::PopupMenu SurgeGUIEditor::makeWorkflowMenu(const juce::Point &where) bool doExpMen = Surge::Storage::getUserDefaultValue( &(this->synth->storage), Surge::Storage::ExpandModMenusWithSubMenus, false); - wfMenu.addItem(Surge::GUI::toOSCase("Add Sub-Menus for Modulation Menu Items"), true, doExpMen, - [this, doExpMen]() { - Surge::Storage::updateUserDefaultValue( - &(this->synth->storage), Surge::Storage::ExpandModMenusWithSubMenus, - !doExpMen); - }); + accMenu.addItem(Surge::GUI::toOSCase("Add Sub-Menus for Modulation Menu Items"), true, doExpMen, + [this, doExpMen]() { + Surge::Storage::updateUserDefaultValue( + &(this->synth->storage), Surge::Storage::ExpandModMenusWithSubMenus, + !doExpMen); + }); bool focusModEditor = Surge::Storage::getUserDefaultValue( &(this->synth->storage), Surge::Storage::FocusModEditorAfterAddModulationFrom, false); - wfMenu.addItem(Surge::GUI::toOSCase("Focus Modulator Editor on \"") + - Surge::GUI::toOSCase("Add Modulation From\" Actions"), - true, focusModEditor, [this, focusModEditor]() { - Surge::Storage::updateUserDefaultValue( - &(this->synth->storage), - Surge::Storage::FocusModEditorAfterAddModulationFrom, !focusModEditor); - }); + accMenu.addItem(Surge::GUI::toOSCase("Focus Modulator Editor on \"") + + Surge::GUI::toOSCase("Add Modulation From\" Actions"), + true, focusModEditor, [this, focusModEditor]() { + Surge::Storage::updateUserDefaultValue( + &(this->synth->storage), + Surge::Storage::FocusModEditorAfterAddModulationFrom, !focusModEditor); + }); - wfMenu.addSeparator(); + return accMenu; +} - bool showVirtualKeyboard = getShowVirtualKeyboard(); +void SurgeGUIEditor::setRecommendedAccessibility() +{ + std::ostringstream oss; + oss << "Set Accessibility Options: "; - Surge::GUI::addMenuWithShortcut(wfMenu, Surge::GUI::toOSCase("Show Virtual Keyboard"), - showShortcutDescription("Alt + K", u8"\U00002325K"), true, - showVirtualKeyboard, [this]() { toggleVirtualKeyboard(); }); + Surge::Storage::updateUserDefaultValue(&(this->synth->storage), + Surge::Storage::UseKeyboardShortcuts_Plugin, true); + Surge::Storage::updateUserDefaultValue(&(this->synth->storage), + Surge::Storage::UseKeyboardShortcuts_Standalone, true); + oss << "Keyboard shortcuts on; "; - bool showOscilloscope = isAnyOverlayPresent(OSCILLOSCOPE); + Surge::Storage::updateUserDefaultValue( + &(this->synth->storage), Surge::Storage::MenuAndEditKeybindingsFollowKeyboardFocus, true); + oss << "Menu Follows Keyboard; "; - Surge::GUI::addMenuWithShortcut(wfMenu, Surge::GUI::toOSCase("Open Oscilloscope"), - showShortcutDescription("Alt + O", u8"\U00002325O"), true, - showOscilloscope, [this]() { toggleOverlay(OSCILLOSCOPE); }); + Surge::Storage::updateUserDefaultValue(&(this->synth->storage), + Surge::Storage::UseNarratorAnnouncements, true); + Surge::Storage::updateUserDefaultValue( + &(this->synth->storage), Surge::Storage::UseNarratorAnnouncementsForPatchTypeahead, true); + oss << "Narrator announcements on; "; - return wfMenu; + Surge::Storage::updateUserDefaultValue(&(this->synth->storage), + Surge::Storage::ExpandModMenusWithSubMenus, true); + Surge::Storage::updateUserDefaultValue( + &(this->synth->storage), Surge::Storage::FocusModEditorAfterAddModulationFrom, true); + oss << "Expanded Modulation Menus and Modulation Focus."; + + enqueueAccessibleAnnouncement(oss.str()); } bool SurgeGUIEditor::getShowVirtualKeyboard() diff --git a/src/surge-xt/gui/SurgeGUIEditor.h b/src/surge-xt/gui/SurgeGUIEditor.h index a113bc4c4b6..55aced83536 100644 --- a/src/surge-xt/gui/SurgeGUIEditor.h +++ b/src/surge-xt/gui/SurgeGUIEditor.h @@ -337,6 +337,8 @@ class SurgeGUIEditor : public Surge::GUI::IComponentTagValue::Listener, std::string midiMappingToHtml(); + std::string patchToHtml(bool includeDefaults = false); + // These are unused right now enum SkinInspectorFlags { @@ -780,12 +782,15 @@ class SurgeGUIEditor : public Surge::GUI::IComponentTagValue::Listener, juce::PopupMenu makePatchDefaultsMenu(const juce::Point &rect); juce::PopupMenu makeValueDisplaysMenu(const juce::Point &rect); juce::PopupMenu makeWorkflowMenu(const juce::Point &rect); + juce::PopupMenu makeAccesibilityMenu(const juce::Point &rect); juce::PopupMenu makeDataMenu(const juce::Point &rect); juce::PopupMenu makeMidiMenu(const juce::Point &rect); juce::PopupMenu makeDevMenu(const juce::Point &rect); juce::PopupMenu makeLfoMenu(const juce::Point &rect); juce::PopupMenu makeMonoModeOptionsMenu(const juce::Point &rect, bool updateDefaults); + void setRecommendedAccessibility(); + public: void addHelpHeaderTo(const std::string &lab, const std::string &hu, juce::PopupMenu &m) const; diff --git a/src/surge-xt/gui/SurgeGUIEditorHtmlGenerators.cpp b/src/surge-xt/gui/SurgeGUIEditorHtmlGenerators.cpp index 034902b4ef3..3bf3730e2cf 100644 --- a/src/surge-xt/gui/SurgeGUIEditorHtmlGenerators.cpp +++ b/src/surge-xt/gui/SurgeGUIEditorHtmlGenerators.cpp @@ -2,6 +2,7 @@ #include "UserDefaults.h" #include #include "fmt/core.h" +#include "sst/cpputils.h" namespace Surge { @@ -632,5 +633,196 @@ std::string SurgeGUIEditor::skinInspectorHtml(SkinInspectorFlags f) htmls << "\n"; } endSection(); + return htmls.str(); +} + +std::string SurgeGUIEditor::patchToHtml(bool includeDefaults) +{ + std::ostringstream htmls; + + htmls << "
\n";
+
+    const auto &patch = synth->storage.getPatch();
+
+    std::vector scenes;
+    if (patch.scenemode.val.i == sm_single)
+    {
+        scenes.push_back(patch.scene_active.val.i);
+    }
+    else
+    {
+        for (int i = 0; i < n_scenes; ++i)
+            scenes.push_back(i);
+    }
+
+    htmls << "Patch " << patch.name << " / " << patch.category << "\n";
+
+    auto dumpParam = [&htmls, includeDefaults](const Parameter &p, const std::string &pfx = "") {
+        if (p.ctrltype == ct_none)
+            return;
+
+        if (includeDefaults || p.get_value_f01() != p.get_default_value_f01())
+        {
+            htmls << pfx << p.get_name() << " = " << p.get_display() << "\n";
+        }
+    };
+
+    // Global Stuff - polylimit, fx bypass, character
+    htmls << "\nGlobal Patch Settings\n";
+    for (const auto &par : {patch.scene_active, patch.scenemode, patch.splitpoint, patch.volume,
+                            patch.polylimit, patch.fx_bypass, patch.character})
+    {
+        dumpParam(par, "   ");
+    }
+
+    std::ostringstream mods;
+    std::set> modsourceSceneActive;
+
+    mods << "Global Modulation\n";
+    for (const auto &mr : patch.modulation_global)
+    {
+        mods << "    " << modulatorName(mr.source_id, false) << " -> "
+             << patch.param_ptr[mr.destination_id]->get_name() << " @ " << mr.depth << std::endl;
+        modsourceSceneActive.emplace(mr.source_id, mr.source_scene);
+    }
+
+    for (const auto &scn : scenes)
+    {
+        for (const auto &[rt, n] : {std::make_pair(patch.scene[scn].modulation_voice, "Voice"),
+                                    {patch.scene[scn].modulation_scene, "Scene"}})
+        {
+            mods << "Scene " << (char)('A' + scn) << " " << n << " Routing\n";
+            for (const auto &mr : rt)
+            {
+                mods << "    " << modulatorName(mr.source_id, false) << " -> "
+                     << patch.param_ptr[mr.destination_id + patch.scene_start[scn]]->get_name()
+                     << " @ " << mr.depth << std::endl;
+                modsourceSceneActive.emplace(mr.source_id, mr.source_scene);
+            }
+        }
+    }
+
+    for (const auto &scn : scenes)
+    {
+        htmls << "\nScene " << (char)('A' + scn) << "\n";
+        std::string pfx = "    ";
+        const auto &sc = patch.scene[scn];
+        int idx{1};
+        for (const auto &o : sc.osc)
+        {
+            htmls << pfx << "Oscillator " << idx << " : " << o.type.get_display() << "\n";
+            auto opfx = pfx + "    ";
+            const auto *p = &(o.type);
+            p++;
+            const auto *e = &(o.retrigger);
+            while (p <= e)
+            {
+                dumpParam(*p, opfx);
+                p++;
+            }
+            if (uses_wavetabledata(idx - 1))
+            {
+                htmls << opfx << "Wavetable : " << o.wavetable_display_name << std::endl;
+            }
+            idx++;
+        }
+        const auto *p = &(sc.pitch);
+        const auto *e = &(sc.send_level[n_send_slots - 1]);
+        while (p <= e)
+        {
+            dumpParam(*p, pfx);
+            p++;
+        }
+
+        idx = 1;
+        for (const auto &f : sc.filterunit)
+        {
+            if (f.type.val.i != sst::filters::fut_none)
+            {
+                htmls << pfx << "Filter " << idx << " : " << f.type.get_display() << "\n";
+                auto opfx = pfx + "    ";
+                const auto *p = &(f.subtype);
+
+                // force subtype
+                htmls << opfx << p->get_name() << " = " << p->get_display() << "\n";
+                p++;
+                const auto *e = &(f.keytrack);
+                while (p <= e)
+                {
+                    dumpParam(*p, opfx);
+                    p++;
+                }
+                if (idx == 2)
+                    dumpParam(sc.f2_cutoff_is_offset, opfx);
+                dumpParam(sc.f2_link_resonance, opfx);
+                idx++;
+            }
+        }
+
+        dumpParam(sc.feedback, pfx);
+        dumpParam(sc.filterblock_configuration, pfx);
+        dumpParam(sc.filter_balance, pfx);
+        dumpParam(sc.lowcut, pfx);
+
+        if (sc.wsunit.type.val.i != (int)sst::waveshapers::WaveshaperType::wst_none)
+        {
+            dumpParam(sc.wsunit.type, pfx);
+            dumpParam(sc.wsunit.drive, pfx + "    ");
+        }
+
+        for (const auto &idx : {ms_ampeg, ms_filtereg})
+        {
+            const auto &en = sc.adsr[idx - ms_ampeg];
+            htmls << pfx << (idx == ms_ampeg ? "AEG" : "FEG") << "\n";
+            const auto *p = &(en.a);
+            const auto *e = &(en.mode);
+            while (p <= e)
+            {
+                dumpParam(*p, pfx + "    ");
+                p++;
+            }
+        }
+
+        for (const auto &[li, lf] : sst::cpputils::enumerate(sc.lfo))
+        {
+            auto ms = ms_lfo1 + li;
+            if (modsourceSceneActive.find({ms, scn}) != modsourceSceneActive.end())
+            {
+                htmls << pfx << modulatorName(ms, false) << "\n";
+                const auto *p = &(lf.rate);
+                const auto *e = &(lf.release);
+                while (p <= e)
+                {
+                    dumpParam(*p, pfx + "    ");
+                    p++;
+                }
+            }
+        }
+
+        // Scenes
+    }
+
+    // FX
+    htmls << "\nFX\n";
+    for (int i = 0; i < n_fx_slots; ++i)
+    {
+        if (includeDefaults || patch.fx[i].type.val.i != fxt_off)
+        {
+            const auto &fx = patch.fx[i];
+            auto pfx = "        ";
+            htmls << "   " << fxslot_names[i] << ": " << fx_type_names[fx.type.val.i] << "\n";
+            if (i == fxslot_send1 || i == fxslot_send2 || i == fxslot_send3 || i == fxslot_send4)
+            {
+                dumpParam(fx.return_level, pfx);
+            }
+            for (const auto &p : fx.p)
+                dumpParam(p, pfx);
+        }
+    }
+
+    htmls << "\n" << mods.str() << "\n";
+
+    htmls << "
\n"; + return htmls.str(); } \ No newline at end of file