diff --git a/docs/examples.rst b/docs/examples.rst index 402d4dd..c3100cc 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -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 {} diff --git a/gtkblueprinttool/ast.py b/gtkblueprinttool/ast.py index 4457045..52f0491 100644 --- a/gtkblueprinttool/ast.py +++ b/gtkblueprinttool/ast.py @@ -180,7 +180,7 @@ class Object(AstNode): @property def gir_ns(self): 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 def gir_class(self): @@ -388,6 +388,27 @@ class LiteralValue(Value): def emit_xml(self, xml: XmlEmitter): 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): pass @@ -413,11 +434,11 @@ class IdentValue(Value): ) elif isinstance(type, gir.BoolType): - # would have been parsed as a LiteralValue if it was correct - raise CompileError( - f"Expected 'true' or 'false' for boolean value", - did_you_mean=(self.tokens['value'], ["true", "false"]), - ) + if self.tokens["value"] not in ["true", "false"]: + raise CompileError( + f"Expected 'true' or 'false' for boolean value", + did_you_mean=(self.tokens['value'], ["true", "false"]), + ) elif type is not None: object = self.root.objects_by_id.get(self.tokens["value"]) @@ -468,3 +489,8 @@ class BaseAttribute(AstNode): else: value.emit_xml(xml) xml.end_tag() + + +class BaseTypedAttribute(BaseAttribute): + """ A BaseAttribute whose parent has a value_type property that can assist + in validation. """ diff --git a/gtkblueprinttool/completions.py b/gtkblueprinttool/completions.py index 2e35295..67ddad6 100644 --- a/gtkblueprinttool/completions.py +++ b/gtkblueprinttool/completions.py @@ -103,7 +103,7 @@ def property_completer(ast_node, match_variables): @completer( - applies_in=[ast.Property], + applies_in=[ast.Property, ast.BaseTypedAttribute], matches=[ [(TokenType.IDENT, None), (TokenType.OP, ":")] ], diff --git a/gtkblueprinttool/extensions/__init__.py b/gtkblueprinttool/extensions/__init__.py index 72b4c1b..779bac6 100644 --- a/gtkblueprinttool/extensions/__init__.py +++ b/gtkblueprinttool/extensions/__init__.py @@ -1,10 +1,11 @@ """ Contains all the syntax beyond basic objects, properties, signal, and templates. """ +from .gtk_a11y import a11y from .gtk_menu import menu from .gtk_styles import styles from .gtk_layout import layout OBJECT_HOOKS = [menu] -OBJECT_CONTENT_HOOKS = [styles, layout] +OBJECT_CONTENT_HOOKS = [a11y, styles, layout] diff --git a/gtkblueprinttool/extensions/gtk_a11y.py b/gtkblueprinttool/extensions/gtk_a11y.py new file mode 100644 index 0000000..d9bc57c --- /dev/null +++ b/gtkblueprinttool/extensions/gtk_a11y.py @@ -0,0 +1,185 @@ +# gtk_a11y.py +# +# Copyright 2021 James Westman +# +# 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 . +# +# 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 + 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 + 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 + 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)) diff --git a/gtkblueprinttool/extensions/gtk_layout.py b/gtkblueprinttool/extensions/gtk_layout.py index 150f003..9a821ca 100644 --- a/gtkblueprinttool/extensions/gtk_layout.py +++ b/gtkblueprinttool/extensions/gtk_layout.py @@ -21,6 +21,7 @@ from ..ast import BaseAttribute from ..ast_utils import AstNode from ..completions_utils import * +from ..lsp_utils import Completion, CompletionItemKind from ..parse_tree import * from ..parser_utils import * from ..xml_emitter import XmlEmitter @@ -37,6 +38,11 @@ class Layout(AstNode): class LayoutProperty(BaseAttribute): tag_name = "property" + @property + def value_type(self): + # there isn't really a way to validate these + return None + layout_prop = Group( LayoutProperty, @@ -55,3 +61,14 @@ layout = Group( 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}" + ) diff --git a/gtkblueprinttool/extensions/gtk_menu.py b/gtkblueprinttool/extensions/gtk_menu.py index 2eb35f9..357bc3c 100644 --- a/gtkblueprinttool/extensions/gtk_menu.py +++ b/gtkblueprinttool/extensions/gtk_menu.py @@ -38,6 +38,10 @@ class Menu(AstNode): class MenuAttribute(BaseAttribute): tag_name = "attribute" + @property + def value_type(self): + return None + menu_contents = Sequence() diff --git a/gtkblueprinttool/gir.py b/gtkblueprinttool/gir.py index c59bba7..103df35 100644 --- a/gtkblueprinttool/gir.py +++ b/gtkblueprinttool/gir.py @@ -56,9 +56,11 @@ def get_namespace(namespace, version): class GirType: - pass + @property + def doc(self): + return None -class BasicType: +class BasicType(GirType): name: str = "unknown type" def assignable_to(self, other) -> bool: diff --git a/gtkblueprinttool/lsp_utils.py b/gtkblueprinttool/lsp_utils.py index bd686c1..c14d2fa 100644 --- a/gtkblueprinttool/lsp_utils.py +++ b/gtkblueprinttool/lsp_utils.py @@ -88,7 +88,10 @@ class Completion: "kind": self.kind, "tags": [CompletionItemTag.Deprecated] if self.deprecated else None, "detail": self.signature, - "documentation": self.docs, + "documentation": { + "kind": "markdown", + "value": self.docs, + }, "deprecated": self.deprecated, "insertText": insert_text, "insertTextFormat": insert_text_format, diff --git a/gtkblueprinttool/parser_utils.py b/gtkblueprinttool/parser_utils.py index cfb8075..ce9687d 100644 --- a/gtkblueprinttool/parser_utils.py +++ b/gtkblueprinttool/parser_utils.py @@ -39,8 +39,6 @@ class_name = AnyOf( literal = Group( ast.LiteralValue, AnyOf( - Sequence(Keyword("true"), UseLiteral("value", True)), - Sequence(Keyword("false"), UseLiteral("value", False)), UseNumber("value"), UseQuoted("value"), ) diff --git a/tests/sample_errors/a11y_prop_dne.blp b/tests/sample_errors/a11y_prop_dne.blp new file mode 100644 index 0000000..b17c507 --- /dev/null +++ b/tests/sample_errors/a11y_prop_dne.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +Widget { + accessibility { + not_a_prop: "Hello, world!"; + } +} diff --git a/tests/sample_errors/a11y_prop_dne.err b/tests/sample_errors/a11y_prop_dne.err new file mode 100644 index 0000000..b7a188d --- /dev/null +++ b/tests/sample_errors/a11y_prop_dne.err @@ -0,0 +1 @@ +5,5,10,'not_a_prop' is not an accessibility property, relation, or state diff --git a/tests/sample_errors/a11y_prop_obj_dne.blp b/tests/sample_errors/a11y_prop_obj_dne.blp new file mode 100644 index 0000000..96074fa --- /dev/null +++ b/tests/sample_errors/a11y_prop_obj_dne.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +Widget { + accessibility { + labelled_by: not_an_object; + } +} diff --git a/tests/sample_errors/a11y_prop_obj_dne.err b/tests/sample_errors/a11y_prop_obj_dne.err new file mode 100644 index 0000000..ee66325 --- /dev/null +++ b/tests/sample_errors/a11y_prop_obj_dne.err @@ -0,0 +1 @@ +5,18,13,Could not find object with ID not_an_object diff --git a/tests/sample_errors/a11y_prop_type.blp b/tests/sample_errors/a11y_prop_type.blp new file mode 100644 index 0000000..52eb37b --- /dev/null +++ b/tests/sample_errors/a11y_prop_type.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +Widget { + accessibility { + orientation: 1; + } +} diff --git a/tests/sample_errors/a11y_prop_type.err b/tests/sample_errors/a11y_prop_type.err new file mode 100644 index 0000000..c8cb1d5 --- /dev/null +++ b/tests/sample_errors/a11y_prop_type.err @@ -0,0 +1 @@ +5,18,1,Cannot convert 1 to Gtk.Orientation diff --git a/tests/sample_errors/obj_class_dne.blp b/tests/sample_errors/obj_class_dne.blp new file mode 100644 index 0000000..e28fc94 --- /dev/null +++ b/tests/sample_errors/obj_class_dne.blp @@ -0,0 +1,3 @@ +using Gtk 4.0; + +NotARealWidget {} diff --git a/tests/samples/accessibility.blp b/tests/samples/accessibility.blp new file mode 100644 index 0000000..648b7c3 --- /dev/null +++ b/tests/samples/accessibility.blp @@ -0,0 +1,10 @@ +using Gtk 4.0; + +Gtk.Widget { + accessibility { + label: _("Hello, world!"); + labelled_by: my_label; + checked: true; + } +} +Gtk.Label my_label {} diff --git a/tests/samples/accessibility.ui b/tests/samples/accessibility.ui new file mode 100644 index 0000000..718e24f --- /dev/null +++ b/tests/samples/accessibility.ui @@ -0,0 +1,12 @@ + + + + + + Hello, world! + my_label + true + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index e0813be..cff20c7 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -86,9 +86,13 @@ class TestSamples(unittest.TestCase): diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff)) raise AssertionError() + else: + # Expected a compiler error but there wasn't one + raise AssertionError() def test_samples(self): + self.assert_sample("accessibility") self.assert_sample("binding") self.assert_sample("child_type") self.assert_sample("flags") @@ -106,6 +110,9 @@ class TestSamples(unittest.TestCase): 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_dne") self.assert_sample_error("duplicate_obj_id")