mirror of
https://github.com/slint-ui/slint.git
synced 2025-09-29 05:14:48 +00:00

Implement basic accessibility (a11y) support, using the Qt backend. _This should get us started, but accessibility support is an additional way to interact with UIs that is very different from the "graphical way" most users will interact with the UI. No single PR will "make a toolkit accessibility", this needs to be an ongoing effort!_ Parts of this PR: * Add functions to access a11y-related properties to Component * Add helper functions to Item struct * Handle accessible- properties in the compiler * Add documentation, add description, enforce some basic rules * Make the Text element accessible by default * Don't optimize away accessibility property in the LLR * Ensure that accessibility property are marked as used * Add some accessibility properties to the native style widgets * Support for bool and integer `accessible` properties * Implement basic support for accessibility * Make basic widgets accessible by default * Make slider focus-able and interactable with keyboard * Tell a11y layer about value changes * Generate QAccessible constants using bindgen * Don't expose the `accessible` properties when using the MCU backend: There is no backend to make use of them * Handle focus change based on keyboard focus of the window * Report accessible widgets at correct positions * Allow for (virtual) focus delegation at the a11y level * Calculate value step size dynamically * Make sure to not send notifications to a11y backend about dead objects
245 lines
7.4 KiB
Text
245 lines
7.4 KiB
Text
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
|
|
|
|
// cSpell: ignore combobox spinbox
|
|
|
|
import { LineEditInner, TextEdit, AboutSlint } from "../common/common.slint";
|
|
import { StyleMetrics, ScrollView } from "std-widgets-impl.slint";
|
|
export { StyleMetrics, ScrollView, TextEdit, AboutSlint, AboutSlint as AboutSixtyFPS }
|
|
|
|
// FIXME: the font-size should be removed but is required right now to compile the printer-demo
|
|
export Button := NativeButton {
|
|
property<length> font-size;
|
|
accessible-label <=> text;
|
|
accessible-role: button;
|
|
enabled: true;
|
|
}
|
|
|
|
export StandardButton := NativeButton {
|
|
property<StandardButtonKind> kind <=> self.standard-button-kind;
|
|
is-standard-button: true;
|
|
}
|
|
|
|
export CheckBox := NativeCheckBox {
|
|
accessible-role: checkbox;
|
|
accessible-label <=> text;
|
|
accessible-checked <=> checked;
|
|
}
|
|
export SpinBox := NativeSpinBox {
|
|
property<length> font-size;
|
|
|
|
accessible-role: spinbox;
|
|
accessible-value: value;
|
|
accessible-value-minimum: minimum;
|
|
accessible-value-maximum: maximum;
|
|
accessible-value-step: (maximum - minimum) / 100;
|
|
|
|
}
|
|
|
|
export Slider := NativeSlider {
|
|
accessible-role: slider;
|
|
accessible-value: value;
|
|
accessible-value-minimum: minimum;
|
|
accessible-value-maximum: maximum;
|
|
accessible-value-step: (maximum - minimum) / 100;
|
|
|
|
property <bool> has-focus <=> fs.has-focus;
|
|
|
|
fs := FocusScope {
|
|
width: 0px;
|
|
|
|
key-pressed(event) => {
|
|
if (root.enabled && event.text == Keys.RightArrow) {
|
|
root.value = Math.min(root.value + 1, root.maximum);
|
|
accept
|
|
} else if (root.enabled && event.text == Keys.LeftArrow) {
|
|
root.value = Math.max(root.value - 1, root.minimum);
|
|
accept
|
|
} else {
|
|
reject
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
export GroupBox := NativeGroupBox {
|
|
GridLayout {
|
|
padding-left: root.native-padding-left;
|
|
padding-right: root.native-padding-right;
|
|
padding-top: root.native-padding-top;
|
|
padding-bottom: root.native-padding-bottom;
|
|
@children
|
|
}
|
|
}
|
|
|
|
export LineEdit := NativeLineEdit {
|
|
property <length> font-size <=> inner.font-size;
|
|
property <string> text <=> inner.text;
|
|
property <string> placeholder-text <=> inner.placeholder-text;
|
|
property input-type <=> inner.input-type;
|
|
property horizontal-alignment <=> inner.horizontal-alignment;
|
|
property read-only <=> inner.read-only;
|
|
enabled: true;
|
|
has-focus <=> inner.has-focus;
|
|
forward-focus: inner;
|
|
callback accepted <=> inner.accepted;
|
|
callback edited <=> inner.edited;
|
|
horizontal-stretch: 1;
|
|
vertical-stretch: 0;
|
|
|
|
HorizontalLayout {
|
|
padding-left: root.native-padding-left;
|
|
padding-right: root.native-padding-right;
|
|
padding-top: root.native-padding-top;
|
|
padding-bottom: root.native-padding-bottom;
|
|
inner := LineEditInner {
|
|
placeholder-color: enabled ? StyleMetrics.placeholder-color : StyleMetrics.placeholder-color-disabled;
|
|
enabled <=> root.enabled;
|
|
}
|
|
}
|
|
}
|
|
|
|
export ListView := ScrollView {
|
|
@children
|
|
}
|
|
|
|
export StandardListView := ListView {
|
|
property<[StandardListViewItem]> model;
|
|
property<int> current-item: -1;
|
|
for item[i] in model : NativeStandardListViewItem {
|
|
item: item;
|
|
index: i;
|
|
is-selected: current-item == i;
|
|
TouchArea {
|
|
clicked => { current-item = i; }
|
|
has-hover <=> parent.has-hover;
|
|
}
|
|
}
|
|
FocusScope {
|
|
key-pressed(event) => {
|
|
if (event.text == Keys.UpArrow && current-item > 0) {
|
|
current-item -= 1;
|
|
accept
|
|
} else if (event.text == Keys.DownArrow && current-item + 1 < model.length) {
|
|
current-item += 1;
|
|
accept
|
|
} else {
|
|
reject
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export ComboBox := NativeComboBox {
|
|
property <[string]> model;
|
|
property <int> current-index : -1;
|
|
enabled: true;
|
|
open-popup => { popup.show(); }
|
|
callback selected(string);
|
|
|
|
accessible-role: combobox;
|
|
accessible-value <=> current-value;
|
|
|
|
popup := PopupWindow {
|
|
Rectangle { background: NativeStyleMetrics.window-background; }
|
|
NativeComboBoxPopup {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
y: root.height;
|
|
width: root.width;
|
|
VerticalLayout {
|
|
spacing: 0px;
|
|
for value[i] in root.model: NativeStandardListViewItem {
|
|
item: { text: value };
|
|
is-selected: current-index == i;
|
|
TouchArea {
|
|
has-hover <=> parent.has-hover;
|
|
clicked => {
|
|
if (root.enabled) {
|
|
current-index = i;
|
|
current-value = value;
|
|
selected(current-value);
|
|
}
|
|
//is-open = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fs := FocusScope {
|
|
key-pressed(event) => {
|
|
if (event.text == Keys.UpArrow) {
|
|
root.current-index = Math.max(root.current-index - 1, 0);
|
|
root.current-value = model[root.current-index];
|
|
return accept;
|
|
} else if (event.text == Keys.DownArrow) {
|
|
root.current-index = Math.min(root.current-index + 1, root.model.length - 1);
|
|
root.current-value = model[root.current-index];
|
|
return accept;
|
|
// PopupWindow can not get hidden again at this time, so do not allow to pop that up.
|
|
// } else if (event.text == Keys.Return) {
|
|
// touch.clicked()
|
|
// return accept;
|
|
}
|
|
return reject;
|
|
}
|
|
}
|
|
}
|
|
|
|
export TabWidgetImpl := NativeTabWidget { }
|
|
|
|
export TabImpl := NativeTab {
|
|
accessible-role: tab;
|
|
accessible-label <=> title;
|
|
}
|
|
|
|
export TabBarImpl := Rectangle {
|
|
// injected properties:
|
|
property<int> current; // The currently selected tab
|
|
property<int> current-focused: fs.has-focus ? current : -1; // The currently focused tab
|
|
property<int> num-tabs; // The total number of tabs
|
|
|
|
accessible-role: tab;
|
|
accessible-delegate-focus: current;
|
|
|
|
HorizontalLayout {
|
|
spacing: 0px; // Qt renders Tabs next to each other and renders "spacing" as part of the tab itself
|
|
alignment: NativeStyleMetrics.tab-bar-alignment;
|
|
@children
|
|
}
|
|
|
|
fs := FocusScope {
|
|
width: 0px; // Do not react on clicks
|
|
key-pressed(event) => {
|
|
if (event.text == Keys.LeftArrow) {
|
|
root.current = Math.max(root.current - 1, 0);
|
|
return accept;
|
|
}
|
|
if (event.text == Keys.RightArrow) {
|
|
root.current = Math.min(root.current + 1, num-tabs - 1);
|
|
return accept;
|
|
}
|
|
return reject;
|
|
}
|
|
}
|
|
}
|
|
|
|
export TabWidget := TabWidget {}
|
|
|
|
export VerticalBox := VerticalLayout {
|
|
spacing: NativeStyleMetrics.layout-spacing;
|
|
padding: NativeStyleMetrics.layout-spacing;
|
|
}
|
|
|
|
export HorizontalBox := HorizontalLayout {
|
|
spacing: NativeStyleMetrics.layout-spacing;
|
|
padding: NativeStyleMetrics.layout-spacing;
|
|
}
|
|
|
|
export GridBox := GridLayout {
|
|
spacing: NativeStyleMetrics.layout-spacing;
|
|
padding: NativeStyleMetrics.layout-spacing;
|
|
}
|