diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index a4c3415..c4438cb 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -17,6 +17,8 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +import typing as T + from ..decompiler import escape_quote from .attributes import BaseAttribute from .common import * @@ -97,6 +99,16 @@ def get_types(gir): } +allow_duplicates = [ + "controls", + "described-by", + "details", + "flow-to", + "labelled-by", + "owns", +] + + def _get_docs(gir, name): name = name.replace("-", "_") if gir_type := ( @@ -111,7 +123,7 @@ class A11yProperty(BaseAttribute): grammar = Statement( UseIdent("name"), ":", - Value, + AnyOf(Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"]), ) @property @@ -132,8 +144,8 @@ class A11yProperty(BaseAttribute): return self.tokens["name"].replace("_", "-") @property - def value(self) -> Value: - return self.children[0] + def values(self) -> T.List[Value]: + return list(self.children) @context(ValueTypeCtx) def value_type(self) -> ValueTypeCtx: @@ -146,7 +158,7 @@ class A11yProperty(BaseAttribute): SymbolKind.Field, self.range, self.group.tokens["name"].range, - self.value.range.text, + ", ".join(v.range.text for v in self.values), ) @validate("name") @@ -165,6 +177,20 @@ class A11yProperty(BaseAttribute): check=lambda child: child.tokens["name"] == self.tokens["name"], ) + @validate("name") + def list_only_allowed_for_subset(self): + if self.tokens["list_form"] and self.tokens["name"] not in allow_duplicates: + raise CompileError( + f"'{self.tokens['name']}' does not allow a list of values", + ) + + @validate("name") + def list_non_empty(self): + if len(self.values) == 0: + raise CompileError( + f"'{self.tokens['name']}' may not be empty", + ) + @docs("name") def prop_docs(self): if self.tokens["name"] in get_types(self.root.gir): diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index a7f4b4b..5e43834 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -292,8 +292,11 @@ class XmlOutput(OutputFormat): def _emit_extensions(self, extension, xml: XmlEmitter): if isinstance(extension, ExtAccessibility): xml.start_tag("accessibility") - for prop in extension.properties: - self._emit_attribute(prop.tag_name, "name", prop.name, prop.value, xml) + for property in extension.properties: + for val in property.values: + self._emit_attribute( + property.tag_name, "name", property.name, val, xml + ) xml.end_tag() elif isinstance(extension, AdwBreakpointCondition): diff --git a/docs/reference/extensions.rst b/docs/reference/extensions.rst index a7b7b93..93ed0fc 100644 --- a/docs/reference/extensions.rst +++ b/docs/reference/extensions.rst @@ -37,12 +37,15 @@ Accessibility Properties .. rst-class:: grammar-block ExtAccessibility = 'accessibility' '{' ExtAccessibilityProp* '}' - ExtAccessibilityProp = `> ':' :ref:`Value ` ';' + ExtAccessibilityProp = `> ':' (:ref:`Value ` | ('[' (:ref: Value ),* ']') ) ';' Valid in any `Gtk.Widget `_. The ``accessibility`` block defines values relevant to accessibility software. The property names and acceptable values are described in the `Gtk.AccessibleRelation `_, `Gtk.AccessibleState `_, and `Gtk.AccessibleProperty `_ enums. +.. note:: + + Relations which allow for a list of values, for example `labelled-by`, must be given as a single relation with a list of values instead of duplicating the relation like done in Gtk.Builder. .. _Syntax ExtAdwBreakpoint: diff --git a/tests/sample_errors/a11y_list_empty.blp b/tests/sample_errors/a11y_list_empty.blp new file mode 100644 index 0000000..401c912 --- /dev/null +++ b/tests/sample_errors/a11y_list_empty.blp @@ -0,0 +1,9 @@ +using Gtk 4.0; + +Box { + accessibility { + label: _("Hello, world!"); + labelled-by: []; + checked: true; + } +} diff --git a/tests/sample_errors/a11y_list_empty.err b/tests/sample_errors/a11y_list_empty.err new file mode 100644 index 0000000..d2b0c86 --- /dev/null +++ b/tests/sample_errors/a11y_list_empty.err @@ -0,0 +1 @@ +6,5,11,'labelled-by' may not be empty diff --git a/tests/sample_errors/a11y_non_list_property.blp b/tests/sample_errors/a11y_non_list_property.blp new file mode 100644 index 0000000..daa3a96 --- /dev/null +++ b/tests/sample_errors/a11y_non_list_property.blp @@ -0,0 +1,15 @@ +using Gtk 4.0; + +Box { + accessibility { + label: _("Hello, world!"); + active-descendant: [my_label1, my_label2, my_label3]; + checked: true; + } +} + +Label my_label1 {} + +Label my_label2 {} + +Label my_label3 {} diff --git a/tests/sample_errors/a11y_non_list_property.err b/tests/sample_errors/a11y_non_list_property.err new file mode 100644 index 0000000..038da92 --- /dev/null +++ b/tests/sample_errors/a11y_non_list_property.err @@ -0,0 +1 @@ +6,5,17,'active-descendant' does not allow a list of values diff --git a/tests/samples/accessibility_multiple_labelled_by.blp b/tests/samples/accessibility_multiple_labelled_by.blp new file mode 100644 index 0000000..78e8b74 --- /dev/null +++ b/tests/samples/accessibility_multiple_labelled_by.blp @@ -0,0 +1,15 @@ +using Gtk 4.0; + +Box { + accessibility { + label: _("Hello, world!"); + labelled-by: [my_label1, my_label2, my_label3]; + checked: true; + } +} + +Label my_label1 {} + +Label my_label2 {} + +Label my_label3 {} diff --git a/tests/samples/accessibility_multiple_labelled_by.ui b/tests/samples/accessibility_multiple_labelled_by.ui new file mode 100644 index 0000000..0e8ab5a --- /dev/null +++ b/tests/samples/accessibility_multiple_labelled_by.ui @@ -0,0 +1,21 @@ + + + + + + + Hello, world! + my_label1 + my_label2 + my_label3 + 1 + + + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 41d572f..b8a3812 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -221,6 +221,8 @@ class TestSamples(unittest.TestCase): "list_factory", # Not implemented yet "subscope", + # Not implemented yet + "accessibility_multiple_labelled_by", ] if sample in REQUIRE_ADW_1_4 and not self.have_adw_1_4: