mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-05 16:09:07 -04:00
Add accessibility properties
This commit is contained in:
parent
d511b3f1e3
commit
b776163cd7
20 changed files with 324 additions and 13 deletions
|
@ -261,3 +261,22 @@ Basic Usage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Accessibility Properties
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Basic Usage
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
Gtk.Widget {
|
||||||
|
accessibility {
|
||||||
|
orientation: vertical;
|
||||||
|
labelled_by: my_label;
|
||||||
|
checked: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.Label my_label {}
|
||||||
|
|
|
@ -180,7 +180,7 @@ class Object(AstNode):
|
||||||
@property
|
@property
|
||||||
def gir_ns(self):
|
def gir_ns(self):
|
||||||
if not self.tokens["ignore_gir"]:
|
if not self.tokens["ignore_gir"]:
|
||||||
return self.root.gir.namespaces.get(self.tokens["namespace"])
|
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def gir_class(self):
|
def gir_class(self):
|
||||||
|
@ -388,6 +388,27 @@ class LiteralValue(Value):
|
||||||
def emit_xml(self, xml: XmlEmitter):
|
def emit_xml(self, xml: XmlEmitter):
|
||||||
xml.put_text(self.tokens["value"])
|
xml.put_text(self.tokens["value"])
|
||||||
|
|
||||||
|
@validate()
|
||||||
|
def validate_for_type(self):
|
||||||
|
type = self.parent.value_type
|
||||||
|
if isinstance(type, gir.IntType):
|
||||||
|
try:
|
||||||
|
int(self.tokens["value"])
|
||||||
|
except:
|
||||||
|
raise CompileError(f"Cannot convert {self.tokens['value']} to integer")
|
||||||
|
|
||||||
|
elif isinstance(type, gir.FloatType):
|
||||||
|
try:
|
||||||
|
float(self.tokens["value"])
|
||||||
|
except:
|
||||||
|
raise CompileError(f"Cannot convert {self.tokens['value']} to float")
|
||||||
|
|
||||||
|
elif isinstance(type, gir.StringType):
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif type is not None:
|
||||||
|
raise CompileError(f"Cannot convert {self.tokens['value']} to {type.full_name}")
|
||||||
|
|
||||||
|
|
||||||
class Flag(AstNode):
|
class Flag(AstNode):
|
||||||
pass
|
pass
|
||||||
|
@ -413,11 +434,11 @@ class IdentValue(Value):
|
||||||
)
|
)
|
||||||
|
|
||||||
elif isinstance(type, gir.BoolType):
|
elif isinstance(type, gir.BoolType):
|
||||||
# would have been parsed as a LiteralValue if it was correct
|
if self.tokens["value"] not in ["true", "false"]:
|
||||||
raise CompileError(
|
raise CompileError(
|
||||||
f"Expected 'true' or 'false' for boolean value",
|
f"Expected 'true' or 'false' for boolean value",
|
||||||
did_you_mean=(self.tokens['value'], ["true", "false"]),
|
did_you_mean=(self.tokens['value'], ["true", "false"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
elif type is not None:
|
elif type is not None:
|
||||||
object = self.root.objects_by_id.get(self.tokens["value"])
|
object = self.root.objects_by_id.get(self.tokens["value"])
|
||||||
|
@ -468,3 +489,8 @@ class BaseAttribute(AstNode):
|
||||||
else:
|
else:
|
||||||
value.emit_xml(xml)
|
value.emit_xml(xml)
|
||||||
xml.end_tag()
|
xml.end_tag()
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTypedAttribute(BaseAttribute):
|
||||||
|
""" A BaseAttribute whose parent has a value_type property that can assist
|
||||||
|
in validation. """
|
||||||
|
|
|
@ -103,7 +103,7 @@ def property_completer(ast_node, match_variables):
|
||||||
|
|
||||||
|
|
||||||
@completer(
|
@completer(
|
||||||
applies_in=[ast.Property],
|
applies_in=[ast.Property, ast.BaseTypedAttribute],
|
||||||
matches=[
|
matches=[
|
||||||
[(TokenType.IDENT, None), (TokenType.OP, ":")]
|
[(TokenType.IDENT, None), (TokenType.OP, ":")]
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
""" Contains all the syntax beyond basic objects, properties, signal, and
|
""" Contains all the syntax beyond basic objects, properties, signal, and
|
||||||
templates. """
|
templates. """
|
||||||
|
|
||||||
|
from .gtk_a11y import a11y
|
||||||
from .gtk_menu import menu
|
from .gtk_menu import menu
|
||||||
from .gtk_styles import styles
|
from .gtk_styles import styles
|
||||||
from .gtk_layout import layout
|
from .gtk_layout import layout
|
||||||
|
|
||||||
OBJECT_HOOKS = [menu]
|
OBJECT_HOOKS = [menu]
|
||||||
|
|
||||||
OBJECT_CONTENT_HOOKS = [styles, layout]
|
OBJECT_CONTENT_HOOKS = [a11y, styles, layout]
|
||||||
|
|
185
gtkblueprinttool/extensions/gtk_a11y.py
Normal file
185
gtkblueprinttool/extensions/gtk_a11y.py
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
# gtk_a11y.py
|
||||||
|
#
|
||||||
|
# Copyright 2021 James Westman <james@jwestman.net>
|
||||||
|
#
|
||||||
|
# This file is free software; you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU Lesser General Public License as
|
||||||
|
# published by the Free Software Foundation; either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This file is distributed in the hope that it will be useful, but
|
||||||
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
from ..ast import BaseTypedAttribute, Value
|
||||||
|
from ..ast_utils import AstNode, validate, docs
|
||||||
|
from ..completions_utils import *
|
||||||
|
from ..gir import StringType, BoolType, IntType, FloatType, GirType
|
||||||
|
from ..lsp_utils import Completion, CompletionItemKind
|
||||||
|
from ..parse_tree import *
|
||||||
|
from ..parser_utils import *
|
||||||
|
from ..xml_emitter import XmlEmitter
|
||||||
|
|
||||||
|
|
||||||
|
def _get_property_types(gir):
|
||||||
|
# from <https://docs.gtk.org/gtk4/enum.AccessibleProperty.html>
|
||||||
|
return {
|
||||||
|
"autocomplete": gir.get_type("AccessibleAutocomplete", "Gtk"),
|
||||||
|
"description": StringType(),
|
||||||
|
"has_popup": BoolType(),
|
||||||
|
"key_shortcuts": StringType(),
|
||||||
|
"label": StringType(),
|
||||||
|
"level": IntType(),
|
||||||
|
"modal": BoolType(),
|
||||||
|
"multi_line": BoolType(),
|
||||||
|
"multi_selectable": BoolType(),
|
||||||
|
"orientation": gir.get_type("Orientation", "Gtk"),
|
||||||
|
"placeholder": StringType(),
|
||||||
|
"read_only": BoolType(),
|
||||||
|
"required": BoolType(),
|
||||||
|
"role_description": StringType(),
|
||||||
|
"sort": gir.get_type("AccessibleSort", "Gtk"),
|
||||||
|
"value_max": FloatType(),
|
||||||
|
"value_min": FloatType(),
|
||||||
|
"value_now": FloatType(),
|
||||||
|
"value_text": StringType(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_relation_types(gir):
|
||||||
|
# from <https://docs.gtk.org/gtk4/enum.AccessibleRelation.html>
|
||||||
|
widget = gir.get_type("Widget", "Gtk")
|
||||||
|
return {
|
||||||
|
"active_descendant": widget,
|
||||||
|
"col_count": IntType(),
|
||||||
|
"col_index": IntType(),
|
||||||
|
"col_index_text": StringType(),
|
||||||
|
"col_span": IntType(),
|
||||||
|
"controls": widget,
|
||||||
|
"described_by": widget,
|
||||||
|
"details": widget,
|
||||||
|
"error_message": widget,
|
||||||
|
"flow_to": widget,
|
||||||
|
"labelled_by": widget,
|
||||||
|
"owns": widget,
|
||||||
|
"pos_in_set": IntType(),
|
||||||
|
"row_count": IntType(),
|
||||||
|
"row_index": IntType(),
|
||||||
|
"row_index_text": StringType(),
|
||||||
|
"row_span": IntType(),
|
||||||
|
"set_size": IntType(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_state_types(gir):
|
||||||
|
# from <https://docs.gtk.org/gtk4/enum.AccessibleState.html>
|
||||||
|
return {
|
||||||
|
"busy": BoolType(),
|
||||||
|
"checked": gir.get_type("AccessibleTristate", "Gtk"),
|
||||||
|
"disabled": BoolType(),
|
||||||
|
"expanded": BoolType(),
|
||||||
|
"hidden": BoolType(),
|
||||||
|
"invalid": gir.get_type("AccessibleInvalidState", "Gtk"),
|
||||||
|
"pressed": gir.get_type("AccessibleTristate", "Gtk"),
|
||||||
|
"selected": BoolType(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_types(gir):
|
||||||
|
return {
|
||||||
|
**_get_property_types(gir),
|
||||||
|
**_get_relation_types(gir),
|
||||||
|
**_get_state_types(gir),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_docs(gir, name):
|
||||||
|
return (
|
||||||
|
gir.get_type("AccessibleProperty", "Gtk").members.get(name)
|
||||||
|
or gir.get_type("AccessibleRelation", "Gtk").members.get(name)
|
||||||
|
or gir.get_type("AccessibleState", "Gtk").members.get(name)
|
||||||
|
).doc
|
||||||
|
|
||||||
|
|
||||||
|
class A11y(AstNode):
|
||||||
|
def emit_xml(self, xml: XmlEmitter):
|
||||||
|
xml.start_tag("accessibility")
|
||||||
|
for child in self.children:
|
||||||
|
child.emit_xml(xml)
|
||||||
|
xml.end_tag()
|
||||||
|
|
||||||
|
|
||||||
|
class A11yProperty(BaseTypedAttribute):
|
||||||
|
@property
|
||||||
|
def tag_name(self):
|
||||||
|
name = self.tokens["name"]
|
||||||
|
gir = self.root.gir
|
||||||
|
if name in _get_property_types(gir):
|
||||||
|
return "property"
|
||||||
|
elif name in _get_relation_types(gir):
|
||||||
|
return "relation"
|
||||||
|
elif name in _get_state_types(gir):
|
||||||
|
return "state"
|
||||||
|
else:
|
||||||
|
raise CompilerBugError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value_type(self) -> GirType:
|
||||||
|
return _get_types(self.root.gir).get(self.tokens["name"])
|
||||||
|
|
||||||
|
@validate("name")
|
||||||
|
def is_valid_property(self):
|
||||||
|
types = _get_types(self.root.gir)
|
||||||
|
if self.tokens["name"] not in types:
|
||||||
|
raise CompileError(
|
||||||
|
f"'{self.tokens['name']}' is not an accessibility property, relation, or state",
|
||||||
|
did_you_mean=(self.tokens["name"], types.keys()),
|
||||||
|
)
|
||||||
|
|
||||||
|
@docs("name")
|
||||||
|
def prop_docs(self):
|
||||||
|
if self.tokens["name"] in _get_types(self.root.gir):
|
||||||
|
return _get_docs(self.root.gir, self.tokens["name"])
|
||||||
|
|
||||||
|
|
||||||
|
a11y_prop = Group(
|
||||||
|
A11yProperty,
|
||||||
|
Statement(
|
||||||
|
UseIdent("name"),
|
||||||
|
Op(":"),
|
||||||
|
value.expected("a value"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
a11y = Group(
|
||||||
|
A11y,
|
||||||
|
Sequence(
|
||||||
|
Keyword("accessibility"),
|
||||||
|
OpenBlock().expected("`{`"),
|
||||||
|
Until(a11y_prop, CloseBlock()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@completer(
|
||||||
|
applies_in=[ast.ObjectContent],
|
||||||
|
matches=new_statement_patterns,
|
||||||
|
)
|
||||||
|
def a11y_completer(ast_node, match_variables):
|
||||||
|
yield Completion(
|
||||||
|
"accessibility", CompletionItemKind.Snippet,
|
||||||
|
snippet="accessibility {\n $0\n}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@completer(
|
||||||
|
applies_in=[A11y],
|
||||||
|
matches=new_statement_patterns,
|
||||||
|
)
|
||||||
|
def a11y_name_completer(ast_node, match_variables):
|
||||||
|
for name, type in _get_types(ast_node.root.gir).items():
|
||||||
|
yield Completion(name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type))
|
|
@ -21,6 +21,7 @@
|
||||||
from ..ast import BaseAttribute
|
from ..ast import BaseAttribute
|
||||||
from ..ast_utils import AstNode
|
from ..ast_utils import AstNode
|
||||||
from ..completions_utils import *
|
from ..completions_utils import *
|
||||||
|
from ..lsp_utils import Completion, CompletionItemKind
|
||||||
from ..parse_tree import *
|
from ..parse_tree import *
|
||||||
from ..parser_utils import *
|
from ..parser_utils import *
|
||||||
from ..xml_emitter import XmlEmitter
|
from ..xml_emitter import XmlEmitter
|
||||||
|
@ -37,6 +38,11 @@ class Layout(AstNode):
|
||||||
class LayoutProperty(BaseAttribute):
|
class LayoutProperty(BaseAttribute):
|
||||||
tag_name = "property"
|
tag_name = "property"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value_type(self):
|
||||||
|
# there isn't really a way to validate these
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
layout_prop = Group(
|
layout_prop = Group(
|
||||||
LayoutProperty,
|
LayoutProperty,
|
||||||
|
@ -55,3 +61,14 @@ layout = Group(
|
||||||
Until(layout_prop, CloseBlock()),
|
Until(layout_prop, CloseBlock()),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@completer(
|
||||||
|
applies_in=[ast.ObjectContent],
|
||||||
|
matches=new_statement_patterns,
|
||||||
|
)
|
||||||
|
def layout_completer(ast_node, match_variables):
|
||||||
|
yield Completion(
|
||||||
|
"layout", CompletionItemKind.Snippet,
|
||||||
|
snippet="layout {\n $0\n}"
|
||||||
|
)
|
||||||
|
|
|
@ -38,6 +38,10 @@ class Menu(AstNode):
|
||||||
class MenuAttribute(BaseAttribute):
|
class MenuAttribute(BaseAttribute):
|
||||||
tag_name = "attribute"
|
tag_name = "attribute"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value_type(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
menu_contents = Sequence()
|
menu_contents = Sequence()
|
||||||
|
|
||||||
|
|
|
@ -56,9 +56,11 @@ def get_namespace(namespace, version):
|
||||||
|
|
||||||
|
|
||||||
class GirType:
|
class GirType:
|
||||||
pass
|
@property
|
||||||
|
def doc(self):
|
||||||
|
return None
|
||||||
|
|
||||||
class BasicType:
|
class BasicType(GirType):
|
||||||
name: str = "unknown type"
|
name: str = "unknown type"
|
||||||
|
|
||||||
def assignable_to(self, other) -> bool:
|
def assignable_to(self, other) -> bool:
|
||||||
|
|
|
@ -88,7 +88,10 @@ class Completion:
|
||||||
"kind": self.kind,
|
"kind": self.kind,
|
||||||
"tags": [CompletionItemTag.Deprecated] if self.deprecated else None,
|
"tags": [CompletionItemTag.Deprecated] if self.deprecated else None,
|
||||||
"detail": self.signature,
|
"detail": self.signature,
|
||||||
"documentation": self.docs,
|
"documentation": {
|
||||||
|
"kind": "markdown",
|
||||||
|
"value": self.docs,
|
||||||
|
},
|
||||||
"deprecated": self.deprecated,
|
"deprecated": self.deprecated,
|
||||||
"insertText": insert_text,
|
"insertText": insert_text,
|
||||||
"insertTextFormat": insert_text_format,
|
"insertTextFormat": insert_text_format,
|
||||||
|
|
|
@ -39,8 +39,6 @@ class_name = AnyOf(
|
||||||
literal = Group(
|
literal = Group(
|
||||||
ast.LiteralValue,
|
ast.LiteralValue,
|
||||||
AnyOf(
|
AnyOf(
|
||||||
Sequence(Keyword("true"), UseLiteral("value", True)),
|
|
||||||
Sequence(Keyword("false"), UseLiteral("value", False)),
|
|
||||||
UseNumber("value"),
|
UseNumber("value"),
|
||||||
UseQuoted("value"),
|
UseQuoted("value"),
|
||||||
)
|
)
|
||||||
|
|
7
tests/sample_errors/a11y_prop_dne.blp
Normal file
7
tests/sample_errors/a11y_prop_dne.blp
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
|
||||||
|
Widget {
|
||||||
|
accessibility {
|
||||||
|
not_a_prop: "Hello, world!";
|
||||||
|
}
|
||||||
|
}
|
1
tests/sample_errors/a11y_prop_dne.err
Normal file
1
tests/sample_errors/a11y_prop_dne.err
Normal file
|
@ -0,0 +1 @@
|
||||||
|
5,5,10,'not_a_prop' is not an accessibility property, relation, or state
|
7
tests/sample_errors/a11y_prop_obj_dne.blp
Normal file
7
tests/sample_errors/a11y_prop_obj_dne.blp
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
|
||||||
|
Widget {
|
||||||
|
accessibility {
|
||||||
|
labelled_by: not_an_object;
|
||||||
|
}
|
||||||
|
}
|
1
tests/sample_errors/a11y_prop_obj_dne.err
Normal file
1
tests/sample_errors/a11y_prop_obj_dne.err
Normal file
|
@ -0,0 +1 @@
|
||||||
|
5,18,13,Could not find object with ID not_an_object
|
7
tests/sample_errors/a11y_prop_type.blp
Normal file
7
tests/sample_errors/a11y_prop_type.blp
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
|
||||||
|
Widget {
|
||||||
|
accessibility {
|
||||||
|
orientation: 1;
|
||||||
|
}
|
||||||
|
}
|
1
tests/sample_errors/a11y_prop_type.err
Normal file
1
tests/sample_errors/a11y_prop_type.err
Normal file
|
@ -0,0 +1 @@
|
||||||
|
5,18,1,Cannot convert 1 to Gtk.Orientation
|
3
tests/sample_errors/obj_class_dne.blp
Normal file
3
tests/sample_errors/obj_class_dne.blp
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
|
||||||
|
NotARealWidget {}
|
10
tests/samples/accessibility.blp
Normal file
10
tests/samples/accessibility.blp
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
|
||||||
|
Gtk.Widget {
|
||||||
|
accessibility {
|
||||||
|
label: _("Hello, world!");
|
||||||
|
labelled_by: my_label;
|
||||||
|
checked: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Gtk.Label my_label {}
|
12
tests/samples/accessibility.ui
Normal file
12
tests/samples/accessibility.ui
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk" version="4.0"/>
|
||||||
|
<object class="GtkWidget">
|
||||||
|
<accessibility>
|
||||||
|
<property name="label" translatable="true">Hello, world!</property>
|
||||||
|
<relation name="labelled_by">my_label</relation>
|
||||||
|
<state name="checked">true</state>
|
||||||
|
</accessibility>
|
||||||
|
</object>
|
||||||
|
<object class="GtkLabel" id="my_label"></object>
|
||||||
|
</interface>
|
|
@ -86,9 +86,13 @@ class TestSamples(unittest.TestCase):
|
||||||
diff = difflib.unified_diff(expected.splitlines(), actual.splitlines())
|
diff = difflib.unified_diff(expected.splitlines(), actual.splitlines())
|
||||||
print("\n".join(diff))
|
print("\n".join(diff))
|
||||||
raise AssertionError()
|
raise AssertionError()
|
||||||
|
else:
|
||||||
|
# Expected a compiler error but there wasn't one
|
||||||
|
raise AssertionError()
|
||||||
|
|
||||||
|
|
||||||
def test_samples(self):
|
def test_samples(self):
|
||||||
|
self.assert_sample("accessibility")
|
||||||
self.assert_sample("binding")
|
self.assert_sample("binding")
|
||||||
self.assert_sample("child_type")
|
self.assert_sample("child_type")
|
||||||
self.assert_sample("flags")
|
self.assert_sample("flags")
|
||||||
|
@ -106,6 +110,9 @@ class TestSamples(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_sample_errors(self):
|
def test_sample_errors(self):
|
||||||
|
self.assert_sample_error("a11y_prop_dne")
|
||||||
|
self.assert_sample_error("a11y_prop_obj_dne")
|
||||||
|
self.assert_sample_error("a11y_prop_type")
|
||||||
self.assert_sample_error("class_assign")
|
self.assert_sample_error("class_assign")
|
||||||
self.assert_sample_error("class_dne")
|
self.assert_sample_error("class_dne")
|
||||||
self.assert_sample_error("duplicate_obj_id")
|
self.assert_sample_error("duplicate_obj_id")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue