From 5a782c653bc0f5105d29007ac54762c3a3fb54fb Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 13 May 2023 19:39:48 -0500 Subject: [PATCH] Add Gtk.Scale mark syntax --- blueprintcompiler/completions_utils.py | 1 + blueprintcompiler/language/__init__.py | 2 + blueprintcompiler/language/common.py | 8 +- blueprintcompiler/language/gtk_scale.py | 152 ++++++++++++++++++++ blueprintcompiler/outputs/xml/__init__.py | 20 ++- blueprintcompiler/parse_tree.py | 18 ++- docs/reference/extensions.rst | 15 ++ tests/sample_errors/scale_mark_position.blp | 7 + tests/sample_errors/scale_mark_position.err | 1 + tests/samples/scale_marks.blp | 9 ++ tests/samples/scale_marks.ui | 11 ++ tests/test_samples.py | 3 + 12 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 blueprintcompiler/language/gtk_scale.py create mode 100644 tests/sample_errors/scale_mark_position.blp create mode 100644 tests/sample_errors/scale_mark_position.err create mode 100644 tests/samples/scale_marks.blp create mode 100644 tests/samples/scale_marks.ui diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index b9811b9..094e449 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -27,6 +27,7 @@ from .lsp_utils import Completion new_statement_patterns = [ [(TokenType.PUNCTUATION, "{")], [(TokenType.PUNCTUATION, "}")], + [(TokenType.PUNCTUATION, "]")], [(TokenType.PUNCTUATION, ";")], ] diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 8cfd1bd..d785e56 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -30,6 +30,7 @@ from .gtk_file_filter import ( ) from .gtk_layout import ExtLayout from .gtk_menu import menu, Menu, MenuAttribute +from .gtk_scale import ExtScaleMarks from .gtk_size_group import ExtSizeGroupWidgets from .gtk_string_list import ExtStringListStrings from .gtk_styles import ExtStyles @@ -68,6 +69,7 @@ OBJECT_CONTENT_HOOKS.children = [ ext_file_filter_suffixes, ExtLayout, ExtListItemFactory, + ExtScaleMarks, ExtSizeGroupWidgets, ExtStringListStrings, ExtStyles, diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 082aaa4..9938bec 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -29,7 +29,13 @@ from ..errors import ( ) from ..completions_utils import * from .. import decompiler as decompile -from ..decompiler import DecompileCtx, decompiler +from ..decompiler import ( + DecompileCtx, + decompiler, + escape_quote, + truthy, + decompile_translatable, +) from ..gir import ( StringType, BoolType, diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py new file mode 100644 index 0000000..a81e03d --- /dev/null +++ b/blueprintcompiler/language/gtk_scale.py @@ -0,0 +1,152 @@ +# gtk_scale.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 .gobject_object import validate_parent_type, ObjectContent +from .common import * +from .values import StringValue + + +class ExtScaleMark(AstNode): + grammar = [ + Keyword("mark"), + Match("(").expected(), + [ + Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))), + UseNumber("value"), + Optional( + [ + ",", + UseIdent("position"), + Optional([",", StringValue]), + ] + ), + ], + Match(")").expected(), + ] + + @property + def value(self) -> float: + if self.tokens["sign"] == "-": + return -self.tokens["value"] + else: + return self.tokens["value"] + + @property + def position(self) -> T.Optional[str]: + return self.tokens["position"] + + @property + def label(self) -> T.Optional[StringValue]: + if len(self.children[StringValue]) == 1: + return self.children[StringValue][0] + else: + return None + + @docs("position") + def position_docs(self) -> T.Optional[str]: + if member := self.root.gir.get_type("PositionType", "Gtk").members.get( + self.position + ): + return member.doc + else: + return None + + @validate("position") + def validate_position(self): + positions = self.root.gir.get_type("PositionType", "Gtk").members + if self.position is not None and positions.get(self.position) is None: + raise CompileError( + f"'{self.position}' is not a member of Gtk.PositionType", + did_you_mean=(self.position, positions.keys()), + ) + + +class ExtScaleMarks(AstNode): + grammar = [ + Keyword("marks"), + Match("[").expected(), + Until(ExtScaleMark, "]", ","), + ] + + @property + def marks(self) -> T.List[ExtScaleMark]: + return self.children + + @validate("marks") + def container_is_size_group(self): + validate_parent_type(self, "Gtk", "Scale", "scale marks") + + @validate("marks") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate 'marks' block") + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Gtk", "Scale"), + matches=new_statement_patterns, +) +def complete_marks(ast_node, match_variables): + yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]") + + +@completer( + applies_in=[ExtScaleMarks], +) +def complete_mark(ast_node, match_variables): + yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),") + + +@decompiler("marks") +def decompile_marks( + ctx, + gir, +): + ctx.print("marks [") + + +@decompiler("mark", cdata=True) +def decompile_mark( + ctx: DecompileCtx, + gir, + value, + position=None, + cdata=None, + translatable="false", + comments=None, + context=None, +): + if comments is not None: + ctx.print(f"/* Translators: {comments} */") + + text = f"mark ({value}" + + if position: + text += f", {position}" + elif cdata: + text += f", bottom" + + if truthy(translatable): + comments, translatable = decompile_translatable( + cdata, translatable, context, comments + ) + text += f", {translatable}" + + text += ")," + ctx.print(text) diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 3774c07..020648c 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -147,9 +147,11 @@ class XmlOutput(OutputFormat): raise CompilerBugError() def _translated_string_attrs( - self, translated: T.Union[QuotedLiteral, Translated] + self, translated: T.Optional[T.Union[QuotedLiteral, Translated]] ) -> T.Dict[str, T.Optional[str]]: - if isinstance(translated, QuotedLiteral): + if translated is None: + return {} + elif isinstance(translated, QuotedLiteral): return {} else: return {"translatable": "true", "context": translated.translate_context} @@ -350,6 +352,20 @@ class XmlOutput(OutputFormat): xml.end_tag() xml.end_tag() + elif isinstance(extension, ExtScaleMarks): + xml.start_tag("marks") + for mark in extension.children: + xml.start_tag( + "mark", + value=mark.value, + position=mark.position, + **self._translated_string_attrs(mark.label and mark.label.child), + ) + if mark.label is not None: + xml.put_text(mark.label.string) + xml.end_tag() + xml.end_tag() + elif isinstance(extension, ExtStringListStrings): xml.start_tag("items") for string in extension.children: diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 8b2963a..ff080ea 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -244,7 +244,7 @@ class ParseNode: what it does and how the parser works before using it.""" return Err(self, message) - def expected(self, expect) -> "Err": + def expected(self, expect: str) -> "Err": """Convenience method for err().""" return self.err("Expected " + expect) @@ -390,9 +390,12 @@ class Until(ParseNode): the child does not match, one token is skipped and the match is attempted again.""" - def __init__(self, child, delimiter): + def __init__(self, child, delimiter, between_delimiter=None): self.child = to_parse_node(child) self.delimiter = to_parse_node(delimiter) + self.between_delimiter = ( + to_parse_node(between_delimiter) if between_delimiter is not None else None + ) def _parse(self, ctx: ParseContext): while not self.delimiter.parse(ctx).succeeded(): @@ -402,6 +405,17 @@ class Until(ParseNode): try: if not self.child.parse(ctx).matched(): ctx.skip_unexpected_token() + + if ( + self.between_delimiter is not None + and not self.between_delimiter.parse(ctx).succeeded() + ): + if self.delimiter.parse(ctx).succeeded(): + return True + else: + if ctx.is_eof(): + return False + ctx.skip_unexpected_token() except CompileError as e: ctx.errors.append(e) ctx.next_token() diff --git a/docs/reference/extensions.rst b/docs/reference/extensions.rst index d8553ad..bfa0c56 100644 --- a/docs/reference/extensions.rst +++ b/docs/reference/extensions.rst @@ -212,6 +212,21 @@ The template type is `Gtk.ListItem `> ( ',' `> ( ',' :ref:`StringValue` )? )? ')' + +Valid in `Gtk.Scale `_. + +The ``marks`` block defines the marks on a scale. A single ``mark`` has up to three arguments: a value, an optional position, and an optional label. The position can be ``left``, ``right``, ``top``, or ``bottom``. The label may be translated. + + .. _Syntax ExtSizeGroupWidgets: Gtk.SizeGroup Widgets diff --git a/tests/sample_errors/scale_mark_position.blp b/tests/sample_errors/scale_mark_position.blp new file mode 100644 index 0000000..40f479b --- /dev/null +++ b/tests/sample_errors/scale_mark_position.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +Scale { + marks [ + mark (0, bottom_right), + ] +} \ No newline at end of file diff --git a/tests/sample_errors/scale_mark_position.err b/tests/sample_errors/scale_mark_position.err new file mode 100644 index 0000000..612ea8a --- /dev/null +++ b/tests/sample_errors/scale_mark_position.err @@ -0,0 +1 @@ +5,14,12,'bottom_right' is not a member of Gtk.PositionType \ No newline at end of file diff --git a/tests/samples/scale_marks.blp b/tests/samples/scale_marks.blp new file mode 100644 index 0000000..a766cfa --- /dev/null +++ b/tests/samples/scale_marks.blp @@ -0,0 +1,9 @@ +using Gtk 4.0; + +Scale { + marks [ + mark (-1, bottom), + mark (0, top, _("Hello, world!")), + mark (2), + ] +} diff --git a/tests/samples/scale_marks.ui b/tests/samples/scale_marks.ui new file mode 100644 index 0000000..719f888 --- /dev/null +++ b/tests/samples/scale_marks.ui @@ -0,0 +1,11 @@ + + + + + + + Hello, world! + + + + \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index 0f39866..f6ba1a0 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -165,6 +165,7 @@ class TestSamples(unittest.TestCase): for f in Path(__file__).parent.glob("samples/*.blp") if not f.stem.endswith("_dec") ] + samples.sort() for sample in samples: REQUIRE_ADW_1_4 = ["adw_breakpoint"] @@ -191,6 +192,7 @@ class TestSamples(unittest.TestCase): sample_errors = [ f.stem for f in Path(__file__).parent.glob("sample_errors/*.blp") ] + sample_errors.sort() for sample_error in sample_errors: REQUIRE_ADW_1_4 = ["adw_breakpoint"] @@ -212,6 +214,7 @@ class TestSamples(unittest.TestCase): self.assert_decompile("property_binding_dec") self.assert_decompile("placeholder_dec") self.assert_decompile("responses") + self.assert_decompile("scale_marks") self.assert_decompile("signal") self.assert_decompile("strings") self.assert_decompile("style_dec")