Add accessibility properties

This commit is contained in:
James Westman 2021-11-12 00:26:23 -06:00
parent d511b3f1e3
commit b776163cd7
No known key found for this signature in database
GPG key ID: CE2DBA0ADB654EA6
20 changed files with 324 additions and 13 deletions

View file

@ -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 {}

View file

@ -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,7 +434,7 @@ 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"]),
@ -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. """

View file

@ -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, ":")]
], ],

View file

@ -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]

View 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))

View file

@ -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}"
)

View file

@ -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()

View file

@ -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:

View file

@ -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,

View file

@ -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"),
) )

View file

@ -0,0 +1,7 @@
using Gtk 4.0;
Widget {
accessibility {
not_a_prop: "Hello, world!";
}
}

View file

@ -0,0 +1 @@
5,5,10,'not_a_prop' is not an accessibility property, relation, or state

View file

@ -0,0 +1,7 @@
using Gtk 4.0;
Widget {
accessibility {
labelled_by: not_an_object;
}
}

View file

@ -0,0 +1 @@
5,18,13,Could not find object with ID not_an_object

View file

@ -0,0 +1,7 @@
using Gtk 4.0;
Widget {
accessibility {
orientation: 1;
}
}

View file

@ -0,0 +1 @@
5,18,1,Cannot convert 1 to Gtk.Orientation

View file

@ -0,0 +1,3 @@
using Gtk 4.0;
NotARealWidget {}

View 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 {}

View 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>

View file

@ -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")