Add Adw.Breakpoint custom syntax

This commit is contained in:
James Westman 2023-04-25 19:43:22 -05:00
parent aafebf0dfb
commit 8fcd08c835
12 changed files with 264 additions and 11 deletions

View file

@ -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,

View file

@ -0,0 +1,127 @@
# adw_breakpoint.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 .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")

View file

@ -28,6 +28,7 @@ from .gobject_object import Object
@dataclass
class ValueTypeCtx:
value_type: T.Optional[GirType]
allow_null: bool = False
@dataclass

View file

@ -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']}",

View file

@ -280,9 +280,16 @@ class IdentLiteral(AstNode):
elif expected_type is not None:
object = self.context[ScopeCtx].objects.get(self.ident)
if object is None:
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()),
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(

View file

@ -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:

View file

@ -42,6 +42,26 @@ Valid in any `Gtk.Widget <https://docs.gtk.org/gtk4/class.Widget.html>`_.
The ``accessibility`` block defines values relevant to accessibility software. The property names and acceptable values are described in the `Gtk.AccessibleRelation <https://docs.gtk.org/gtk4/enum.AccessibleRelation.html>`_, `Gtk.AccessibleState <https://docs.gtk.org/gtk4/enum.AccessibleState.html>`_, and `Gtk.AccessibleProperty <https://docs.gtk.org/gtk4/enum.AccessibleProperty.html>`_ enums.
.. _Syntax ExtAdwBreakpoint:
Adw.Breakpoint
--------------
.. rst-class:: grammar-block
ExtAdwBreakpointCondition = 'condition' '(' <condition::ref:`QUOTED<Syntax QUOTED>`> ')'
ExtAdwBreakpoint = 'setters' '{' ExtAdwBreakpointSetter* '}'
ExtAdwBreakpointSetter = <object::ref:`IDENT<Syntax IDENT>`> '.' <property::ref:`IDENT<Syntax IDENT>`> ':' :ref:`Value <Syntax Value>` ';'
Valid in `Adw.Breakpoint <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Breakpoint.html>`_.
Defines the condition for a breakpoint and the properties that will be set at that breakpoint. See the documentation for `Adw.Breakpoint <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Breakpoint.html>`_.
.. note::
The `Adw.Breakpoint:condition <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/property.Breakpoint.condition.html>`_ property has type `Adw.BreakpointCondition <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/struct.BreakpointCondition.html>`_, which GtkBuilder doesn't know how to parse from a string. Therefore, the ``condition`` syntax is used instead.
.. _Syntax ExtAdwMessageDialog:
Adw.MessageDialog Responses

View file

@ -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";
}
}

View file

@ -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

View file

@ -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;
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkLabel" id="label"></object>
<object class="AdwBreakpoint">
<condition>max-width: 600px</condition>
<setter object="label" property="label" translatable="true">Hello, world!</setter>
<setter object="label" property="visible">false</setter>
<setter object="label" property="extra-menu"></setter>
</object>
</interface>

View file

@ -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")