Add Gtk.Scale mark syntax

This commit is contained in:
James Westman 2023-05-13 19:39:48 -05:00
parent 83d11ccb8c
commit 5a782c653b
12 changed files with 242 additions and 5 deletions

View file

@ -27,6 +27,7 @@ from .lsp_utils import Completion
new_statement_patterns = [ new_statement_patterns = [
[(TokenType.PUNCTUATION, "{")], [(TokenType.PUNCTUATION, "{")],
[(TokenType.PUNCTUATION, "}")], [(TokenType.PUNCTUATION, "}")],
[(TokenType.PUNCTUATION, "]")],
[(TokenType.PUNCTUATION, ";")], [(TokenType.PUNCTUATION, ";")],
] ]

View file

@ -30,6 +30,7 @@ from .gtk_file_filter import (
) )
from .gtk_layout import ExtLayout from .gtk_layout import ExtLayout
from .gtk_menu import menu, Menu, MenuAttribute from .gtk_menu import menu, Menu, MenuAttribute
from .gtk_scale import ExtScaleMarks
from .gtk_size_group import ExtSizeGroupWidgets from .gtk_size_group import ExtSizeGroupWidgets
from .gtk_string_list import ExtStringListStrings from .gtk_string_list import ExtStringListStrings
from .gtk_styles import ExtStyles from .gtk_styles import ExtStyles
@ -68,6 +69,7 @@ OBJECT_CONTENT_HOOKS.children = [
ext_file_filter_suffixes, ext_file_filter_suffixes,
ExtLayout, ExtLayout,
ExtListItemFactory, ExtListItemFactory,
ExtScaleMarks,
ExtSizeGroupWidgets, ExtSizeGroupWidgets,
ExtStringListStrings, ExtStringListStrings,
ExtStyles, ExtStyles,

View file

@ -29,7 +29,13 @@ from ..errors import (
) )
from ..completions_utils import * from ..completions_utils import *
from .. import decompiler as decompile from .. import decompiler as decompile
from ..decompiler import DecompileCtx, decompiler from ..decompiler import (
DecompileCtx,
decompiler,
escape_quote,
truthy,
decompile_translatable,
)
from ..gir import ( from ..gir import (
StringType, StringType,
BoolType, BoolType,

View file

@ -0,0 +1,152 @@
# gtk_scale.py
#
# Copyright 2023 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View file

@ -147,9 +147,11 @@ class XmlOutput(OutputFormat):
raise CompilerBugError() raise CompilerBugError()
def _translated_string_attrs( def _translated_string_attrs(
self, translated: T.Union[QuotedLiteral, Translated] self, translated: T.Optional[T.Union[QuotedLiteral, Translated]]
) -> T.Dict[str, T.Optional[str]]: ) -> T.Dict[str, T.Optional[str]]:
if isinstance(translated, QuotedLiteral): if translated is None:
return {}
elif isinstance(translated, QuotedLiteral):
return {} return {}
else: else:
return {"translatable": "true", "context": translated.translate_context} return {"translatable": "true", "context": translated.translate_context}
@ -350,6 +352,20 @@ class XmlOutput(OutputFormat):
xml.end_tag() xml.end_tag()
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): elif isinstance(extension, ExtStringListStrings):
xml.start_tag("items") xml.start_tag("items")
for string in extension.children: for string in extension.children:

View file

@ -244,7 +244,7 @@ class ParseNode:
what it does and how the parser works before using it.""" what it does and how the parser works before using it."""
return Err(self, message) return Err(self, message)
def expected(self, expect) -> "Err": def expected(self, expect: str) -> "Err":
"""Convenience method for err().""" """Convenience method for err()."""
return self.err("Expected " + expect) 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 the child does not match, one token is skipped and the match is attempted
again.""" again."""
def __init__(self, child, delimiter): def __init__(self, child, delimiter, between_delimiter=None):
self.child = to_parse_node(child) self.child = to_parse_node(child)
self.delimiter = to_parse_node(delimiter) 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): def _parse(self, ctx: ParseContext):
while not self.delimiter.parse(ctx).succeeded(): while not self.delimiter.parse(ctx).succeeded():
@ -402,6 +405,17 @@ class Until(ParseNode):
try: try:
if not self.child.parse(ctx).matched(): if not self.child.parse(ctx).matched():
ctx.skip_unexpected_token() 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: except CompileError as e:
ctx.errors.append(e) ctx.errors.append(e)
ctx.next_token() ctx.next_token()

View file

@ -212,6 +212,21 @@ The template type is `Gtk.ListItem <https://docs.gtk.org/gtk4/class.ListItem.htm
} }
.. _Syntax ExtScaleMarks:
Gtk.Scale Marks
---------------
.. rst-class:: grammar-block
ExtScaleMarks = 'marks' '[' (ExtScaleMark),* ']'
ExtScaleMark = 'mark' '(' ( '-' | '+' )? <value::ref:`NUMBER<Syntax NUMBER>`> ( ',' <position::ref:`IDENT<Syntax IDENT>`> ( ',' :ref:`StringValue<Syntax StringValue>` )? )? ')'
Valid in `Gtk.Scale <https://docs.gtk.org/gtk4/class.Scale.html>`_.
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: .. _Syntax ExtSizeGroupWidgets:
Gtk.SizeGroup Widgets Gtk.SizeGroup Widgets

View file

@ -0,0 +1,7 @@
using Gtk 4.0;
Scale {
marks [
mark (0, bottom_right),
]
}

View file

@ -0,0 +1 @@
5,14,12,'bottom_right' is not a member of Gtk.PositionType

View file

@ -0,0 +1,9 @@
using Gtk 4.0;
Scale {
marks [
mark (-1, bottom),
mark (0, top, _("Hello, world!")),
mark (2),
]
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkScale">
<marks>
<mark value="-1" position="bottom"></mark>
<mark value="0" position="top" translatable="true">Hello, world!</mark>
<mark value="2"></mark>
</marks>
</object>
</interface>

View file

@ -165,6 +165,7 @@ class TestSamples(unittest.TestCase):
for f in Path(__file__).parent.glob("samples/*.blp") for f in Path(__file__).parent.glob("samples/*.blp")
if not f.stem.endswith("_dec") if not f.stem.endswith("_dec")
] ]
samples.sort()
for sample in samples: for sample in samples:
REQUIRE_ADW_1_4 = ["adw_breakpoint"] REQUIRE_ADW_1_4 = ["adw_breakpoint"]
@ -191,6 +192,7 @@ class TestSamples(unittest.TestCase):
sample_errors = [ sample_errors = [
f.stem for f in Path(__file__).parent.glob("sample_errors/*.blp") f.stem for f in Path(__file__).parent.glob("sample_errors/*.blp")
] ]
sample_errors.sort()
for sample_error in sample_errors: for sample_error in sample_errors:
REQUIRE_ADW_1_4 = ["adw_breakpoint"] REQUIRE_ADW_1_4 = ["adw_breakpoint"]
@ -212,6 +214,7 @@ class TestSamples(unittest.TestCase):
self.assert_decompile("property_binding_dec") self.assert_decompile("property_binding_dec")
self.assert_decompile("placeholder_dec") self.assert_decompile("placeholder_dec")
self.assert_decompile("responses") self.assert_decompile("responses")
self.assert_decompile("scale_marks")
self.assert_decompile("signal") self.assert_decompile("signal")
self.assert_decompile("strings") self.assert_decompile("strings")
self.assert_decompile("style_dec") self.assert_decompile("style_dec")