diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1ec071e..6d373cc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: - pages build: - image: registry.gitlab.gnome.org/jwestman/blueprint-compiler + image: registry.gitlab.gnome.org/gnome/blueprint-compiler stage: build script: - black --check --diff ./ tests @@ -33,7 +33,7 @@ build: path: coverage.xml fuzz: - image: registry.gitlab.gnome.org/jwestman/blueprint-compiler + image: registry.gitlab.gnome.org/gnome/blueprint-compiler stage: build script: - meson _build diff --git a/blueprint-compiler.doap b/blueprint-compiler.doap new file mode 100644 index 0000000..f3e4000 --- /dev/null +++ b/blueprint-compiler.doap @@ -0,0 +1,27 @@ + + + Blueprint + A modern language for creating GTK interfaces + Blueprint is a language and associated tooling for building user interfaces for GTK. + + Python + + + + + + + + James Westman + + jwestman + + + diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index de6c06f..850b6d8 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -255,7 +255,11 @@ def decompile_element( ctx._node_stack.append(xml) ctx.start_block() - gir = decompiler(*args, **kwargs) + + try: + gir = decompiler(*args, **kwargs) + except TypeError as e: + raise UnsupportedError(tag=xml.tag) if not decompiler._skip_children: for child in xml.children: @@ -266,8 +270,6 @@ def decompile_element( except UnsupportedError as e: raise e - except TypeError as e: - raise UnsupportedError(tag=xml.tag) def decompile(data: str) -> str: diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index 1e7297c..df1c2e1 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -219,7 +219,7 @@ def report_bug(): # pragma: no cover f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** The blueprint-compiler program has crashed. Please report the above stacktrace, along with the input file(s) if possible, on GitLab: -{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue +{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues/new?issue {Colors.CLEAR}""" ) diff --git a/blueprintcompiler/interactive_port.py b/blueprintcompiler/interactive_port.py index 0c37885..12dd485 100644 --- a/blueprintcompiler/interactive_port.py +++ b/blueprintcompiler/interactive_port.py @@ -71,7 +71,7 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: print( f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the porting tool. If you think it's a bug (which is likely), please file an issue on GitLab: -{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""" +{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""" ) return CouldNotPort("does not compile") @@ -136,7 +136,7 @@ def step1(): wrap.write( f"""[wrap-git] directory = blueprint-compiler -url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git +url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git revision = {VERSION} depth = 1 diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index 6e26048..9376211 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -60,19 +60,21 @@ class ScopeCtx: passed = {} for obj in self._iter_recursive(self.node): - if obj.tokens["id"] is None: + from .gtk_menu import Menu + + if not (isinstance(obj, Object) or isinstance(obj, Menu)) or obj.id is None: continue - if obj.tokens["id"] in passed: + if obj.id in passed: token = obj.group.tokens["id"] if not isinstance(obj, Template) and not isinstance( obj, ExtListItemFactory ): raise CompileError( - f"Duplicate object ID '{obj.tokens['id']}'", + f"Duplicate object ID '{obj.id}'", token.range, ) - passed[obj.tokens["id"]] = obj + passed[obj.id] = obj def _iter_recursive(self, node: AstNode): yield node diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index e0b4246..de6fbf1 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -302,12 +302,18 @@ expr.children = [ @decompiler("lookup", skip_children=True, cdata=True) def decompile_lookup( - ctx: DecompileCtx, gir: gir.GirContext, cdata: str, name: str, type: str + ctx: DecompileCtx, + gir: gir.GirContext, + cdata: str, + name: str, + type: T.Optional[str] = None, ): if ctx.parent_node is not None and ctx.parent_node.tag == "property": ctx.print("expr ") - if t := ctx.type_by_cname(type): + if type is None: + type = "" + elif t := ctx.type_by_cname(type): type = decompile.full_name(t) else: type = "$" + type @@ -327,7 +333,7 @@ def decompile_lookup( if constant == ctx.template_class: ctx.print("template." + name) elif constant == "": - ctx.print("item as <" + type + ">." + name) + ctx.print(f"item as <{type}>.{name}") else: ctx.print(constant + "." + name) return diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 9c27b97..3b4235f 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -225,8 +225,14 @@ class Signal(AstNode): @decompiler("signal") -def decompile_signal(ctx, gir, name, handler, swapped=None, after="false", object=None): +def decompile_signal( + ctx: DecompileCtx, gir, name, handler, swapped=None, after="false", object=None +): object_name = object or "" + + if object_name == ctx.template_class: + object_name = "template" + name = name.replace("_", "-") line = f"{name} => ${handler}({object_name})" diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index ae062fb..e590539 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -17,7 +17,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -""" Utilities for parsing an AST from a token stream. """ +"""Utilities for parsing an AST from a token stream.""" import typing as T from enum import Enum diff --git a/docs/collect-sections.py b/docs/collect-sections.py index 07bc5f6..a2dd004 100755 --- a/docs/collect-sections.py +++ b/docs/collect-sections.py @@ -9,7 +9,7 @@ from pathlib import Path __all__ = ["get_docs_section"] -DOCS_ROOT = "https://jwestman.pages.gitlab.gnome.org/blueprint-compiler" +DOCS_ROOT = "https://gnome.pages.gitlab.gnome.org/blueprint-compiler" sections: dict[str, "Section"] = {} diff --git a/docs/flatpak.rst b/docs/flatpak.rst index 86112cf..8081c8d 100644 --- a/docs/flatpak.rst +++ b/docs/flatpak.rst @@ -16,7 +16,7 @@ a module in your flatpak manifest: "sources": [ { "type": "git", - "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", + "url": "https://gitlab.gnome.org/GNOME/blueprint-compiler", "tag": "v0.16.0" } ] diff --git a/docs/index.rst b/docs/index.rst index a71b968..6cd130f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -59,7 +59,7 @@ Features Links ----- -- `Source code `_ +- `Source code `_ - `Workbench `_ lets you try, preview and export Blueprint - `GNOME Builder `_ provides builtin support - `Vim syntax highlighting plugin by thetek42 `_ diff --git a/docs/reference/extensions.rst b/docs/reference/extensions.rst index 0961d14..2fd5dbb 100644 --- a/docs/reference/extensions.rst +++ b/docs/reference/extensions.rst @@ -10,7 +10,7 @@ Properties are the main way to set values on objects, but they are limited by th Extensions are a feature of ``Gtk.Buildable``--see `Gtk.Buildable.custom_tag_start() `_ for internal details. - Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue `_. + Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue `_. .. rst-class:: grammar-block diff --git a/docs/setup.rst b/docs/setup.rst index 839f8f6..914c753 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -8,7 +8,7 @@ Setting up Blueprint on a new or existing project Using the porting tool ~~~~~~~~~~~~~~~~~~~~~~ -Clone `blueprint-compiler `_ +Clone `blueprint-compiler `_ from source. You can install it using ``meson _build`` and ``ninja -C _build install``, or you can leave it uninstalled. @@ -29,7 +29,7 @@ blueprint-compiler works as a meson subproject. [wrap-git] directory = blueprint-compiler - url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git + url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git revision = main depth = 1 diff --git a/docs/tutorial.rst b/docs/tutorial.rst new file mode 100644 index 0000000..7fdbf5a --- /dev/null +++ b/docs/tutorial.rst @@ -0,0 +1,481 @@ +======== +Tutorial +======== + +.. margin at column 75 + +Read this if you want to learn how to use Blueprint and never used +the XML syntax that can be read by GtkBuilder. + +For compatibility with Blueprint IDE extensions, blueprint files +should end with ``.blp``. + + +Namespaces +---------- + +Blueprint needs the widget library to be imported. These include Gtk, +Libadwaita, Shumate, etc. To import a namespace, write ``using`` followed +by the library and version number. + +.. code-block:: + + using Gtk 4.0; + using Adw 1; + +The Gtk import is required in all blueprints and the minor version +number must be 0. + + +Comments +-------- + +Blueprint has inline or multi-line comments + +.. code-block:: + + // This is an inline comment + /* This is + a multiline + comment */ + +Multi-line comments can't have inner multi-line comments. The compiler +will interpret the inner comment's closing token as the outer comment's +closing token. For example, the following will not compile: + +.. code-block:: + + // Bad comment below: + /* Outer comment + /* Inner comment */ + */ + + +Widgets +------- + +Create widgets in the following format: + +.. code-block:: + + Namespace.WidgetClass { + + } + +The Gtk namespace is implied for widgets, so you can just write the +widget class + +.. code-block:: + + Box { + + } + +Other namespaces must be written explicitly. + +.. code-block:: + + Adw.Leaflet { + + } + +Consult the widget library's documentation for a list of widgets. +A good place to start is +`the Gtk4 widget list `_. + +Naming Widgets +~~~~~~~~~~~~~~ + +Widgets can be given a **name/ID** so that they can be referenced by your +program or other widgets in the blueprint. + +.. code-block:: + + Namespace.WidgetClass widget_id { + + } + +Any time you want to use this widget as a property (more about that in the +next section) or something else, write the widget's **ID** (e.g. +``main_window``). + + +Properties +---------- + +Every widget has properties defined by their GObject class. +For example, the Libadwaita documentation lists the +`properties of the Toast class `_. +Write properties inside the curly brackets of a widget: + +.. code-block:: + + Namespace.WidgetClass { + property-name: value; + } + +Properties values are *all lowercase* (except strings) and must end with a +semicolon (``;``). + +Property Types +~~~~~~~~~~~~~~ + +These are the **types** of values that can be used in properties: + - Booleans: ``true``, ``false`` + - Numbers: e.g. ``1``, ``1.5``, ``-2``, ``-2.5`` + - Strings (single- or double-quoted): e.g. ``"a string"``, ``'another string'`` + - Enums + - Widgets + +Properties are **strongly typed**, so you can't use, for example, a string +for the orientation property, which requires an ``Orientation`` enum +vartiant as its value. + +Enum Properties +~~~~~~~~~~~~~~~ + +In the Gtk documentation, enum variants have long names and are +capitalized. For example, these are the +`Orientation `_ +enum variants: + + - GTK_ORIENTATION_HORIZONTAL + - GTK_ORIENTATION_VERTICAL + +In the blueprint, you would only write the *variant* part of the enum in +*lowercase*, just like you would in the XML. + +.. code-block:: + + Box { + orientation: horizontal; + } + +Widget Properties +~~~~~~~~~~~~~~~~~ + +Some widgets take other widgets as properties. For example, the +``Gtk.StackSidebar`` has a stack property which takes a ``Gtk.Stack`` widget. +You can create a new widget for the value, or you can reference another +widget by its **ID**. + +.. code-block:: + + StackSidebar { + stack: Stack { }; + } + +OR + +.. code-block:: + + StackSidebar { + stack: my_stack; + } + + Stack my_stack { + + } + +Note the use of a semicolon at the end of the property in both cases. +Inline widget properties are not exempt of this rule. + + +Property Bindings +----------------- + +If you want a widget's property to have the same value as another widget's +property (without hard-coding the value), you could ``bind`` two widgets' +properties of the same type. Bindings must reference a *source* widget by +**ID**. As long as the two properties have the same type, you can bind +properties of different names and of widgets with different widget classes. + +.. code-block:: + + Box my_box { + halign: fill; // Source + } + + Button { + valign: bind my_box.halign; // Target + } + +Binding Flags +~~~~~~~~~~~~~ + +Modify the behavior of bindings with flags. Flags are written after the +binding. The default behavior is that the *Target*'s value will be +changed to the *Source*'s value when the binding is created and when the +*Source* value changes. + +.. code-block:: + + Box my_box { + hexpand: true; // Source + } + + Button { + vexpand: bind my_box.hexpand inverted bidirectional; // Target + } + +no-sync-create + Prevent setting the *Tartget* with the *Source*'s value, + updating the target value when the *Source* value changes, not when + the binding is first created. Useful when the *Target* property has + another initial value that is not the *Source* value. + +bidirectional + When either the *Source* or *Target* value is modified, the other's + value will be updated. For example, if the logic of the program + changes the Button's vexpand value to ``false``, then the Box's halign + value will also be updated to ``false``. + +inverted + If the property is a boolean, the value of the bind can be negated + with this flag. For example, if the Box's hexpand property is ``true``, + the Button's vexpand property will be ``false`` in the code above. + + +Signals +------- + +Gtk allows you to register signals in your program. This can be done by +getting the object from the GtkBuilder and connecting a handler to the +signal. Or register the handler with the application and reference it in +the blueprint. + +Signals have an *event name*, a *handler* (aka callback), and optionally +some *flags*. Each widget will have a set of defined signals. Consult the +widget's documentation for a list of its signals. + +To register a handler with the application, consult the documentation for +your language's bindings of Gtk. + +.. code-block:: + + WidgetClass { + event_name => handler_name() flags; + } + +.. TODO: add a list of flags and their descriptions + +By default, signals in the blueprint will pass the widget that the signal +is for as an argument to the *handler*. However, you can specify the +widget that is passed to the handler by referencing its **ID** inside the +parenthesis. + +.. code-block:: + + Label my_label { + label: "Hide me"; + } + + Button { + clicked => hide_widget(my_label); + } + + +Custom Widget Classes +--------------------- + +Some programs have custom widgets defined in their logic, so blueprint +won't know that they exist. Writing widgets not defined in the GIR will +result in an error. Prepend a custom widget with a period (``.``) to prevent the +compiler from trying to validate the widget. This is essentially saying +the widget has no *namespace*. + +To register a custom widget with the application consult the documentation +for your language's bindings of Gtk. + +.. code-block:: + + .MyCustomWidget { + + } + + +Templates +--------- +.. TODO + + +CSS Style Classes +----------------- + +.. TODO: Unsure if to group styles with widget-specific items + +Widgets can be given style classes that can be used with your CSS or +`predefined styles `_ +in libraries like Libadwaita. + +.. code-block:: + + Button { + label: "Click me"; + styles ["my-style", "pill"] + } + +Note the lack of a *colon* after "styles" and a *semicolon* at the end of +the line. This syntax looks like the properties syntax, but it compiles to +XML completely different from properties. + +Consult your language's bindings of Gtk to use a CSS file. + +Non-property Elements +~~~~~~~~~~~~~~~~~~~~~ + +Some widgets will have elements which are not properties, but they sort +of act like properties. Most of the time they will be specific only to a +certain widget. *Styles* is one of these elements, except that styles can +be used for any widget. Similar to how every widget has styles, +``Gtk.ComboBoxText`` has *items*: + +.. code-block:: + + Gtk.ComboBoxText { + items [ + item1: "Item 1", + item2: _("Items can be translated"), + "The item ID is not required", + ] + } + +See :doc:`examples ` for a list of more of these +widget-specific items. + + +Menus +----- + +Menus are usually the widgets that are placed along the top-bar of a +window, or pop up when you right-click some other widget. In Blueprint, a +``menu`` is a ``Gio.MenuModel`` that can be shown by MenuButtons or other +widgets. + +In Blueprint, a ``menu`` can have *items*, *sections*, and *submenus*. +Like widgets, a ``menu`` can also be given an **ID**. +The `Menu Model section of the Gtk.PopoverMenu documentation `_ +has complete details on the menu model. + +Here is an example of a menu: + +.. code-block:: + + menu my_menu { + section { + label: "File"; + item { + label: "Open"; + action: "win.open"; + icon-name: "document-open-symbolic"; + } + item { + label: "Save"; + action: "win.save"; + icon-name: "document-save-symbolic"; + } + submenu { + label: "Save As"; + icon-name: "document-save-as-symbolic"; + item { + label: "PDF"; + action: "win.save_as_pdf"; + } + } + } + } + +There is a shorthand for *items*. Items require at least a label. The +action and icon-name are optional. + +.. code-block:: + + menu { + item ( "Item 2" ) + item ( "Item 2", "app.action", "icon-name" ) + } + +A widget that uses a ``menu`` is ``Gtk.MenuButton``. It has the *menu-model* +property, which takes a menu. Write the menu at the root of the blueprint +(meaning not inside any widgets) and reference it by **ID**. + +.. code-block:: + + MenuButton { + menu-model: my_menu; + } + + +Child Types +----------- + +Child types describe how a child widget is placed on a parent widget. For +example, HeaderBar widgets can have children placed either at the *start* +or the *end* of the HeaderBar. Child widgets of HeaderBars can have the +*start* or *end* types. Values for child types a widget can have are +defined in the widget's documentation. + +Child types in blueprint are written between square brackets (``[`` ``]``) and before +the child the type is for. + +The following blueprint code... + +.. code-block:: + + HeaderBar { + [start] + Button { + label: "Button"; + } + } + +\... would look like this: + +.. code-block:: + + --------------------------- + | Button | + --------------------------- + +And the following blueprint code... + +.. code-block:: + + HeaderBar { + [end] + Button { + label: "Button"; + } + } + +\... would look like this: + +.. code-block:: + + --------------------------- + | Button | + --------------------------- + + +Translatable Strings +-------------------- + +Mark any string as translatable using this syntax: ``_("...")``. + +Two strings that are the same in English could be translated in different +ways in other languages because of different *contexts*. Translatable +strings with context look like this: ``C_("context", "...")``. An example +where a context is needed is the word "have", which in Spanish could +translate to "tener" or "haber". + +.. code-block:: + + Label { + label: C_("1st have", "have"); + } + + Label { + label: C_("2nd have", "have"); + } + +See `translations `_ for more details. diff --git a/tests/samples/issue_187.ui b/tests/samples/issue_187.ui new file mode 100644 index 0000000..941a00f --- /dev/null +++ b/tests/samples/issue_187.ui @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/tests/samples/issue_187_dec.blp b/tests/samples/issue_187_dec.blp new file mode 100644 index 0000000..30b997c --- /dev/null +++ b/tests/samples/issue_187_dec.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +template ListItem { + child: Label { + label: bind template.item as <$RecentObject>.filename; + }; +} diff --git a/tests/samples/issue_195.blp b/tests/samples/issue_195.blp new file mode 100644 index 0000000..50b5b95 --- /dev/null +++ b/tests/samples/issue_195.blp @@ -0,0 +1,11 @@ +using Gtk 4.0; +using Adw 1; + +Adw.AlertDialog dialog1 { + responses [ + ok: "Ok", + cancel: "Cancel", + ] +} + +Button cancel {} diff --git a/tests/samples/issue_195.ui b/tests/samples/issue_195.ui new file mode 100644 index 0000000..b57a379 --- /dev/null +++ b/tests/samples/issue_195.ui @@ -0,0 +1,16 @@ + + + + + + + Ok + Cancel + + + + \ No newline at end of file diff --git a/tests/samples/signal_template_object.blp b/tests/samples/signal_template_object.blp new file mode 100644 index 0000000..16dd5a0 --- /dev/null +++ b/tests/samples/signal_template_object.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +template $MyTemplate { + Button { + clicked => $my_signal_handler(template); + } +} diff --git a/tests/samples/signal_template_object.ui b/tests/samples/signal_template_object.ui new file mode 100644 index 0000000..c9a680a --- /dev/null +++ b/tests/samples/signal_template_object.ui @@ -0,0 +1,16 @@ + + + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 1f56eb6..0807d65 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -181,11 +181,7 @@ class TestSamples(unittest.TestCase): def test_samples(self): # list the samples directory - samples = [ - f.stem - for f in Path(__file__).parent.glob("samples/*.blp") - if not f.stem.endswith("_dec") - ] + samples = [f.stem for f in Path(__file__).parent.glob("samples/*.blp")] samples.sort() for sample in samples: REQUIRE_ADW_1_4 = ["adw_breakpoint"] @@ -202,6 +198,7 @@ class TestSamples(unittest.TestCase): "parseable", "signal", "signal_not_swapped", + "signal_template_object", "template", "template_binding", "template_binding_extern", @@ -215,7 +212,7 @@ class TestSamples(unittest.TestCase): ] # Decompiler-only tests - SKIP_COMPILE = ["issue_177", "translator_comments"] + SKIP_COMPILE = ["issue_177", "issue_187", "translator_comments"] SKIP_DECOMPILE = [ # Comments are not preserved in either direction @@ -228,7 +225,7 @@ class TestSamples(unittest.TestCase): continue with self.subTest(sample): - if sample not in SKIP_COMPILE: + if sample not in SKIP_COMPILE and not sample.endswith("_dec"): self.assert_sample(sample, skip_run=sample in SKIP_RUN) with self.subTest("decompile/" + sample):