From 736681a84161737bc53a8ba995c65e42aa9642a3 Mon Sep 17 00:00:00 2001 From: Gleb Smirnov Date: Fri, 18 Feb 2022 19:03:41 +0300 Subject: [PATCH] feat: add syntax for `GtkDialog`'s action widgets --- blueprintcompiler/language/gobject_object.py | 25 ++++ blueprintcompiler/language/gtk_dialog.py | 132 ++++++++++++++++++ .../language/gtkbuilder_child.py | 18 +++ 3 files changed, 175 insertions(+) create mode 100644 blueprintcompiler/language/gtk_dialog.py diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index c8889a0..f4b760a 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -19,6 +19,7 @@ import typing as T +from .gtk_dialog import ResponseId from .common import * @@ -80,14 +81,38 @@ class Object(AstNode): if self.gir_class: return self.gir_class.doc + @property + def action_widgets(self) -> T.List[ResponseId]: + """Get list of `GtkDialog`'s action widgets. + + Empty if object is not `GtkDialog`. + """ + from .gtkbuilder_child import Child + + return [ + child.response_id + for child in self.children[ObjectContent][0].children[Child] + if child.response_id + ] def emit_xml(self, xml: XmlEmitter): + from .gtkbuilder_child import Child + xml.start_tag("object", **{ "class": self.gir_class.glib_type_name if self.gir_class else self.tokens["class_name"], "id": self.tokens["id"], }) for child in self.children: child.emit_xml(xml) + + # List action widgets + action_widgets = self.action_widgets + if action_widgets: + xml.start_tag("action-widgets") + for action_widget in action_widgets: + action_widget.emit_action_widget(xml) + xml.end_tag() + xml.end_tag() diff --git a/blueprintcompiler/language/gtk_dialog.py b/blueprintcompiler/language/gtk_dialog.py new file mode 100644 index 0000000..2656a25 --- /dev/null +++ b/blueprintcompiler/language/gtk_dialog.py @@ -0,0 +1,132 @@ +# gtk_dialog.py +# +# Copyright 2022 Gleb Smirnov +# +# 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 * + + +class ResponseId(AstNode): + """Response ID of GtkDialog's action widget.""" + + grammar = [ + UseIdent("response"), + "=", + AnyOf( + UseIdent("response_id"), + UseNumber("response_id") + ), + Optional([ + Keyword("default"), UseLiteral("is_default", True) + ]) + ] + + @validate() + def child_type_is_action(self) -> None: + """Check that child type is "action".""" + child_type = self.parent.tokens["child_type"] + if child_type != "action": + raise CompileError(f"Only action widget can have response ID") + + @validate() + def parent_is_dialog(self) -> None: + """Chech that parent widget is `GtkDialog`.""" + from .gobject_object import validate_parent_type + + validate_parent_type(self, "Gtk", "Dialog", "action widgets") + + @validate() + def widget_have_id(self) -> None: + """Check that action widget have ID.""" + from .gobject_object import Object + + _object = self.parent.children[Object][0] + if _object.tokens["id"] is None: + raise CompileError(f"Action widget must have ID") + + @validate("response_id") + def correct_response_type(self) -> None: + """Validate response type. + + Response type might be GtkResponseType member + or positive number. + """ + gir = self.root.gir + response = self.tokens["response_id"] + + if isinstance(response, int): + if response < 0: + raise CompileError( + "Numeric response type can't be negative") + elif isinstance(response, float): + raise CompileError( + "Response type must be GtkResponseType member or integer," + " not float" + ) + else: + responses = gir.get_type("ResponseType", "Gtk").members.keys() + if response not in responses: + raise CompileError( + f"Response type \"{response}\" doesn't exist") + + @validate("default") + def no_multiple_default(self) -> None: + """Only one action widget in dialog can be default.""" + from .gtkbuilder_child import Child + from .gobject_object import Object + + if not self.tokens["is_default"]: + return + + action_widgets = self.parent_by_type(Object).action_widgets + for widget in action_widgets: + if widget == self: + break + if widget.tokens["is_default"]: + raise CompileError("Default response is already set") + + @property + def widget_id(self) -> str: + """Get action widget ID.""" + from .gobject_object import Object + + _object: Object = self.parent.children[Object][0] + return _object.tokens["id"] + + def emit_xml(self, xml: XmlEmitter) -> None: + """Emit nothing. + + Response ID don't have to emit any XML in place, + but have to emit action-widget tag in separate + place (see `ResponseId.emit_action_widget`) + """ + + def emit_action_widget(self, xml: XmlEmitter) -> None: + """Emit action-widget XML. + + Must be called while tag is open. + + For more details see `GtkDialog` docs. + """ + xml.start_tag( + "action-widget", + response=self.tokens["response_id"], + default=self.tokens["is_default"] + ) + xml.put_text(self.widget_id) + xml.end_tag() diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index 68b317f..6bda160 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -18,7 +18,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from functools import cache + from .gobject_object import Object +from .gtk_dialog import ResponseId from .common import * @@ -28,11 +31,26 @@ class Child(AstNode): "[", Optional(["internal-child", UseLiteral("internal_child", True)]), UseIdent("child_type").expected("a child type"), + Optional(ResponseId), "]", ]), Object, ] + @property + @cache + def response_id(self) -> T.Optional[ResponseId]: + """Get action widget's response ID. + + If child is not action widget, returns `None`. + """ + response_ids = self.children[ResponseId] + + if response_ids: + return response_ids[0] + else: + return None + def emit_xml(self, xml: XmlEmitter): child_type = internal_child = None if self.tokens["internal_child"]: