mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
Add Adw.Breakpoint custom syntax
This commit is contained in:
parent
aafebf0dfb
commit
8fcd08c835
12 changed files with 264 additions and 11 deletions
|
@ -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,
|
||||
|
|
127
blueprintcompiler/language/adw_breakpoint.py
Normal file
127
blueprintcompiler/language/adw_breakpoint.py
Normal 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")
|
|
@ -28,6 +28,7 @@ from .gobject_object import Object
|
|||
@dataclass
|
||||
class ValueTypeCtx:
|
||||
value_type: T.Optional[GirType]
|
||||
allow_null: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
@ -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']}",
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
12
tests/sample_errors/adw_breakpoint.blp
Normal file
12
tests/sample_errors/adw_breakpoint.blp
Normal 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";
|
||||
}
|
||||
}
|
4
tests/sample_errors/adw_breakpoint.err
Normal file
4
tests/sample_errors/adw_breakpoint.err
Normal 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
|
13
tests/samples/adw_breakpoint.blp
Normal file
13
tests/samples/adw_breakpoint.blp
Normal 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;
|
||||
}
|
||||
}
|
11
tests/samples/adw_breakpoint.ui
Normal file
11
tests/samples/adw_breakpoint.ui
Normal 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>
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue