From 8fcd08c8352e3420f0c45058971749119f5a8683 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 25 Apr 2023 19:43:22 -0500 Subject: [PATCH] Add Adw.Breakpoint custom syntax --- blueprintcompiler/language/__init__.py | 9 +- blueprintcompiler/language/adw_breakpoint.py | 127 ++++++++++++++++++ blueprintcompiler/language/contexts.py | 1 + .../language/gobject_property.py | 5 - blueprintcompiler/language/values.py | 15 ++- blueprintcompiler/outputs/xml/__init__.py | 31 +++++ docs/reference/extensions.rst | 20 +++ tests/sample_errors/adw_breakpoint.blp | 12 ++ tests/sample_errors/adw_breakpoint.err | 4 + tests/samples/adw_breakpoint.blp | 13 ++ tests/samples/adw_breakpoint.ui | 11 ++ tests/test_samples.py | 27 +++- 12 files changed, 264 insertions(+), 11 deletions(-) create mode 100644 blueprintcompiler/language/adw_breakpoint.py create mode 100644 tests/sample_errors/adw_breakpoint.blp create mode 100644 tests/sample_errors/adw_breakpoint.err create mode 100644 tests/samples/adw_breakpoint.blp create mode 100644 tests/samples/adw_breakpoint.ui diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index b5e2f04..8cfd1bd 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,8 +1,13 @@ from .gtk_list_item_factory import ExtListItemFactory from .adw_message_dialog import ExtAdwMessageDialog from .attributes import BaseAttribute +from .adw_breakpoint import ( + AdwBreakpointSetters, + AdwBreakpointSetter, + AdwBreakpointCondition, +) from .binding import Binding -from .contexts import ValueTypeCtx +from .contexts import ScopeCtx, ValueTypeCtx from .expression import ( CastExpr, ClosureArg, @@ -53,6 +58,8 @@ from .common import * OBJECT_CONTENT_HOOKS.children = [ Signal, Property, + AdwBreakpointCondition, + AdwBreakpointSetters, ExtAccessibility, ExtAdwMessageDialog, ExtComboBoxItems, diff --git a/blueprintcompiler/language/adw_breakpoint.py b/blueprintcompiler/language/adw_breakpoint.py new file mode 100644 index 0000000..20d0b89 --- /dev/null +++ b/blueprintcompiler/language/adw_breakpoint.py @@ -0,0 +1,127 @@ +# adw_breakpoint.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 .common import * +from .contexts import ScopeCtx, ValueTypeCtx +from .gobject_object import Object, validate_parent_type +from .values import Value + + +class AdwBreakpointCondition(AstNode): + grammar = ["condition", "(", UseQuoted("condition"), Match(")").expected()] + + @property + def condition(self) -> str: + return self.tokens["condition"] + + @validate() + def unique(self): + self.validate_unique_in_parent("Duplicate condition statement") + + +class AdwBreakpointSetter(AstNode): + grammar = Statement( + UseIdent("object"), + Match(".").expected(), + UseIdent("property"), + Match(":").expected(), + Value, + ) + + @property + def object_id(self) -> str: + return self.tokens["object"] + + @property + def object(self) -> T.Optional[Object]: + return self.context[ScopeCtx].objects.get(self.object_id) + + @property + def property_name(self) -> T.Optional[str]: + return self.tokens["property"] + + @property + def value(self) -> Value: + return self.children[Value][0] + + @property + def gir_class(self) -> T.Optional[GirType]: + if self.object is not None: + return self.object.gir_class + else: + return None + + @property + def gir_property(self): + if self.gir_class is not None and not isinstance(self.gir_class, ExternType): + return self.gir_class.properties.get(self.property_name) + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + if self.gir_property is not None: + type = self.gir_property.type + else: + type = None + + return ValueTypeCtx(type, allow_null=True) + + @validate("object") + def object_exists(self): + if self.object is None: + raise CompileError( + f"Could not find object with ID {self.object_id}", + did_you_mean=(self.object_id, self.context[ScopeCtx].objects.keys()), + ) + + @validate("property") + def property_exists(self): + if self.gir_class is None or self.gir_class.incomplete: + # Objects that we have no gir data on should not be validated + # This happens for classes defined by the app itself + return + + if self.gir_property is None: + raise CompileError( + f"Class {self.gir_class.full_name} does not have a property called {self.property_name}", + did_you_mean=(self.property_name, self.gir_class.properties.keys()), + ) + + @validate() + def unique(self): + self.validate_unique_in_parent( + f"Duplicate setter for {self.object_id}.{self.property_name}", + lambda x: x.object_id == self.object_id + and x.property_name == self.property_name, + ) + + +class AdwBreakpointSetters(AstNode): + grammar = ["setters", Match("{").expected(), Until(AdwBreakpointSetter, "}")] + + @property + def setters(self) -> T.List[AdwBreakpointSetter]: + return self.children[AdwBreakpointSetter] + + @validate() + def container_is_breakpoint(self): + validate_parent_type(self, "Adw", "Breakpoint", "breakpoint setters") + + @validate() + def unique(self): + self.validate_unique_in_parent("Duplicate setters block") diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index 49b7b40..1709918 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -28,6 +28,7 @@ from .gobject_object import Object @dataclass class ValueTypeCtx: value_type: T.Optional[GirType] + allow_null: bool = False @dataclass diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 31ff40d..09873bc 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -76,11 +76,6 @@ class Property(AstNode): # This happens for classes defined by the app itself return - if isinstance(self.parent.parent, Template): - # If the property is part of a template, it might be defined by - # the application and thus not in gir - return - if self.gir_property is None: raise CompileError( f"Class {self.gir_class.full_name} does not have a property called {self.tokens['name']}", diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 626a337..676de37 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -280,10 +280,17 @@ class IdentLiteral(AstNode): elif expected_type is not None: object = self.context[ScopeCtx].objects.get(self.ident) if object is None: - raise CompileError( - f"Could not find object with ID {self.ident}", - did_you_mean=(self.ident, self.context[ScopeCtx].objects.keys()), - ) + if self.ident == "null": + if not self.context[ValueTypeCtx].allow_null: + raise CompileError("null is not permitted here") + else: + raise CompileError( + f"Could not find object with ID {self.ident}", + did_you_mean=( + self.ident, + self.context[ScopeCtx].objects.keys(), + ), + ) elif object.gir_class and not object.gir_class.assignable_to(expected_type): raise CompileError( f"Cannot assign {object.gir_class.full_name} to {expected_type.full_name}" diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index bd70e85..1b5b579 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -280,6 +280,37 @@ class XmlOutput(OutputFormat): self._emit_attribute(prop.tag_name, "name", prop.name, prop.value, xml) xml.end_tag() + elif isinstance(extension, AdwBreakpointCondition): + xml.start_tag("condition") + xml.put_text(extension.condition) + xml.end_tag() + + elif isinstance(extension, AdwBreakpointSetters): + for setter in extension.setters: + attrs = {} + + if isinstance(setter.value.child, Translated): + attrs = self._translated_string_attrs(setter.value.child) + + xml.start_tag( + "setter", + object=setter.object_id, + property=setter.property_name, + **attrs, + ) + if isinstance(setter.value.child, Translated): + xml.put_text(setter.value.child.string) + elif ( + isinstance(setter.value.child, Literal) + and isinstance(setter.value.child.value, IdentLiteral) + and setter.value.child.value.ident == "null" + and setter.context[ScopeCtx].objects.get("null") is None + ): + pass + else: + self._emit_value(setter.value, xml) + xml.end_tag() + elif isinstance(extension, Filters): xml.start_tag(extension.tokens["tag_name"]) for prop in extension.children: diff --git a/docs/reference/extensions.rst b/docs/reference/extensions.rst index a29938b..1017032 100644 --- a/docs/reference/extensions.rst +++ b/docs/reference/extensions.rst @@ -42,6 +42,26 @@ Valid in any `Gtk.Widget `_. The ``accessibility`` block defines values relevant to accessibility software. The property names and acceptable values are described in the `Gtk.AccessibleRelation `_, `Gtk.AccessibleState `_, and `Gtk.AccessibleProperty `_ enums. +.. _Syntax ExtAdwBreakpoint: + +Adw.Breakpoint +-------------- + +.. rst-class:: grammar-block + + ExtAdwBreakpointCondition = 'condition' '(' `> ')' + ExtAdwBreakpoint = 'setters' '{' ExtAdwBreakpointSetter* '}' + ExtAdwBreakpointSetter = `> '.' `> ':' :ref:`Value ` ';' + +Valid in `Adw.Breakpoint `_. + +Defines the condition for a breakpoint and the properties that will be set at that breakpoint. See the documentation for `Adw.Breakpoint `_. + +.. note:: + + The `Adw.Breakpoint:condition `_ property has type `Adw.BreakpointCondition `_, which GtkBuilder doesn't know how to parse from a string. Therefore, the ``condition`` syntax is used instead. + + .. _Syntax ExtAdwMessageDialog: Adw.MessageDialog Responses diff --git a/tests/sample_errors/adw_breakpoint.blp b/tests/sample_errors/adw_breakpoint.blp new file mode 100644 index 0000000..7eb4525 --- /dev/null +++ b/tests/sample_errors/adw_breakpoint.blp @@ -0,0 +1,12 @@ +using Gtk 4.0; +using Adw 1; + +Label label {} + +Adw.Breakpoint { + setters { + label.foo: "bar"; + not_an_object.visible: true; + label.foo: "baz"; + } +} \ No newline at end of file diff --git a/tests/sample_errors/adw_breakpoint.err b/tests/sample_errors/adw_breakpoint.err new file mode 100644 index 0000000..bd2f6b7 --- /dev/null +++ b/tests/sample_errors/adw_breakpoint.err @@ -0,0 +1,4 @@ +8,11,3,Class Gtk.Label does not have a property called foo +9,5,13,Could not find object with ID not_an_object +10,11,3,Class Gtk.Label does not have a property called foo +10,5,17,Duplicate setter for label.foo \ No newline at end of file diff --git a/tests/samples/adw_breakpoint.blp b/tests/samples/adw_breakpoint.blp new file mode 100644 index 0000000..1d085e4 --- /dev/null +++ b/tests/samples/adw_breakpoint.blp @@ -0,0 +1,13 @@ +using Gtk 4.0; +using Adw 1; + +Gtk.Label label {} + +Adw.Breakpoint { + condition ("max-width: 600px") + setters { + label.label: _("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 new file mode 100644 index 0000000..b2d5ec3 --- /dev/null +++ b/tests/samples/adw_breakpoint.ui @@ -0,0 +1,11 @@ + + + + + + max-width: 600px + Hello, world! + false + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 88cecdf..6e19a08 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -143,6 +143,9 @@ class TestSamples(unittest.TestCase): raise AssertionError() def test_samples(self): + have_adw = False + have_adw_1_4 = False + try: import gi @@ -151,11 +154,15 @@ class TestSamples(unittest.TestCase): have_adw = True Adw.init() + if Adw.MINOR_VERSION >= 4: + have_adw_1_4 = True except: - have_adw = False + pass self.assert_sample("accessibility") self.assert_sample("action_widgets") + if have_adw_1_4: + self.assert_sample("adw_breakpoint") self.assert_sample("child_type") self.assert_sample("combo_box_text") self.assert_sample("comments") @@ -208,6 +215,22 @@ class TestSamples(unittest.TestCase): self.assert_sample("using") def test_sample_errors(self): + have_adw = False + have_adw_1_4 = False + + try: + import gi + + gi.require_version("Adw", "1") + from gi.repository import Adw + + have_adw = True + Adw.init() + if Adw.MINOR_VERSION >= 4: + have_adw_1_4 = True + except: + pass + self.assert_sample_error("a11y_in_non_widget") self.assert_sample_error("a11y_prop_dne") self.assert_sample_error("a11y_prop_obj_dne") @@ -219,6 +242,8 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("action_widget_in_invalid_container") self.assert_sample_error("action_widget_response_dne") self.assert_sample_error("action_widget_negative_response") + if have_adw_1_4: + self.assert_sample_error("adw_breakpoint") self.assert_sample_error("bitfield_member_dne") self.assert_sample_error("children") self.assert_sample_error("class_assign")