decompiler: Support Adw.Breakpoint syntax

Also, improve handling of translated strings.
This commit is contained in:
James Westman 2024-08-24 12:21:32 -05:00
parent 078ce2f5b8
commit a12ac1b976
9 changed files with 141 additions and 48 deletions

View file

@ -56,7 +56,6 @@ class DecompileCtx:
self.gir = GirContext() self.gir = GirContext()
self._blocks_need_end: T.List[str] = [] self._blocks_need_end: T.List[str] = []
self._last_line_type: LineType = LineType.NONE self._last_line_type: LineType = LineType.NONE
self.template_class: T.Optional[str] = None
self._obj_type_stack: list[T.Optional[GirType]] = [] self._obj_type_stack: list[T.Optional[GirType]] = []
self._node_stack: list[Element] = [] self._node_stack: list[Element] = []
@ -112,6 +111,45 @@ class DecompileCtx:
else: else:
return self._node_stack[-1] 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: def end_block_with(self, text: str) -> None:
self._blocks_need_end[-1] = text self._blocks_need_end[-1] = text
@ -122,7 +160,15 @@ class DecompileCtx:
if len(self._blocks_need_end): if len(self._blocks_need_end):
self._blocks_need_end[-1] = _CLOSING[line[-1]] 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): def get_enum_name(value):
for member in type.members.values(): for member in type.members.values():
if ( if (
@ -133,16 +179,18 @@ class DecompileCtx:
return member.name return member.name
return value.replace("-", "_") return value.replace("-", "_")
if type is None: if translatable is not None and truthy(translatable[0]):
self.print(f"{escape_quote(value)}") return decompile_translatable(value, *translatable)
elif type is None:
return "", f"{escape_quote(value)}"
elif type.assignable_to(FloatType()): elif type.assignable_to(FloatType()):
self.print(str(value)) return "", str(value)
elif type.assignable_to(BoolType()): elif type.assignable_to(BoolType()):
val = truthy(value) val = truthy(value)
self.print("true" if val else "false") return "", ("true" if val else "false")
elif type.assignable_to(ArrayType(StringType())): elif type.assignable_to(ArrayType(StringType())):
items = ", ".join([escape_quote(x) for x in value.split("\n")]) items = ", ".join([escape_quote(x) for x in value.split("\n")])
self.print(f"[{items}]") return "", f"[{items}]"
elif ( elif (
type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf")) type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture")) 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.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger")
) )
): ):
self.print(f"{escape_quote(value)}") return "", escape_quote(value)
elif value == self.template_class: elif value == self.template_class:
self.print("template") return "", "template"
elif type.assignable_to( elif type.assignable_to(
self.gir.namespaces["Gtk"].lookup_type("GObject.Object") self.gir.namespaces["Gtk"].lookup_type("GObject.Object")
) or isinstance(type, Interface): ) or isinstance(type, Interface):
self.print(value) return "", ("null" if value == "" else value)
elif isinstance(type, Bitfield): elif isinstance(type, Bitfield):
flags = [get_enum_name(flag) for flag in value.split("|")] flags = [get_enum_name(flag) for flag in value.split("|")]
self.print(" | ".join(flags)) return "", " | ".join(flags)
elif isinstance(type, Enumeration): elif isinstance(type, Enumeration):
self.print(get_enum_name(value)) return "", get_enum_name(value)
elif isinstance(type, TypeType): elif isinstance(type, TypeType):
if t := self.type_by_cname(value): if t := self.type_by_cname(value):
self.print(f"typeof<{full_name(t)}>") return "", f"typeof<{full_name(t)}>"
else: else:
self.print(f"typeof<${value}>") return "", f"typeof<${value}>"
else: else:
self.print(f"{escape_quote(value)}") return "", 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(";")
def decompile_element( def decompile_element(
@ -247,7 +290,7 @@ def canon(string: str) -> str:
def truthy(string: str) -> bool: 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: def full_name(gir: GirType) -> str:
@ -318,9 +361,11 @@ def decompile_translatable(
translatable: T.Optional[str], translatable: T.Optional[str],
context: T.Optional[str], context: T.Optional[str],
comments: 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 translatable is not None and truthy(translatable):
if comments is not None: if comments is None:
comments = ""
else:
comments = comments.replace("/*", " ").replace("*/", " ") comments = comments.replace("/*", " ").replace("*/", " ")
comments = f"/* Translators: {comments} */" comments = f"/* Translators: {comments} */"
@ -329,7 +374,7 @@ def decompile_translatable(
else: else:
return comments, f"_({escape_quote(string)})" return comments, f"_({escape_quote(string)})"
else: else:
return comments, f"{escape_quote(string)}" return "", f"{escape_quote(string)}"
@decompiler("property", cdata=True) @decompiler("property", cdata=True)
@ -376,7 +421,8 @@ def decompile_property(
elif gir is None or gir.properties.get(name) is None: elif gir is None or gir.properties.get(name) is None:
ctx.print(f"{name}: {escape_quote(cdata)};") ctx.print(f"{name}: {escape_quote(cdata)};")
else: 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 return gir

View file

@ -195,3 +195,42 @@ class AdwBreakpointSetters(AstNode):
@validate() @validate()
def unique(self): def unique(self):
self.validate_unique_in_parent("Duplicate setters block") 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};")

View file

@ -319,7 +319,8 @@ def decompile_constant(
else: else:
ctx.print(cdata) ctx.print(cdata)
else: 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) @decompiler("closure", skip_children=True)

View file

@ -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) @decompiler("accessibility", skip_children=True, element=True)
def decompile_accessibility(ctx: DecompileCtx, _gir, element): def decompile_accessibility(ctx: DecompileCtx, _gir, element):
ctx.print("accessibility {") ctx.print("accessibility {")
already_printed = set() already_printed = set()
types = get_types(ctx.gir)
for child in element.children: for child in element.children:
name = child["name"] name = child["name"]
@ -270,13 +265,20 @@ def decompile_accessibility(ctx: DecompileCtx, _gir, element):
ctx.print(f"{name}: [") ctx.print(f"{name}: [")
for value in element.children: for value in element.children:
if value["name"] == name: if value["name"] == name:
decompile_attr(ctx, value) comments, string = ctx.decompile_value(
ctx.print(", ") value.cdata,
types.get(value["name"]),
(value["translatable"], value["context"], value["comments"]),
)
ctx.print(f"{comments} {string},")
ctx.print("];") ctx.print("];")
else: else:
ctx.print(f"{name}:") comments, string = ctx.decompile_value(
decompile_attr(ctx, child) child.cdata,
ctx.print(";") types.get(child["name"]),
(child["translatable"], child["context"], child["comments"]),
)
ctx.print(f"{comments} {name}: {string};")
already_printed.add(name) already_printed.add(name)
ctx.print("}") ctx.print("}")

View file

@ -102,6 +102,4 @@ def decompile_template(ctx: DecompileCtx, gir, klass, parent=None):
else: else:
ctx.print(f"template {class_name(klass)} : {class_name(parent)} {{") 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) return ctx.type_by_cname(klass) or ctx.type_by_cname(parent)

View file

@ -1,12 +1,13 @@
using Gtk 4.0; using Gtk 4.0;
using Adw 1; using Adw 1;
Gtk.Label label {} Label label {}
Adw.Breakpoint { Adw.Breakpoint {
condition ("max-width: 600px") condition ("max-width: 600px")
setters { setters {
label.label: _("Hello, world!"); label.label: C_("test", "Hello, world!");
label.visible: false; label.visible: false;
label.extra-menu: null; label.extra-menu: null;
} }

View file

@ -9,7 +9,7 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
<object class="GtkLabel" id="label"></object> <object class="GtkLabel" id="label"></object>
<object class="AdwBreakpoint"> <object class="AdwBreakpoint">
<condition>max-width: 600px</condition> <condition>max-width: 600px</condition>
<setter object="label" property="label" translatable="yes">Hello, world!</setter> <setter object="label" property="label" translatable="yes" context="test">Hello, world!</setter>
<setter object="label" property="visible">false</setter> <setter object="label" property="visible">false</setter>
<setter object="label" property="extra-menu"></setter> <setter object="label" property="extra-menu"></setter>
</object> </object>

View file

@ -0,0 +1,10 @@
using Gtk 4.0;
using Adw 1;
template $MyTemplate {
Adw.Breakpoint {
setters {
template.some-prop: "true";
}
}
}

View file

@ -210,10 +210,6 @@ class TestSamples(unittest.TestCase):
# Not implemented yet # Not implemented yet
"action_widgets", "action_widgets",
# Not implemented yet # Not implemented yet
"adw_breakpoint",
# Not implemented yet
"adw_breakpoint_template",
# Not implemented yet
"gtkcolumnview", "gtkcolumnview",
# Comments are not preserved in either direction # Comments are not preserved in either direction
"comments", "comments",