From a12ac1b976807dd553e99ca87c678b75cf302002 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 24 Aug 2024 12:21:32 -0500 Subject: [PATCH] decompiler: Support Adw.Breakpoint syntax Also, improve handling of translated strings. --- blueprintcompiler/decompiler.py | 96 ++++++++++++++----- blueprintcompiler/language/adw_breakpoint.py | 39 ++++++++ blueprintcompiler/language/expression.py | 3 +- blueprintcompiler/language/gtk_a11y.py | 26 ++--- .../language/gtkbuilder_template.py | 2 - tests/samples/adw_breakpoint.blp | 7 +- tests/samples/adw_breakpoint.ui | 2 +- tests/samples/adw_breakpoint_template_dec.blp | 10 ++ tests/test_samples.py | 4 - 9 files changed, 141 insertions(+), 48 deletions(-) create mode 100644 tests/samples/adw_breakpoint_template_dec.blp diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index f5c45fb..81072d9 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -56,7 +56,6 @@ class DecompileCtx: self.gir = GirContext() self._blocks_need_end: T.List[str] = [] self._last_line_type: LineType = LineType.NONE - self.template_class: T.Optional[str] = None self._obj_type_stack: list[T.Optional[GirType]] = [] self._node_stack: list[Element] = [] @@ -112,6 +111,45 @@ class DecompileCtx: else: return self._node_stack[-1] + @property + def parent_node(self) -> T.Optional[Element]: + if len(self._node_stack) < 2: + return None + else: + return self._node_stack[-2] + + @property + def root_node(self) -> T.Optional[Element]: + if len(self._node_stack) == 0: + return None + else: + return self._node_stack[0] + + @property + def template_class(self) -> T.Optional[str]: + assert self.root_node is not None + for child in self.root_node.children: + if child.tag == "template": + return child["class"] + return None + + def find_object(self, id: str) -> T.Optional[Element]: + assert self.root_node is not None + for child in self.root_node.children: + if child.tag == "template" and child["class"] == id: + return child + + def find_in_children(node: Element) -> T.Optional[Element]: + if node.tag in ["object", "menu"] and node["id"] == id: + return node + else: + for child in node.children: + if result := find_in_children(child): + return result + return None + + return find_in_children(self.root_node) + def end_block_with(self, text: str) -> None: self._blocks_need_end[-1] = text @@ -122,7 +160,15 @@ class DecompileCtx: if len(self._blocks_need_end): self._blocks_need_end[-1] = _CLOSING[line[-1]] - def print_value(self, value: str, type: T.Optional[GirType]) -> None: + # Converts a value from an XML element to a blueprint string + # based on the given type. Returns a tuple of translator comments + # (if any) and the decompiled syntax. + def decompile_value( + self, + value: str, + type: T.Optional[GirType], + translatable: T.Optional[T.Tuple[str, str, str]] = None, + ) -> T.Tuple[str, str]: def get_enum_name(value): for member in type.members.values(): if ( @@ -133,16 +179,18 @@ class DecompileCtx: return member.name return value.replace("-", "_") - if type is None: - self.print(f"{escape_quote(value)}") + if translatable is not None and truthy(translatable[0]): + return decompile_translatable(value, *translatable) + elif type is None: + return "", f"{escape_quote(value)}" elif type.assignable_to(FloatType()): - self.print(str(value)) + return "", str(value) elif type.assignable_to(BoolType()): val = truthy(value) - self.print("true" if val else "false") + return "", ("true" if val else "false") elif type.assignable_to(ArrayType(StringType())): items = ", ".join([escape_quote(x) for x in value.split("\n")]) - self.print(f"[{items}]") + return "", f"[{items}]" elif ( type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf")) or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture")) @@ -156,30 +204,25 @@ class DecompileCtx: self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger") ) ): - self.print(f"{escape_quote(value)}") + return "", escape_quote(value) elif value == self.template_class: - self.print("template") + return "", "template" elif type.assignable_to( self.gir.namespaces["Gtk"].lookup_type("GObject.Object") ) or isinstance(type, Interface): - self.print(value) + return "", ("null" if value == "" else value) elif isinstance(type, Bitfield): flags = [get_enum_name(flag) for flag in value.split("|")] - self.print(" | ".join(flags)) + return "", " | ".join(flags) elif isinstance(type, Enumeration): - self.print(get_enum_name(value)) + return "", get_enum_name(value) elif isinstance(type, TypeType): if t := self.type_by_cname(value): - self.print(f"typeof<{full_name(t)}>") + return "", f"typeof<{full_name(t)}>" else: - self.print(f"typeof<${value}>") + return "", f"typeof<${value}>" else: - self.print(f"{escape_quote(value)}") - - def print_attribute(self, name: str, value: str, type: GirType) -> None: - self.print(f"{name}: ") - self.print_value(value, type) - self.print(";") + return "", escape_quote(value) def decompile_element( @@ -247,7 +290,7 @@ def canon(string: str) -> str: def truthy(string: str) -> bool: - return string.lower() in ["yes", "true", "t", "y", "1"] + return string is not None and string.lower() in ["yes", "true", "t", "y", "1"] def full_name(gir: GirType) -> str: @@ -318,9 +361,11 @@ def decompile_translatable( translatable: T.Optional[str], context: T.Optional[str], comments: T.Optional[str], -) -> T.Tuple[T.Optional[str], str]: +) -> T.Tuple[str, str]: if translatable is not None and truthy(translatable): - if comments is not None: + if comments is None: + comments = "" + else: comments = comments.replace("/*", " ").replace("*/", " ") comments = f"/* Translators: {comments} */" @@ -329,7 +374,7 @@ def decompile_translatable( else: return comments, f"_({escape_quote(string)})" else: - return comments, f"{escape_quote(string)}" + return "", f"{escape_quote(string)}" @decompiler("property", cdata=True) @@ -376,7 +421,8 @@ def decompile_property( elif gir is None or gir.properties.get(name) is None: ctx.print(f"{name}: {escape_quote(cdata)};") else: - ctx.print_attribute(name, cdata, gir.properties.get(name).type) + _, string = ctx.decompile_value(cdata, gir.properties.get(name).type) + ctx.print(f"{name}: {string};") return gir diff --git a/blueprintcompiler/language/adw_breakpoint.py b/blueprintcompiler/language/adw_breakpoint.py index 4927c11..eec7c7e 100644 --- a/blueprintcompiler/language/adw_breakpoint.py +++ b/blueprintcompiler/language/adw_breakpoint.py @@ -195,3 +195,42 @@ class AdwBreakpointSetters(AstNode): @validate() def unique(self): self.validate_unique_in_parent("Duplicate setters block") + + +@decompiler("condition", cdata=True) +def decompile_condition(ctx: DecompileCtx, gir, cdata): + ctx.print(f"condition({escape_quote(cdata)})") + + +@decompiler("setter", element=True) +def decompile_setter(ctx: DecompileCtx, gir, element): + assert ctx.parent_node is not None + # only run for the first setter + for child in ctx.parent_node.children: + if child.tag == "setter": + if child != element: + # already decompiled + return + else: + break + + ctx.print("setters {") + for child in ctx.parent_node.children: + if child.tag == "setter": + object_id = child["object"] + property_name = child["property"] + obj = ctx.find_object(object_id) + if obj is not None: + gir_class = ctx.type_by_cname(obj["class"]) + else: + gir_class = None + + if object_id == ctx.template_class: + object_id = "template" + + comments, string = ctx.decompile_value( + child.cdata, + gir_class, + (child["translatable"], child["context"], child["comments"]), + ) + ctx.print(f"{comments} {object_id}.{property_name}: {string};") diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index 117fd30..37e73e8 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -319,7 +319,8 @@ def decompile_constant( else: ctx.print(cdata) else: - ctx.print_value(cdata, ctx.type_by_cname(type)) + _, string = ctx.decompile_value(cdata, ctx.type_by_cname(type)) + ctx.print(string) @decompiler("closure", skip_children=True) diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index ec49002..8870a74 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -249,17 +249,12 @@ def a11y_name_completer(lsp, ast_node, match_variables): ) -def decompile_attr(ctx: DecompileCtx, attr): - if attr["translatable"] is not None and decompile.truthy(attr["translatable"]): - ctx.print(f"_({escape_quote(attr.cdata)})") - else: - ctx.print_value(attr.cdata, get_types(ctx.gir).get(attr["name"])) - - @decompiler("accessibility", skip_children=True, element=True) def decompile_accessibility(ctx: DecompileCtx, _gir, element): ctx.print("accessibility {") already_printed = set() + types = get_types(ctx.gir) + for child in element.children: name = child["name"] @@ -270,13 +265,20 @@ def decompile_accessibility(ctx: DecompileCtx, _gir, element): ctx.print(f"{name}: [") for value in element.children: if value["name"] == name: - decompile_attr(ctx, value) - ctx.print(", ") + comments, string = ctx.decompile_value( + value.cdata, + types.get(value["name"]), + (value["translatable"], value["context"], value["comments"]), + ) + ctx.print(f"{comments} {string},") ctx.print("];") else: - ctx.print(f"{name}:") - decompile_attr(ctx, child) - ctx.print(";") + comments, string = ctx.decompile_value( + child.cdata, + types.get(child["name"]), + (child["translatable"], child["context"], child["comments"]), + ) + ctx.print(f"{comments} {name}: {string};") already_printed.add(name) ctx.print("}") diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index be8473c..29f7b37 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -102,6 +102,4 @@ def decompile_template(ctx: DecompileCtx, gir, klass, parent=None): else: ctx.print(f"template {class_name(klass)} : {class_name(parent)} {{") - ctx.template_class = klass - return ctx.type_by_cname(klass) or ctx.type_by_cname(parent) diff --git a/tests/samples/adw_breakpoint.blp b/tests/samples/adw_breakpoint.blp index 1d085e4..dfdfd9f 100644 --- a/tests/samples/adw_breakpoint.blp +++ b/tests/samples/adw_breakpoint.blp @@ -1,13 +1,14 @@ using Gtk 4.0; using Adw 1; -Gtk.Label label {} +Label label {} Adw.Breakpoint { condition ("max-width: 600px") + setters { - label.label: _("Hello, world!"); + label.label: C_("test", "Hello, world!"); label.visible: false; label.extra-menu: null; } -} \ No newline at end of file +} diff --git a/tests/samples/adw_breakpoint.ui b/tests/samples/adw_breakpoint.ui index 29cbf9b..8b7dc0e 100644 --- a/tests/samples/adw_breakpoint.ui +++ b/tests/samples/adw_breakpoint.ui @@ -9,7 +9,7 @@ corresponding .blp file and regenerate this file with blueprint-compiler. max-width: 600px - Hello, world! + Hello, world! false diff --git a/tests/samples/adw_breakpoint_template_dec.blp b/tests/samples/adw_breakpoint_template_dec.blp new file mode 100644 index 0000000..dc8afb2 --- /dev/null +++ b/tests/samples/adw_breakpoint_template_dec.blp @@ -0,0 +1,10 @@ +using Gtk 4.0; +using Adw 1; + +template $MyTemplate { + Adw.Breakpoint { + setters { + template.some-prop: "true"; + } + } +} diff --git a/tests/test_samples.py b/tests/test_samples.py index 41d572f..11fe6f8 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -210,10 +210,6 @@ class TestSamples(unittest.TestCase): # Not implemented yet "action_widgets", # Not implemented yet - "adw_breakpoint", - # Not implemented yet - "adw_breakpoint_template", - # Not implemented yet "gtkcolumnview", # Comments are not preserved in either direction "comments",