1478 lines
60 KiB
C
1478 lines
60 KiB
C
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
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, androidstudio, xcode_iphone
|
||
|
|
||
|
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 (MessageBoxIconType::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<ToggleButton, numRadioButtons> radioButtons;
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
RadioButtonsGroupComponent radioButtons;
|
||
|
TextButton textButton { "Press me!" };
|
||
|
ShapeButton shapeButton { "Pressable JUCE Logo",
|
||
|
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<Slider, 3> 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 and iOS, "
|
||
|
"TalkBack on Android, 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<AccessibilityHandler> 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<CustomAccessibilityHandler> (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.isActionEnabled()) result.addAction (AccessibilityActionType::press, [this] { press.onAction(); });
|
||
|
if (toggle.isActionEnabled()) result.addAction (AccessibilityActionType::toggle, [this] { toggle.onAction(); });
|
||
|
if (focus.isActionEnabled()) result.addAction (AccessibilityActionType::focus, [this] { focus.onAction(); });
|
||
|
if (showMenu.isActionEnabled()) 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);
|
||
|
}
|
||
|
|
||
|
bool isActionEnabled() const noexcept { return enabledToggle.getToggleState(); }
|
||
|
|
||
|
private:
|
||
|
void timerCallback() override
|
||
|
{
|
||
|
const auto alpha = [this]
|
||
|
{
|
||
|
const auto progress = static_cast<float> (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<int> 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<AccessibilityValueInterface> 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<Numeric> (*this);
|
||
|
if (valueType == ValueType::ranged) return std::make_unique<Ranged> (*this);
|
||
|
if (valueType == ValueType::text) return std::make_unique<Text> (*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<Property, 12> 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()
|
||
|
{
|
||
|
setTitle ("Custom Navigation");
|
||
|
setDescription ("A demo of custom component navigation.");
|
||
|
setFocusContainerType (FocusContainerType::focusContainer);
|
||
|
|
||
|
addAndMakeVisible (descriptionLabel);
|
||
|
addAndMakeVisible (navigableComponents);
|
||
|
}
|
||
|
|
||
|
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.items = { GridItem (descriptionLabel).withMargin (2),
|
||
|
GridItem (navigableComponents).withMargin (5) };
|
||
|
|
||
|
grid.performLayout (getLocalBounds());
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
//==============================================================================
|
||
|
class NavigableComponentsHolder : public Component
|
||
|
{
|
||
|
public:
|
||
|
NavigableComponentsHolder()
|
||
|
{
|
||
|
setTitle ("Navigable Components");
|
||
|
setDescription ("A container of some navigable components.");
|
||
|
setFocusContainerType (FocusContainerType::focusContainer);
|
||
|
|
||
|
constexpr int numChildren = 12;
|
||
|
|
||
|
for (int i = 1; i <= numChildren; ++i)
|
||
|
{
|
||
|
children.push_back (std::make_unique<NavigableComponent> (i, numChildren, *this));
|
||
|
addAndMakeVisible (*children.back());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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.templateColumns = { Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)), Grid::TrackInfo (Grid::Fr (1)) };
|
||
|
|
||
|
for (auto& child : children)
|
||
|
grid.items.add (GridItem (*child).withMargin (5));
|
||
|
|
||
|
grid.performLayout (getLocalBounds());
|
||
|
}
|
||
|
|
||
|
std::unique_ptr<ComponentTraverser> createFocusTraverser() override
|
||
|
{
|
||
|
struct CustomTraverser : public FocusTraverser
|
||
|
{
|
||
|
explicit CustomTraverser (NavigableComponentsHolder& owner)
|
||
|
: navigableComponentsHolder (owner) {}
|
||
|
|
||
|
Component* getDefaultComponent (Component*) override
|
||
|
{
|
||
|
for (auto& child : navigableComponentsHolder.children)
|
||
|
if (child->defaultToggle.getToggleState() && child->focusableToggle.getToggleState())
|
||
|
return child.get();
|
||
|
|
||
|
return nullptr;
|
||
|
}
|
||
|
|
||
|
Component* getNextComponent (Component* current) override
|
||
|
{
|
||
|
const auto& comps = navigableComponentsHolder.children;
|
||
|
|
||
|
const auto iter = std::find_if (comps.cbegin(), comps.cend(),
|
||
|
[current] (const std::unique_ptr<NavigableComponent>& 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 = navigableComponentsHolder.children;
|
||
|
|
||
|
const auto iter = std::find_if (comps.cbegin(), comps.cend(),
|
||
|
[current] (const std::unique_ptr<NavigableComponent>& c) { return c.get() == current; });
|
||
|
|
||
|
if (iter != comps.cend() && iter != comps.cbegin())
|
||
|
return std::prev (iter)->get();
|
||
|
|
||
|
return nullptr;
|
||
|
}
|
||
|
|
||
|
std::vector<Component*> getAllComponents (Component*) override
|
||
|
{
|
||
|
std::vector<Component*> comps;
|
||
|
|
||
|
for (auto& child : navigableComponentsHolder.children)
|
||
|
if (child->focusableToggle.getToggleState())
|
||
|
comps.push_back (child.get());
|
||
|
|
||
|
return comps;
|
||
|
}
|
||
|
|
||
|
NavigableComponentsHolder& navigableComponentsHolder;
|
||
|
};
|
||
|
|
||
|
return std::make_unique<CustomTraverser> (*this);
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
struct NavigableComponent : public Component
|
||
|
{
|
||
|
NavigableComponent (int index, int total, NavigableComponentsHolder& 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);
|
||
|
|
||
|
setFocusContainerType (FocusContainerType::focusContainer);
|
||
|
}
|
||
|
|
||
|
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<NavigableComponent>& 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);
|
||
|
}
|
||
|
|
||
|
std::vector<std::unique_ptr<NavigableComponent>> children;
|
||
|
|
||
|
//==============================================================================
|
||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NavigableComponentsHolder)
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
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." };
|
||
|
|
||
|
NavigableComponentsHolder navigableComponents;
|
||
|
|
||
|
//==============================================================================
|
||
|
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)
|
||
|
};
|