diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 62f0823..e8c6016 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -47,6 +47,9 @@ class AstNode: self.tokens = ChainMap(tokens, defaultdict(lambda: None)) self.incomplete = incomplete + self._unique_id: str | None = None + self._next_unique_id: int = 0 + self.parent = None for child in self.children: child.parent = self @@ -71,6 +74,13 @@ class AstNode: else: return self.parent.parent_by_type(type) + @property + def unique_id(self): + if self._unique_id is None: + self.root._next_unique_id += 1 + self._unique_id = "__" + str(self.root._next_unique_id) + return self._unique_id + @lazy_prop def errors(self): return list(self._get_errors()) diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index c8889a0..bdd8401 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -41,6 +41,15 @@ class ObjectContent(AstNode): for x in self.children: x.emit_xml(xml) + if self.parent.gir_class and self.parent.gir_class.assignable_to(self.root.gir.get_type("Dialog", "Gtk")): + action_widgets = [widget for widget in self.children if hasattr(widget, "is_action_widget") and widget.is_action_widget] + if len(action_widgets): + xml.start_tag("action-widgets") + for widget in action_widgets: + widget.emit_action_widget(xml) + xml.end_tag() + + class Object(AstNode): grammar: T.Any = [ class_name, @@ -48,6 +57,13 @@ class Object(AstNode): ObjectContent, ] + @property + def id(self): + if self.tokens["id"] is None: + if hasattr(self.parent, "child_needs_id") and self.parent.child_needs_id: + return self.unique_id + return self.tokens["id"] + @validate("namespace") def gir_ns_exists(self): if not self.tokens["ignore_gir"]: @@ -84,7 +100,7 @@ class Object(AstNode): def emit_xml(self, xml: XmlEmitter): xml.start_tag("object", **{ "class": self.gir_class.glib_type_name if self.gir_class else self.tokens["class_name"], - "id": self.tokens["id"], + "id": self.id, }) for child in self.children: child.emit_xml(xml) diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index 68b317f..6200808 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -26,17 +26,44 @@ class Child(AstNode): grammar = [ Optional([ "[", - Optional(["internal-child", UseLiteral("internal_child", True)]), - UseIdent("child_type").expected("a child type"), + AnyOf( + [ + Keyword("action"), + "response", "=", AnyOf(UseNumber("response_id"), UseIdent("response_enum")), + ], + [ + Optional(["internal-child", UseLiteral("internal_child", True)]), + UseIdent("child_type"), + ] + ), "]", ]), Object, ] + @property + def child_needs_id(self): + return self.is_action_widget + + @property + def is_action_widget(self): + return self.tokens["action"] is not None + + @validate("action") + def action_widget(self): + if self.is_action_widget: + parent = self.parent_by_type(Object).gir_class + dialog = self.root.gir.get_type("Dialog", "Gtk") + info_bar = self.root.gir.get_type("InfoBar", "Gtk") + if not (parent is None or parent.assignable_to(dialog) or parent.assignable_to(info_bar)): + raise CompileError(f"Parent type {parent.full_name} does not have action widgets") + def emit_xml(self, xml: XmlEmitter): child_type = internal_child = None if self.tokens["internal_child"]: internal_child = self.tokens["child_type"] + elif self.tokens["action"]: + child_type = "action" else: child_type = self.tokens["child_type"] xml.start_tag("child", type=child_type, internal_child=internal_child) @@ -44,6 +71,24 @@ class Child(AstNode): child.emit_xml(xml) xml.end_tag() + @validate("response_enum") + def valid_response_enum(self): + if response := self.tokens["response_enum"]: + if response not in self.root.gir.get_type("ResponseType", "Gtk").members: + raise CompileError(f"{response} is not a member of Gtk.ResponseType") + + @docs("response_enum") + def response_enum_docs(self): + member = self.root.gir.get_type("ResponseType", "Gtk").members.get(self.tokens["response_enum"]) + if member: + return member.doc + + def emit_action_widget(self, xml: XmlEmitter): + if self.is_action_widget: + xml.start_tag("action-widget", response=self.tokens["response_id"] or self.tokens["response_enum"]) + xml.put_text(self.children[Object][0].id) + xml.end_tag() + @decompiler("child") def decompile_child(ctx, gir, type=None, internal_child=None): diff --git a/tests/sample_errors/action_parent.blp b/tests/sample_errors/action_parent.blp new file mode 100644 index 0000000..fb3f502 --- /dev/null +++ b/tests/sample_errors/action_parent.blp @@ -0,0 +1,6 @@ +using Gtk 4.0; + +Box { + [action response=ok] + Label {} +} diff --git a/tests/sample_errors/action_parent.err b/tests/sample_errors/action_parent.err new file mode 100644 index 0000000..f311efe --- /dev/null +++ b/tests/sample_errors/action_parent.err @@ -0,0 +1 @@ +4,4,6,Parent type Gtk.Box does not have action widgets diff --git a/tests/sample_errors/action_response.blp b/tests/sample_errors/action_response.blp new file mode 100644 index 0000000..1454aec --- /dev/null +++ b/tests/sample_errors/action_response.blp @@ -0,0 +1,6 @@ +using Gtk 4.0; + +Dialog { + [action response=foo] + Button {} +} diff --git a/tests/sample_errors/action_response.err b/tests/sample_errors/action_response.err new file mode 100644 index 0000000..fa7ff4a --- /dev/null +++ b/tests/sample_errors/action_response.err @@ -0,0 +1 @@ +4,20,3,foo is not a member of Gtk.ResponseType diff --git a/tests/samples/actions.blp b/tests/samples/actions.blp new file mode 100644 index 0000000..6ebba09 --- /dev/null +++ b/tests/samples/actions.blp @@ -0,0 +1,8 @@ +using Gtk 4.0; + +Gtk.Dialog { + [action response=1] + Button {} + [action response=cancel] + Button cancel {} +} diff --git a/tests/samples/actions.ui b/tests/samples/actions.ui new file mode 100644 index 0000000..c96fd8c --- /dev/null +++ b/tests/samples/actions.ui @@ -0,0 +1,16 @@ + + + + + + + + + + + + __1 + cancel + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index d0524ef..778efb0 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -111,6 +111,7 @@ class TestSamples(unittest.TestCase): def test_samples(self): self.assert_sample("accessibility") + self.assert_sample("actions") self.assert_sample("binding") self.assert_sample("child_type") self.assert_sample("combo_box_text") @@ -141,6 +142,8 @@ class TestSamples(unittest.TestCase): 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("action_parent") + self.assert_sample_error("action_response") self.assert_sample_error("class_assign") self.assert_sample_error("class_dne") self.assert_sample_error("consecutive_unexpected_tokens")