diff --git a/examples/GUI/AccessibilityDemo.h b/examples/GUI/AccessibilityDemo.h new file mode 100644 index 0000000000..045e69b472 --- /dev/null +++ b/examples/GUI/AccessibilityDemo.h @@ -0,0 +1,1441 @@ +/* + ============================================================================== + + This file is part of the JUCE examples. + Copyright (c) 2020 - Raw Material Software Limited + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, + WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR + PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +/******************************************************************************* + The block below describes the properties of this PIP. A PIP is a short snippet + of code that can be read by the Projucer and used to generate a JUCE project. + + BEGIN_JUCE_PIP_METADATA + + name: AccessibilityDemo + version: 1.0.0 + vendor: JUCE + website: http://juce.com + description: Simple UI demonstrating accessibility features on supported + platforms. + + dependencies: juce_core, juce_data_structures, juce_events, juce_graphics, + juce_gui_basics + exporters: xcode_mac, vs2019 + + moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1 + + type: Component + mainClass: AccessibilityDemo + + useLocalCopy: 1 + + END_JUCE_PIP_METADATA + +*******************************************************************************/ + +#pragma once + +#include "../Assets/DemoUtilities.h" + +//============================================================================== +/** + A simple holder component with some content, a title and an info tooltip + containing a brief description. + + This component sets its accessibility title and help text properties and + also acts as a focus container for its children. +*/ +class ContentComponent : public Component +{ +public: + ContentComponent (const String& title, const String& info, Component& contentToDisplay) + : titleLabel ({}, title), + content (contentToDisplay) + { + addAndMakeVisible (titleLabel); + addAndMakeVisible (infoIcon); + + setTitle (title); + setDescription (info); + setFocusContainerType (FocusContainerType::focusContainer); + + infoIcon.setTooltip (info); + infoIcon.setHelpText (info); + + addAndMakeVisible (content); + } + + void paint (Graphics& g) override + { + g.setColour (Colours::black); + g.drawRoundedRectangle (getLocalBounds().reduced (2).toFloat(), 5.0f, 3.0f); + } + + void resized() override + { + auto bounds = getLocalBounds().reduced (5); + + auto topArea = bounds.removeFromTop (30); + infoIcon.setBounds (topArea.removeFromLeft (30).reduced (5)); + titleLabel.setBounds (topArea.reduced (5)); + + content.setBounds (bounds); + } + +private: + //============================================================================== + struct InfoIcon : public Component, + public SettableTooltipClient + { + InfoIcon() + { + constexpr uint8 infoPathData[] = + { + 110,109,0,0,122,67,0,0,0,0,98,79,35,224,66,0,0,0,0,0,0,0,0,79,35,224,66,0,0,0,0,0,0,122,67,98,0,0, + 0,0,44,247,193,67,79,35,224,66,0,0,250,67,0,0,122,67,0,0,250,67,98,44,247,193,67,0,0,250,67,0,0, + 250,67,44,247,193,67,0,0,250,67,0,0,122,67,98,0,0,250,67,79,35,224,66,44,247,193,67,0,0,0,0,0,0,122, + 67,0,0,0,0,99,109,114,79,101,67,79,35,224,66,108,71,88,135,67,79,35,224,66,108,71,88,135,67,132,229, + 28,67,108,116,79,101,67,132,229,28,67,108,116,79,101,67,79,35,224,66,99,109,79,35,149,67,106,132, + 190,67,108,98,185,123,67,106,132,190,67,98,150,123,106,67,106,132,190,67,176,220,97,67,168,17,187, + 67,176,220,97,67,18,150,177,67,108,176,220,97,67,248,52,108,67,98,176,220,97,67,212,8,103,67,238, + 105,94,67,18,150,99,67,204,61,89,67,18,150,99,67,108,98,185,73,67,18,150,99,67,108,98,185,73,67,88, + 238,59,67,108,160,70,120,67,88,238,59,67,98,54,194,132,67,88,238,59,67,169,17,137,67,60,141,68,67, + 169,17,137,67,8,203,85,67,108,169,17,137,67,26,97,166,67,98,169,17,137,67,43,247,168,67,10,203,138, + 67,141,176,170,67,27,97,141,67,141,176,170,67,108,80,35,149,67,141,176,170,67,108,80,35,149,67,106, + 132,190,67,99,101,0,0 + }; + + infoPath.loadPathFromData (infoPathData, numElementsInArray (infoPathData)); + + setTitle ("Info"); + } + + void paint (Graphics& g) override + { + auto bounds = getLocalBounds().toFloat().reduced (2); + + g.setColour (Colours::white); + g.fillPath (infoPath, RectanglePlacement (RectanglePlacement::centred) + .getTransformToFit (infoPath.getBounds(), bounds)); + } + + Path infoPath; + }; + + //============================================================================== + Label titleLabel; + InfoIcon infoIcon; + Component& content; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ContentComponent) +}; + +//============================================================================== +/** + The top-level component containing the accessible JUCE widget examples. + + Most JUCE UI elements have built-in accessibility support and will be + visible and controllable by accessibility clients. There are a few examples + of some widgets in this demo such as Sliders, Buttons and a TreeView. +*/ +class JUCEWidgetsComponent : public Component +{ +public: + JUCEWidgetsComponent() + { + setTitle ("JUCE Widgets"); + setDescription ("A demo of a few of the accessible built-in JUCE widgets."); + setFocusContainerType (FocusContainerType::focusContainer); + + addAndMakeVisible (descriptionLabel); + + addAndMakeVisible (buttons); + addAndMakeVisible (sliders); + addAndMakeVisible (treeView); + } + + void resized() override + { + Grid grid; + + grid.templateRows = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (2)) }; + grid.templateColumns = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)) }; + + grid.items = { GridItem (descriptionLabel).withMargin ({ 2 }).withColumn ({ GridItem::Span (2) }), + GridItem (buttons).withMargin ({ 2 }), + GridItem (sliders).withMargin ({ 2 }), + GridItem (treeView).withMargin ({ 2 }).withColumn ({ GridItem::Span (2) }) }; + + grid.performLayout (getLocalBounds()); + } + +private: + //============================================================================== + class ButtonsComponent : public Component + { + public: + ButtonsComponent() + { + addAndMakeVisible (radioButtons); + addAndMakeVisible (textButton); + + shapeButton.setShape (getJUCELogoPath(), false, true, false); + shapeButton.onClick = [] { AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, "Alert", "This is an AlertWindow"); }; + addAndMakeVisible (shapeButton); + } + + void resized() override + { + auto bounds = getLocalBounds(); + + radioButtons.setBounds (bounds.removeFromLeft (bounds.getWidth() / 2).reduced (5)); + textButton.setBounds (bounds.removeFromTop (bounds.getHeight() / 2).reduced (5)); + shapeButton.setBounds (bounds.reduced (5)); + } + + private: + //============================================================================== + struct RadioButtonsGroupComponent : public Component + { + RadioButtonsGroupComponent() + { + int index = 1; + for (auto& b : radioButtons) + { + b.setRadioGroupId (1); + b.setButtonText ("Button " + String (index++)); + addAndMakeVisible (b); + } + + radioButtons[(size_t) Random::getSystemRandom().nextInt (numRadioButtons)].setToggleState (true, dontSendNotification); + + setTitle ("Radio Buttons Group"); + setFocusContainerType (FocusContainerType::focusContainer); + } + + void resized() override + { + auto bounds = getLocalBounds(); + const auto height = bounds.getHeight() / numRadioButtons; + + for (auto& b : radioButtons) + b.setBounds (bounds.removeFromTop (height).reduced (2)); + } + + static constexpr int numRadioButtons = 3; + std::array radioButtons; + }; + + //============================================================================== + RadioButtonsGroupComponent radioButtons; + TextButton textButton { "TextButton" }; + ShapeButton shapeButton { "ShapeButton", + Colours::darkorange, + Colours::darkorange.brighter (0.5f), + Colours::darkorange.brighter (0.75f) }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonsComponent) + }; + + //============================================================================== + class SlidersComponent : public Component + { + public: + SlidersComponent() + { + auto setUpSlider = [this] (Slider& slider, Slider::SliderStyle style, + double start, double end, double interval) + { + slider.setSliderStyle (style); + slider.setRange (start, end, interval); + + if (style != Slider::IncDecButtons) + slider.setTextBoxStyle (Slider::NoTextBox, false, 0, 0); + + slider.setValue (start + (end - start) * Random::getSystemRandom().nextDouble()); + + addAndMakeVisible (slider); + }; + + setUpSlider (horizontalSlider, Slider::LinearHorizontal, 1.0, 100.0, 1.0); + setUpSlider (incDecSlider, Slider::IncDecButtons, 1.0, 10.0, 1.0); + + for (auto& rotary : rotarySliders) + setUpSlider (rotary, Slider::Rotary, 1.0, 10.0, 1.0); + } + + void resized() override + { + Grid grid; + + grid.templateRows = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (2)) }; + + grid.templateColumns = { Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)) }; + + grid.items = { GridItem (horizontalSlider).withMargin ({ 2 }).withColumn ({ GridItem::Span (2) }), + GridItem (incDecSlider).withMargin ({ 2 }) }; + + for (auto& rotary : rotarySliders) + grid.items.add (GridItem (rotary).withMargin ({ 2 })); + + grid.performLayout (getLocalBounds()); + } + + private: + Slider horizontalSlider, incDecSlider; + std::array rotarySliders; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SlidersComponent) + }; + + //============================================================================== + class TreeViewComponent : public Component + { + public: + TreeViewComponent() + { + tree.setRootItem (&root); + tree.setRootItemVisible (false); + + addAndMakeVisible (tree); + } + + void resized() override + { + tree.setBounds (getLocalBounds()); + } + + private: + //============================================================================== + struct RootItem : public TreeViewItem + { + RootItem() + { + struct Item : public TreeViewItem + { + Item (int index, int depth, int numSubItems) + : textToDisplay ("Item " + String (index) + + ". Depth: " + String (depth) + + ". Num sub-items: " + String (numSubItems)) + { + for (int i = 0; i < numSubItems; ++i) + addSubItem (new Item (i, + depth + 1, + Random::getSystemRandom().nextInt (jmax (0, 5 - depth)))); + } + + bool mightContainSubItems() override + { + return getNumSubItems() > 0; + } + + void paintItem (Graphics& g, int width, int height) override + { + if (isSelected()) + { + g.setColour (Colours::yellow.withAlpha (0.3f)); + g.fillRect (0, 0, width, height); + } + + g.setColour (Colours::black); + g.drawRect (0, height - 1, width, 1); + + g.setColour (Colours::white); + g.drawText (textToDisplay, 0, 0, width, height, Justification::centredLeft); + } + + String getAccessibilityName() override { return textToDisplay; } + + const String textToDisplay; + }; + + for (int i = 0; i < 10; ++i) + addSubItem (new Item (i, 0, Random::getSystemRandom().nextInt (10))); + } + + bool mightContainSubItems() override + { + return true; + } + }; + + //============================================================================== + TreeView tree; + RootItem root; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TreeViewComponent) + }; + + //============================================================================== + Label descriptionLabel { {}, "This is a demo of a few of the accessible built-in JUCE widgets.\n\n" + "To navigate this demo with a screen reader, either enable VoiceOver on macOS " + "or Narrator on Windows and follow the navigational prompts." }; + + ButtonsComponent buttonsComponent; + SlidersComponent slidersComponent; + TreeViewComponent treeViewComponent; + + ContentComponent buttons { "Buttons", + "Examples of some JUCE buttons.", + buttonsComponent }; + ContentComponent sliders { "Sliders", + "Examples of some JUCE sliders.", + slidersComponent }; + ContentComponent treeView { "TreeView", + "A JUCE TreeView.", + treeViewComponent }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JUCEWidgetsComponent) +}; + +struct NameAndRole +{ + const char* name; + AccessibilityRole role; +}; + +constexpr NameAndRole accessibilityRoles[] +{ + { "Ignored", AccessibilityRole::ignored }, + { "Unspecified", AccessibilityRole::unspecified }, + { "Button", AccessibilityRole::button }, + { "ComboBox", AccessibilityRole::comboBox }, + { "Slider", AccessibilityRole::slider }, + { "Static Text", AccessibilityRole::staticText }, + { "Editable Text", AccessibilityRole::editableText }, + { "Image", AccessibilityRole::image }, + { "Group", AccessibilityRole::group }, + { "Window", AccessibilityRole::window } +}; + +//============================================================================== +/** + The top-level component containing a customisable accessible widget. + + The AccessibleComponent class just draws a JUCE logo and overrides the + Component::createAccessibilityHandler() method to return a custom AccessibilityHandler. + The properties of this handler are set by the various controls in the demo. +*/ +class CustomWidgetComponent : public Component +{ +public: + CustomWidgetComponent() + { + setTitle ("Custom Widget"); + setDescription ("A demo of a customisable accessible widget."); + setFocusContainerType (FocusContainerType::focusContainer); + + addAndMakeVisible (descriptionLabel); + + addAndMakeVisible (infoComponent); + addAndMakeVisible (actionsComponent); + addAndMakeVisible (valueInterfaceComponent); + addAndMakeVisible (stateComponent); + addAndMakeVisible (accessibleComponent); + } + + void resized() override + { + Grid grid; + + grid.templateRows = { Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (2)), + Grid::TrackInfo (Grid::Fr (2)), + Grid::TrackInfo (Grid::Fr (2)) }; + + grid.templateColumns = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)) }; + + grid.items = { GridItem (descriptionLabel).withMargin ({ 2 }).withColumn ({ GridItem::Span (2) }), + GridItem (infoComponent).withMargin ({ 2 }).withColumn ({ GridItem::Span (2) }), + GridItem (actionsComponent).withMargin ({ 2 }), + GridItem (valueInterfaceComponent).withMargin ({ 2 }), + GridItem (stateComponent).withMargin ({ 2 }), + GridItem (accessibleComponent).withMargin ({ 10 }) }; + + grid.performLayout (getLocalBounds()); + } + +private: + //============================================================================== + class AccessibleComponent : public Component + { + public: + explicit AccessibleComponent (CustomWidgetComponent& owner) + : customWidgetComponent (owner) + { + } + + void paint (Graphics& g) override + { + g.setColour (Colours::darkorange); + + g.fillPath (juceLogoPath, + juceLogoPath.getTransformToScaleToFit (getLocalBounds().toFloat(), true)); + } + + std::unique_ptr createAccessibilityHandler() override + { + /** + The AccessibilityHandler class is the interface between JUCE components + and accessibility clients. This derived class represents the properties + set via the demo UI. + */ + struct CustomAccessibilityHandler : public AccessibilityHandler + { + explicit CustomAccessibilityHandler (CustomWidgetComponent& comp) + : AccessibilityHandler (comp.accessibleComponent, + comp.infoComponent.getRole(), + comp.actionsComponent.getActions(), + { comp.valueInterfaceComponent.getValueInterface() }), + customWidgetComponent (comp) + { + } + + String getTitle() const override { return customWidgetComponent.infoComponent.getTitle(); } + String getDescription() const override { return customWidgetComponent.infoComponent.getDescription(); } + String getHelp() const override { return customWidgetComponent.infoComponent.getHelp(); } + + AccessibleState getCurrentState() const override { return customWidgetComponent.stateComponent.getAccessibleState(); } + + CustomWidgetComponent& customWidgetComponent; + }; + + return std::make_unique (customWidgetComponent); + } + + private: + CustomWidgetComponent& customWidgetComponent; + Path juceLogoPath { getJUCELogoPath() }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibleComponent) + }; + + //============================================================================== + class InfoComponent : public Component + { + public: + explicit InfoComponent (CustomWidgetComponent& owner) + { + titleEditor.setText ("Custom"); + descriptionEditor.setText ("A short description of the custom component."); + helpEditor.setText ("Some help text for the custom component."); + + titleEditor.onTextChange = [&owner] + { + if (auto* handler = owner.accessibleComponent.getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::titleChanged); + }; + + for (auto* editor : { &descriptionEditor, &helpEditor }) + { + editor->setMultiLine (true); + editor->setReturnKeyStartsNewLine (true); + editor->setJustification (Justification::centredLeft); + } + + addAndMakeVisible (titleLabel); + addAndMakeVisible (titleEditor); + + addAndMakeVisible (descriptionLabel); + addAndMakeVisible (descriptionEditor); + + addAndMakeVisible (helpLabel); + addAndMakeVisible (helpEditor); + + setUpAccessibilityRoleSelector (owner); + addAndMakeVisible (roleBox); + addAndMakeVisible (roleLabel); + } + + void resized() override + { + Grid grid; + + grid.templateRows = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (3)) }; + + grid.templateColumns = { Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)) }; + + grid.items = { GridItem (titleLabel).withMargin ({ 2 }), + GridItem (titleEditor).withMargin ({ 2 }).withColumn ({ GridItem::Span (2) }), + + GridItem (roleLabel).withMargin ({ 2 }), + GridItem (roleBox).withMargin ({ 2 }).withColumn ({ GridItem::Span (2) }), + + GridItem (descriptionLabel).withMargin ({ 2 }), + GridItem (descriptionEditor).withMargin ({ 2 }).withColumn ({ GridItem::Span (2) }), + + GridItem (helpLabel).withMargin ({ 2 }), + GridItem (helpEditor).withMargin ({ 2 }).withColumn ({ GridItem::Span (2) }) }; + + grid.performLayout (getLocalBounds()); + } + + AccessibilityRole getRole() const noexcept { return accessibilityRoles[(size_t) roleBox.getSelectedItemIndex()].role; } + + String getTitle() const noexcept { return titleEditor.getText(); } + String getDescription() const noexcept { return descriptionEditor.getText(); } + String getHelp() const noexcept { return helpEditor.getText(); } + + private: + void setUpAccessibilityRoleSelector (CustomWidgetComponent& owner) + { + int itemId = 1; + for (const auto& nameAndRole : accessibilityRoles) + roleBox.addItem (nameAndRole.name, itemId++); + + roleBox.setSelectedItemIndex (1); + roleBox.onChange = [&owner] { owner.accessibleComponent.invalidateAccessibilityHandler(); }; + } + + //============================================================================== + Label titleLabel { {}, "Title" }, descriptionLabel { {}, "Description" }, helpLabel { {}, "Help" }; + TextEditor titleEditor, descriptionEditor, helpEditor; + + Label roleLabel { {}, "Role" }; + ComboBox roleBox; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InfoComponent) + }; + + //============================================================================== + class ActionsComponent : public Component + { + public: + explicit ActionsComponent (CustomWidgetComponent& owner) + : customWidgetComponent (owner) + { + addAndMakeVisible (press); + addAndMakeVisible (toggle); + addAndMakeVisible (focus); + addAndMakeVisible (showMenu); + } + + void resized() override + { + Grid grid; + + grid.templateRows = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)) }; + grid.templateColumns = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)) }; + + grid.items = { GridItem (press).withMargin (2), GridItem (toggle).withMargin (2), + GridItem (focus).withMargin (2), GridItem (showMenu).withMargin (2), }; + + grid.performLayout (getLocalBounds()); + } + + AccessibilityActions getActions() noexcept + { + AccessibilityActions result; + + if (press.isEnabled()) result.addAction (AccessibilityActionType::press, [this] { press.onAction(); }); + if (toggle.isEnabled()) result.addAction (AccessibilityActionType::toggle, [this] { toggle.onAction(); }); + if (focus.isEnabled()) result.addAction (AccessibilityActionType::focus, [this] { focus.onAction(); }); + if (showMenu.isEnabled()) result.addAction (AccessibilityActionType::showMenu, [this] { showMenu.onAction(); }); + + return result; + } + + private: + //============================================================================== + class AccessibilityActionComponent : public Component, + private Timer + { + public: + AccessibilityActionComponent (CustomWidgetComponent& owner, + const String& actionName, + bool initialState) + { + enabledToggle.setButtonText (actionName); + enabledToggle.onClick = [&owner] { owner.accessibleComponent.invalidateAccessibilityHandler(); }; + enabledToggle.setToggleState (initialState, dontSendNotification); + + addAndMakeVisible (enabledToggle); + } + + void resized() override + { + auto bounds = getLocalBounds(); + + flashArea = bounds.removeFromRight (bounds.getHeight()).reduced (5); + bounds.removeFromRight (5); + enabledToggle.setBounds (bounds); + } + + void paint (Graphics& g) override + { + g.setColour (flashColour); + g.fillRoundedRectangle (flashArea.toFloat(), 5.0f); + } + + void onAction() + { + if (isTimerRunning()) + reset(); + + startTime = Time::getMillisecondCounter(); + startTimer (5); + } + + private: + void timerCallback() override + { + const auto alpha = [this] + { + const auto progress = static_cast (Time::getMillisecondCounter() - startTime) / (flashTimeMs / 2); + + return progress > 1.0f ? 2.0f - progress + : progress; + }(); + + if (alpha < 0.0f) + { + reset(); + return; + } + + flashColour = defaultColour.overlaidWith (Colours::yellow.withAlpha (alpha)); + repaint(); + } + + void reset() + { + stopTimer(); + flashColour = defaultColour; + repaint(); + } + + //============================================================================== + static constexpr int flashTimeMs = 500; + + ToggleButton enabledToggle; + Rectangle flashArea; + uint32 startTime = 0; + Colour defaultColour = Colours::lightgrey, flashColour = defaultColour; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityActionComponent) + }; + + //============================================================================== + CustomWidgetComponent& customWidgetComponent; + + AccessibilityActionComponent press { customWidgetComponent, "Press", true }, + toggle { customWidgetComponent, "Toggle", false }, + focus { customWidgetComponent, "Focus", true }, + showMenu { customWidgetComponent, "Show menu", false }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ActionsComponent) + }; + + //============================================================================== + class ValueInterfaceComponent : public Component + { + public: + explicit ValueInterfaceComponent (CustomWidgetComponent& owner) + : customWidgetComponent (owner) + { + valueTypeBox.addItemList ({ "Numeric", "Ranged", "Text" }, 1); + valueTypeBox.setSelectedId (1); + addAndMakeVisible (valueTypeBox); + + valueTypeBox.onChange = [this] + { + updateValueUI(); + customWidgetComponent.accessibleComponent.invalidateAccessibilityHandler(); + }; + + addAndMakeVisible (readOnlyToggle); + + numericValueEditor.setInputRestrictions (10, "0123456789."); + addChildComponent (numericValueEditor); + + addChildComponent (rangedValueComponent); + addChildComponent (textValueEditor); + + updateValueUI(); + } + + void resized() override + { + Grid grid; + + grid.templateRows = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (2)) }; + grid.templateColumns = { Grid::TrackInfo (Grid::Fr (2)), Grid::TrackInfo (Grid::Fr (1)) }; + + auto& valueEditComponent = [this]() -> Component& + { + if (numericValueEditor.isVisible()) return numericValueEditor; + if (rangedValueComponent.isVisible()) return rangedValueComponent; + if (textValueEditor.isVisible()) return textValueEditor; + + jassertfalse; + return numericValueEditor; + }(); + + grid.items = { GridItem (valueTypeBox).withMargin (2), GridItem (readOnlyToggle).withMargin (2), + GridItem (valueEditComponent).withMargin (2).withColumn ({ GridItem::Span (2) }), }; + + grid.performLayout (getLocalBounds()); + } + + std::unique_ptr getValueInterface() + { + struct Numeric : public AccessibilityNumericValueInterface + { + explicit Numeric (ValueInterfaceComponent& o) + : owner (o) + { + } + + bool isReadOnly() const override { return owner.readOnlyToggle.getToggleState(); } + double getCurrentValue() const override { return owner.numericValueEditor.getText().getDoubleValue(); } + void setValue (double newValue) override { owner.numericValueEditor.setText (String (newValue)); } + + ValueInterfaceComponent& owner; + }; + + struct Ranged : public AccessibilityRangedNumericValueInterface + { + explicit Ranged (ValueInterfaceComponent& o) + : owner (o) + { + } + + bool isReadOnly() const override { return owner.readOnlyToggle.getToggleState(); } + double getCurrentValue() const override { return owner.rangedValueComponent.valueSlider.getValue(); } + void setValue (double newValue) override { owner.rangedValueComponent.valueSlider.setValue (newValue); } + + AccessibleValueRange getRange() const override + { + const auto& slider = owner.rangedValueComponent.valueSlider; + + return { { slider.getMinimum(), slider.getMaximum() }, + slider.getInterval() }; + } + + ValueInterfaceComponent& owner; + }; + + struct Text : public AccessibilityTextValueInterface + { + explicit Text (ValueInterfaceComponent& o) + : owner (o) + { + } + + bool isReadOnly() const override { return owner.readOnlyToggle.getToggleState(); } + String getCurrentValueAsString() const override { return owner.textValueEditor.getText(); } + void setValueAsString (const String& newValue) override { owner.textValueEditor.setText (newValue); } + + ValueInterfaceComponent& owner; + }; + + const auto valueType = indexToValueType (valueTypeBox.getSelectedId()); + + if (valueType == ValueType::numeric) return std::make_unique (*this); + if (valueType == ValueType::ranged) return std::make_unique (*this); + if (valueType == ValueType::text) return std::make_unique (*this); + + jassertfalse; + return nullptr; + } + + private: + //============================================================================== + struct RangedValueComponent : public Component + { + RangedValueComponent() + { + const auto setUpNumericTextEditor = [this] (TextEditor& ed, double initialValue) + { + ed.setInputRestrictions (10, "0123456789."); + ed.setText (String (initialValue)); + ed.onReturnKey = [this] { updateSliderRange(); }; + + addAndMakeVisible (ed); + }; + + setUpNumericTextEditor (minValueEditor, 0.0); + setUpNumericTextEditor (maxValueEditor, 10.0); + setUpNumericTextEditor (intervalValueEditor, 0.1); + + addAndMakeVisible (minLabel); + addAndMakeVisible (maxLabel); + addAndMakeVisible (intervalLabel); + + valueSlider.setSliderStyle (Slider::SliderStyle::LinearHorizontal); + addAndMakeVisible (valueSlider); + updateSliderRange(); + } + + void resized() override + { + Grid grid; + + grid.templateRows = { Grid::TrackInfo (Grid::Fr (2)), Grid::TrackInfo (Grid::Fr (3)), Grid::TrackInfo (Grid::Fr (3)) }; + grid.templateColumns = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)) }; + + grid.items = { GridItem (minLabel).withMargin (2), GridItem (maxLabel).withMargin (2), GridItem (intervalLabel).withMargin (2), + GridItem (minValueEditor).withMargin (2), GridItem (maxValueEditor).withMargin (2), GridItem (intervalValueEditor).withMargin (2), + GridItem (valueSlider).withMargin (2).withColumn ({ GridItem::Span (3) }) }; + + grid.performLayout (getLocalBounds()); + } + + void updateSliderRange() + { + auto minValue = minValueEditor.getText().getDoubleValue(); + auto maxValue = maxValueEditor.getText().getDoubleValue(); + const auto intervalValue = jmax (intervalValueEditor.getText().getDoubleValue(), 0.0001); + + if (maxValue <= minValue) + { + maxValue = minValue + intervalValue; + maxValueEditor.setText (String (maxValue)); + } + else if (minValue >= maxValue) + { + minValue = maxValue - intervalValue; + minValueEditor.setText (String (minValue)); + } + + valueSlider.setRange (minValue, maxValue, intervalValue); + } + + Label minLabel { {}, "Min" }, maxLabel { {}, "Max" }, intervalLabel { {}, "Interval" }; + TextEditor minValueEditor, maxValueEditor, intervalValueEditor; + Slider valueSlider; + }; + + //============================================================================== + enum class ValueType { numeric, ranged, text }; + + static ValueType indexToValueType (int index) noexcept + { + if (index == 1) return ValueType::numeric; + if (index == 2) return ValueType::ranged; + if (index == 3) return ValueType::text; + + jassertfalse; + return ValueType::numeric; + } + + void updateValueUI() + { + const auto valueType = indexToValueType (valueTypeBox.getSelectedId()); + + numericValueEditor.setVisible (valueType == ValueType::numeric); + textValueEditor.setVisible (valueType == ValueType::text); + rangedValueComponent.setVisible (valueType == ValueType::ranged); + + resized(); + } + + //============================================================================== + CustomWidgetComponent& customWidgetComponent; + + ComboBox valueTypeBox; + ToggleButton readOnlyToggle { "Read-Only" }; + + TextEditor numericValueEditor, textValueEditor; + RangedValueComponent rangedValueComponent; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ValueInterfaceComponent) + }; + + //============================================================================== + class StateComponent : public Component + { + public: + StateComponent() + { + for (auto& property : properties) + addAndMakeVisible (property.button); + } + + void resized() override + { + Grid grid; + + grid.templateRows = { Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)) }; + + grid.templateColumns = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)) }; + + for (auto& property : properties) + grid.items.add (GridItem (property.button)); + + grid.performLayout (getLocalBounds()); + } + + AccessibleState getAccessibleState() const + { + AccessibleState result; + + for (auto& property : properties) + if (property.button.getToggleState()) + result = property.setState (std::move (result)); + + return result; + } + + private: + struct Property + { + Property (const String& name, + bool initialState, + AccessibleState (AccessibleState::* fn)() const) + : button (name), + setStateFn (fn) + { + button.setToggleState (initialState, dontSendNotification); + } + + AccessibleState setState (AccessibleState s) const noexcept { return (s.*setStateFn)(); } + + ToggleButton button; + AccessibleState (AccessibleState::* setStateFn)() const; + }; + + std::array properties + { { + { "Checkable", false, &AccessibleState::withCheckable }, + { "Checked", false, &AccessibleState::withChecked }, + { "Collapsed", false, &AccessibleState::withCollapsed }, + { "Expandable", false, &AccessibleState::withExpandable }, + { "Expanded", false, &AccessibleState::withExpanded }, + { "Focusable", true, &AccessibleState::withFocusable }, + { "Focused", false, &AccessibleState::withFocused }, + { "Ignored", false, &AccessibleState::withIgnored }, + { "Selectable", false, &AccessibleState::withSelectable }, + { "Multi-Selectable", false, &AccessibleState::withMultiSelectable }, + { "Selected", false, &AccessibleState::withSelected }, + { "Accessible Offscreen", false, &AccessibleState::withAccessibleOffscreen } + } }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StateComponent) + }; + + //============================================================================== + Label descriptionLabel { {}, "This is a demo of a custom accessible widget.\n\n" + "The JUCE logo component at the bottom of the page will use the settable properties when queried by " + "an accessibility client." }; + + InfoComponent infoComponent { *this }; + ActionsComponent actionsComponent { *this }; + ValueInterfaceComponent valueInterfaceComponent { *this }; + StateComponent stateComponent; + + ContentComponent info { "Info", + "Set the title, role, description and help text properties of the component.", + infoComponent }; + ContentComponent actions { "Actions", + "Specify the accessibility actions that the component can perform. When invoked the indicator will flash.", + actionsComponent }; + ContentComponent valueInterface { "Value", + "Sets the value that this component represents. This can be numeric, ranged or textual and can optionally be read-only.", + valueInterfaceComponent }; + ContentComponent state { "State", + "Modify the AccessibleState properties of the component.", + stateComponent }; + + AccessibleComponent accessibleComponent { *this }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomWidgetComponent) +}; + +//============================================================================== +/** + The top-level component containing an example of custom child component navigation. +*/ +class CustomNavigationComponent : public Component +{ +public: + CustomNavigationComponent() + { + addAndMakeVisible (descriptionLabel); + + setTitle ("Navigation"); + setHelpText ("Control the navigation order of child components."); + setFocusContainerType (FocusContainerType::focusContainer); + + constexpr int numChildren = 12; + + for (int i = 1; i <= numChildren; ++i) + { + children.push_back (std::make_unique (i, numChildren, *this)); + addAndMakeVisible (*children.back()); + } + } + + void resized() override + { + Grid grid; + + grid.templateRows = { Grid::TrackInfo (Grid::Fr (2)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)) }; + + grid.templateColumns = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)) }; + + grid.items.add (GridItem (descriptionLabel).withMargin (2).withColumn ({ GridItem::Span (3) })); + + for (auto& child : children) + grid.items.add (GridItem (*child).withMargin (5)); + + grid.performLayout (getLocalBounds()); + } + + std::unique_ptr createFocusTraverser() override + { + struct CustomTraverser : public FocusTraverser + { + explicit CustomTraverser (CustomNavigationComponent& owner) + : customNavigationComponent (owner) {} + + Component* getDefaultComponent (Component*) override + { + for (auto& child : customNavigationComponent.children) + if (child->defaultToggle.getToggleState() && child->focusableToggle.getToggleState()) + return child.get(); + + return nullptr; + } + + Component* getNextComponent (Component* current) override + { + const auto& comps = customNavigationComponent.children; + + const auto iter = std::find_if (comps.cbegin(), comps.cend(), + [current] (const std::unique_ptr& c) { return c.get() == current; }); + + if (iter != comps.cend() && iter != std::prev (comps.cend())) + return std::next (iter)->get(); + + return nullptr; + } + + Component* getPreviousComponent (Component* current) override + { + const auto& comps = customNavigationComponent.children; + + const auto iter = std::find_if (comps.cbegin(), comps.cend(), + [current] (const std::unique_ptr& c) { return c.get() == current; }); + + if (iter != comps.cend() && iter != comps.cbegin()) + return std::prev (iter)->get(); + + return nullptr; + } + + std::vector getAllComponents (Component*) override + { + std::vector comps; + + for (auto& child : customNavigationComponent.children) + if (child->focusableToggle.getToggleState()) + comps.push_back (child.get()); + + return comps; + } + + CustomNavigationComponent& customNavigationComponent; + }; + + return std::make_unique (*this); + } + +private: + struct NavigableComponent : public Component + { + NavigableComponent (int index, int total, CustomNavigationComponent& owner) + { + const auto textColour = Colours::black.withAlpha (0.8f); + + titleLabel.setColour (Label::textColourId, textColour); + orderLabel.setColour (Label::textColourId, textColour); + + const auto setToggleButtonColours = [textColour] (ToggleButton& b) + { + b.setColour (ToggleButton::textColourId, textColour); + b.setColour (ToggleButton::tickDisabledColourId, textColour); + b.setColour (ToggleButton::tickColourId, textColour); + }; + + setToggleButtonColours (focusableToggle); + setToggleButtonColours (defaultToggle); + + const auto title = "Component " + String (index); + setTitle (title); + titleLabel.setText (title, dontSendNotification); + focusableToggle.setToggleState (true, dontSendNotification); + defaultToggle.setToggleState (index == 1, dontSendNotification); + + for (int i = 1; i <= total; ++i) + orderBox.addItem (String (i), i); + + orderBox.setSelectedId (index); + + orderBox.onChange = [this, &owner] { owner.orderChanged (*this); }; + + focusableToggle.onClick = [this] { repaint(); }; + + defaultToggle.onClick = [this, &owner] + { + if (! defaultToggle.getToggleState()) + defaultToggle.setToggleState (true, dontSendNotification); + else + owner.defaultChanged (*this); + }; + + addAndMakeVisible (titleLabel); + + addAndMakeVisible (focusableToggle); + addAndMakeVisible (defaultToggle); + addAndMakeVisible (orderLabel); + addAndMakeVisible (orderBox); + } + + void paint (Graphics& g) override + { + g.fillAll (backgroundColour.withAlpha (focusableToggle.getToggleState() ? 1.0f : 0.5f)); + } + + void resized() override + { + Grid grid; + + grid.templateRows = { Grid::TrackInfo (Grid::Fr (2)), + Grid::TrackInfo (Grid::Fr (3)), + Grid::TrackInfo (Grid::Fr (3)) }; + + grid.templateColumns = { Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)) }; + + grid.items = { GridItem (titleLabel).withMargin (2).withColumn ({ GridItem::Span (4) }), + GridItem (focusableToggle).withMargin (2).withColumn ({ GridItem::Span (2) }), + GridItem (defaultToggle).withMargin (2).withColumn ({ GridItem::Span (2) }), + GridItem (orderLabel).withMargin (2), + GridItem (orderBox).withMargin (2).withColumn ({ GridItem::Span (3) }) }; + + grid.performLayout (getLocalBounds()); + } + + Colour backgroundColour = getRandomBrightColour(); + Label titleLabel; + ToggleButton focusableToggle { "Focusable" }, defaultToggle { "Default" }; + Label orderLabel { {}, "Order" }; + ComboBox orderBox; + }; + + void orderChanged (const NavigableComponent& changedChild) + { + const auto addressMatches = [&] (const std::unique_ptr& child) + { + return child.get() == &changedChild; + }; + + const auto iter = std::find_if (children.begin(), children.end(), addressMatches); + + if (iter != children.end()) + std::swap (*iter, *std::next (children.begin(), changedChild.orderBox.getSelectedItemIndex())); + + int order = 1; + + for (auto& child : children) + child->orderBox.setSelectedId (order++); + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::structureChanged); + } + + void defaultChanged (const NavigableComponent& newDefault) + { + for (auto& child : children) + if (child->defaultToggle.getToggleState() && child.get() != &newDefault) + child->defaultToggle.setToggleState (false, dontSendNotification); + } + + Label descriptionLabel { {}, "This is a demo of how to control the navigation order of components when navigating with an accessibility client.\n\n" + "You can set the order of navigation, whether components are focusable and set a default component which will " + "receive the focus first." }; + + std::vector> children; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomNavigationComponent) +}; + +//============================================================================== +/** + The top-level component containing an example of how to post system announcements. + + The AccessibilityHandler::postAnnouncement() method will post some text to the native + screen reader application to be read out along with a priority determining how + it should be read out (whether it should interrupt other announcements, etc.). +*/ +class AnnouncementsComponent : public Component +{ +public: + AnnouncementsComponent() + { + addAndMakeVisible (descriptionLabel); + + textEntryBox.setMultiLine (true); + textEntryBox.setReturnKeyStartsNewLine (true); + textEntryBox.setText ("Announcement text."); + addAndMakeVisible (textEntryBox); + + priorityComboBox.addItemList ({ "Priority - Low", "Priority - Medium", "Priority - High" }, 1); + priorityComboBox.setSelectedId (2); + addAndMakeVisible (priorityComboBox); + + announceButton.onClick = [this] + { + auto priority = [this] + { + switch (priorityComboBox.getSelectedId()) + { + case 1: return AccessibilityHandler::AnnouncementPriority::low; + case 2: return AccessibilityHandler::AnnouncementPriority::medium; + case 3: return AccessibilityHandler::AnnouncementPriority::high; + } + + jassertfalse; + return AccessibilityHandler::AnnouncementPriority::medium; + }(); + + AccessibilityHandler::postAnnouncement (textEntryBox.getText(), priority); + }; + + addAndMakeVisible (announceButton); + + setTitle ("Announcements"); + setHelpText ("Type some text into the box and click the announce button to have it read out."); + setFocusContainerType (FocusContainerType::focusContainer); + } + + void resized() override + { + Grid grid; + + grid.templateRows = { Grid::TrackInfo (Grid::Fr (3)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)), + Grid::TrackInfo (Grid::Fr (1)) }; + + grid.templateColumns = { Grid::TrackInfo (Grid::Fr (3)), + Grid::TrackInfo (Grid::Fr (2)) }; + + grid.items = { GridItem (descriptionLabel).withMargin (2).withColumn ({ GridItem::Span (2) }), + GridItem (textEntryBox).withMargin (2).withArea ({ 2 }, { 1 }, { 5 }, { 2 }), + GridItem (priorityComboBox).withMargin (2).withArea ({ 5 }, { 1 }, { 6 }, { 2 }), + GridItem (announceButton).withMargin (2).withArea ({ 4 }, { 2 }, { 5 }, { 3 }) }; + + grid.performLayout (getLocalBounds()); + } + +private: + Label descriptionLabel { {}, "This is a demo of posting system announcements that will be read out by an accessibility client.\n\n" + "You can enter some text to be read out in the text box below, set a priority for the message and then " + "post it using the \"Announce\" button." }; + + TextEditor textEntryBox; + ComboBox priorityComboBox; + TextButton announceButton { "Announce" }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnnouncementsComponent) +}; + +//============================================================================== +/** + The main demo content component. + + This just contains a TabbedComponent with a tab for each of the top-level demos. +*/ +class AccessibilityDemo : public Component +{ +public: + AccessibilityDemo() + { + setTitle ("Accessibility Demo"); + setDescription ("A demo of JUCE's accessibility features."); + setFocusContainerType (FocusContainerType::focusContainer); + + tabs.setTitle ("Demo tabs"); + + const auto tabColour = getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker (0.1f); + + tabs.addTab ("JUCE Widgets", tabColour, &juceWidgetsComponent, false); + tabs.addTab ("Custom Widget", tabColour, &customWidgetComponent, false); + tabs.addTab ("Custom Navigation", tabColour, &customNavigationComponent, false); + tabs.addTab ("Announcements", tabColour, &announcementsComponent, false); + addAndMakeVisible (tabs); + + setSize (800, 600); + } + + void paint (Graphics& g) override + { + g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); + } + + void resized() override + { + tabs.setBounds (getLocalBounds()); + } + +private: + TooltipWindow tooltipWindow { nullptr, 100 }; + + TabbedComponent tabs { TabbedButtonBar::Orientation::TabsAtTop }; + + JUCEWidgetsComponent juceWidgetsComponent; + CustomWidgetComponent customWidgetComponent; + CustomNavigationComponent customNavigationComponent; + AnnouncementsComponent announcementsComponent; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityDemo) +};