diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index 1d9f4dd..f7d858c 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -275,9 +275,28 @@ def decompile_placeholder(ctx, gir): pass +def decompile_translatable( + string: str, + translatable: T.Optional[str], + context: T.Optional[str], + comments: T.Optional[str], +) -> T.Tuple[T.Optional[str], str]: + if translatable is not None and truthy(translatable): + if comments is not None: + comments = comments.replace("/*", " ").replace("*/", " ") + comments = f"/* Translators: {comments} */" + + if context is not None: + return comments, f'C_("{escape_quote(context)}", "{escape_quote(string)}")' + else: + return comments, f'_("{escape_quote(string)}")' + else: + return comments, f'"{escape_quote(string)}"' + + @decompiler("property", cdata=True) def decompile_property( - ctx, + ctx: DecompileCtx, gir, name, cdata, @@ -306,12 +325,12 @@ def decompile_property( flags += " bidirectional" ctx.print(f"{name}: bind-property {bind_source}.{bind_property}{flags};") elif truthy(translatable): - if context is not None: - ctx.print( - f'{name}: C_("{escape_quote(context)}", "{escape_quote(cdata)}");' - ) - else: - ctx.print(f'{name}: _("{escape_quote(cdata)}");') + comments, translatable = decompile_translatable( + cdata, translatable, context, comments + ) + if comments is not None: + ctx.print(comments) + ctx.print(f"{name}: {translatable};") elif gir is None or gir.properties.get(name) is None: ctx.print(f'{name}: "{escape_quote(cdata)}";') else: diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 8f68647..a7334b1 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,3 +1,4 @@ +from .adw_message_dialog import Responses from .attributes import BaseAttribute, BaseTypedAttribute from .binding import Binding from .contexts import ValueTypeCtx @@ -56,6 +57,7 @@ OBJECT_CONTENT_HOOKS.children = [ Widgets, Items, Strings, + Responses, Child, ] diff --git a/blueprintcompiler/language/adw_message_dialog.py b/blueprintcompiler/language/adw_message_dialog.py new file mode 100644 index 0000000..2911735 --- /dev/null +++ b/blueprintcompiler/language/adw_message_dialog.py @@ -0,0 +1,131 @@ +# adw_message_dialog.py +# +# Copyright 2023 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 ..decompiler import truthy, decompile_translatable +from .common import * +from .contexts import ValueTypeCtx +from .gobject_object import ObjectContent, validate_parent_type +from .values import QuotedLiteral, Translated + + +class Response(AstNode): + grammar = [ + UseIdent("id"), + Match(":").expected(), + AnyOf(QuotedLiteral, Translated).expected("a value"), + ZeroOrMore( + AnyOf(Keyword("destructive"), Keyword("suggested"), Keyword("disabled")) + ), + ] + + @property + def id(self) -> str: + return self.tokens["id"] + + @property + def appearance(self) -> T.Optional[str]: + if "destructive" in self.tokens: + return "destructive" + if "suggested" in self.tokens: + return "suggested" + return None + + @property + def enabled(self) -> bool: + return "disabled" not in self.tokens + + @property + def value(self) -> T.Union[QuotedLiteral, Translated]: + return self.children[0] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(StringType()) + + @validate("id") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate response ID '{self.id}'", + check=lambda child: child.id == self.id, + ) + + +class Responses(AstNode): + grammar = [ + Keyword("responses"), + Match("[").expected(), + Delimited(Response, ","), + "]", + ] + + @property + def responses(self) -> T.List[Response]: + return self.children + + @validate("responses") + def container_is_message_dialog(self): + validate_parent_type(self, "Adw", "MessageDialog", "responses") + + @validate("responses") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate responses block") + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Adw", "MessageDialog"), + matches=new_statement_patterns, +) +def style_completer(ast_node, match_variables): + yield Completion( + "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" + ) + + +@decompiler("responses") +def decompile_responses(ctx, gir): + ctx.print(f"responses [") + + +@decompiler("response", cdata=True) +def decompile_response( + ctx, + gir, + cdata, + id, + appearance=None, + enabled=None, + translatable=None, + context=None, + comments=None, +): + comments, translated = decompile_translatable( + cdata, translatable, context, comments + ) + if comments is not None: + ctx.print(comments) + + flags = "" + if appearance is not None: + flags += f" {appearance}" + if enabled is not None and not truthy(enabled): + flags += " disabled" + + ctx.print(f"{id}: {translated}{flags},") diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 6f71595..ad6e658 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -126,14 +126,17 @@ class XmlOutput(OutputFormat): xml.end_tag() def _translated_string_attrs( - self, translated: Translated + self, translated: T.Union[QuotedLiteral, Translated] ) -> T.Dict[str, T.Optional[str]]: - return { - "translatable": "true", - "context": translated.child.context - if isinstance(translated.child, TranslatedWithContext) - else None, - } + if isinstance(translated, QuotedLiteral): + return {} + else: + return { + "translatable": "true", + "context": translated.child.context + if isinstance(translated.child, TranslatedWithContext) + else None, + } def _emit_signal(self, signal: Signal, xml: XmlEmitter): name = signal.name @@ -270,6 +273,24 @@ class XmlOutput(OutputFormat): self._emit_attribute("property", "name", prop.name, prop.value, xml) xml.end_tag() + elif isinstance(extension, Responses): + xml.start_tag("responses") + for response in extension.responses: + # todo: translated + xml.start_tag( + "response", + id=response.id, + **self._translated_string_attrs(response.value), + enabled=None if response.enabled else "false", + appearance=response.appearance, + ) + if isinstance(response.value, Translated): + xml.put_text(response.value.child.string) + else: + xml.put_text(response.value.value) + xml.end_tag() + xml.end_tag() + elif isinstance(extension, Strings): xml.start_tag("items") for prop in extension.children: diff --git a/tests/samples/responses.blp b/tests/samples/responses.blp new file mode 100644 index 0000000..d7032a7 --- /dev/null +++ b/tests/samples/responses.blp @@ -0,0 +1,10 @@ +using Gtk 4.0; +using Adw 1; + +Adw.MessageDialog { + responses [ + cancel: _("Cancel"), + discard: _("Discard") destructive, + save: "Save" suggested disabled, + ] +} \ No newline at end of file diff --git a/tests/samples/responses.ui b/tests/samples/responses.ui new file mode 100644 index 0000000..ba26de5 --- /dev/null +++ b/tests/samples/responses.ui @@ -0,0 +1,11 @@ + + + + + + Cancel + Discard + Save + + + \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index c292214..9988bb6 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -143,6 +143,17 @@ class TestSamples(unittest.TestCase): raise AssertionError() def test_samples(self): + try: + import gi + + gi.require_version("Adw", "1") + from gi.repository import Adw + + have_adw = True + Adw.init() + except: + have_adw = False + self.assert_sample("accessibility") self.assert_sample("action_widgets") self.assert_sample("child_type") @@ -166,6 +177,7 @@ class TestSamples(unittest.TestCase): ) # The image resource doesn't exist self.assert_sample("property") self.assert_sample("property_binding") + self.assert_sample("responses", skip_run=not have_adw) self.assert_sample("signal", skip_run=True) # The callback doesn't exist self.assert_sample("size_group") self.assert_sample("string_list") @@ -257,6 +269,7 @@ class TestSamples(unittest.TestCase): self.assert_decompile("property") self.assert_decompile("property_binding_dec") self.assert_decompile("placeholder_dec") + self.assert_decompile("responses") self.assert_decompile("signal") self.assert_decompile("strings") self.assert_decompile("style_dec")