Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composite template support for Vala, Rust, Python + "Custom Widget" demo #823

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/Library/demos/Custom Widget/code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use crate::workbench;
use gtk::{glib, subclass::prelude::*};

mod imp {
use super::*;

#[derive(Debug, Default, gtk::CompositeTemplate)]
// The file will be provided by Workbench when the demo compiles, it just contains the template.
#[template(file = "workbench_template.ui")]
pub struct AwesomeButton {}

#[glib::object_subclass]
impl ObjectSubclass for AwesomeButton {
const NAME: &'static str = "AwesomeButton";
type Type = super::AwesomeButton;
type ParentType = gtk::Button;

fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}

fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}

#[gtk::template_callbacks]
impl AwesomeButton {
#[template_callback]
fn onclicked(_button: &gtk::Button) {
println!("Clicked")
}
}

impl ObjectImpl for AwesomeButton {}
impl WidgetImpl for AwesomeButton {}
impl ButtonImpl for AwesomeButton {}
}

glib::wrapper! {
pub struct AwesomeButton(ObjectSubclass<imp::AwesomeButton>) @extends gtk::Widget, gtk::Button;
}

impl AwesomeButton {
pub fn new() -> Self {
glib::Object::new()
}
}

pub fn main() {
gtk::init().unwrap();

let container = gtk::ScrolledWindow::new();
let flow_box = gtk::FlowBox::builder().hexpand(true).build();
container.set_child(&flow_box);

let mut widgets = Vec::with_capacity(100);
for _ in 0..100 {
widgets.push(AwesomeButton::new());
}
for widget in &widgets {
flow_box.append(widget);
}

workbench::preview(&container)
}
28 changes: 28 additions & 0 deletions src/Library/demos/Custom Widget/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import gi

gi.require_version("Gtk", "4.0")

from gi.repository import Gtk
import workbench


@Gtk.Template(string=workbench.template)
class AwesomeButton(Gtk.Button):
# This is normally just "AwesomeButton" as defined in the XML/Blueprint.
# In your actual code, just put that here. We need to do it like this for technical reasons.
__gtype_name__ = workbench.template_gtype_name

@Gtk.Template.Callback()
def onclicked(self, _button):
print("Clicked")


container = Gtk.ScrolledWindow()
flow_box = Gtk.FlowBox(hexpand=True)
container.set_child(flow_box)

for _ in range(100):
widget = AwesomeButton()
flow_box.append(widget)

workbench.preview(container)
25 changes: 25 additions & 0 deletions src/Library/demos/Custom Widget/main.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env -S vala workbench.vala workbench.Resource.c --gresources=workbench_demo.xml --pkg gtk4

// The resource will be provided by Workbench when the demo compiles, see shebang above.
[GtkTemplate (ui = "/re/sonny/Workbench/demo/workbench_template.ui")]
public class AwesomeButton : Gtk.Button {
[GtkCallback]
private void onclicked (Gtk.Button button) {
message ("Clicked");
}
}

public void main () {
var container = new Gtk.ScrolledWindow();
var flow_box = new Gtk.FlowBox() {
hexpand = true
};
container.set_child(flow_box);

for (var i = 0; i < 100; i++) {
var widget = new AwesomeButton();
flow_box.append(widget);
}

workbench.preview(container);
}
17 changes: 15 additions & 2 deletions src/Previewer/External.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Adw from "gi://Adw";
import dbus_previewer from "./DBusPreviewer.js";
import { decode } from "../util.js";

export default function External({ output, builder, onWindowChange }) {
const stack = builder.get_object("stack_preview");
Expand Down Expand Up @@ -57,9 +58,21 @@ export default function External({ output, builder, onWindowChange }) {
.catch(console.error);
}

async function updateXML({ xml, target_id, original_id }) {
async function updateXML({
xml,
target_id,
original_id,
template_gtype_name,
template,
}) {
try {
await dbus_proxy.UpdateUiAsync(xml, target_id, original_id || "");
await dbus_proxy.UpdateUiAsync(
xml,
target_id,
original_id || "",
template_gtype_name || "",
template ? decode(template) : "",
);
} catch (err) {
console.debug(err);
}
Expand Down
26 changes: 18 additions & 8 deletions src/Previewer/Previewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,16 @@ export default function Previewer({
let tree;
let original_id;
let template;
let template_gtype_name;

if (!text) {
text = `<?xml version="1.0" encoding="UTF-8"?><interface><object class="GtkBox"></object></interface>";`;
}

try {
tree = xml.parse(text);
({ target_id, text, original_id, template } = targetBuildable(tree));
({ target_id, text, original_id, template, template_gtype_name } =
targetBuildable(tree));
} catch (err) {
// console.error(err);
console.debug(err);
Expand Down Expand Up @@ -224,17 +226,20 @@ export default function Previewer({
}
dropdown_preview_align.visible = !!template;

await current.updateXML({
const update_xml_params = {
xml: text,
builder,
object_preview,
target_id,
original_id,
template,
});
template_gtype_name,
};
await current.updateXML(update_xml_params);
code_view_css.clearDiagnostics();
await current.updateCSS(code_view_css.buffer.text);
symbols = null;
return update_xml_params;
}

const schedule_update = unstack(update, console.error);
Expand Down Expand Up @@ -369,21 +374,20 @@ function getTemplate(tree) {
const original = tree.toString();
tree.remove(template);

// Insert a dummy target back in.
const target_id = makeWorkbenchTargetId();
const el = new xml.Element("object", {
class: parent,
class: "GtkBox",
id: target_id,
});
template.children.forEach((child) => {
el.cnode(child);
});
tree.cnode(el);

return {
target_id: el.attrs.id,
text: tree.toString(),
original_id: undefined,
template: encode(original),
template_gtype_name: template.attrs.class,
};
}

Expand All @@ -409,7 +413,13 @@ function targetBuildable(tree) {
const target_id = makeWorkbenchTargetId();
child.attrs.id = target_id;

return { target_id, text: tree.toString(), original_id, template: null };
return {
target_id,
text: tree.toString(),
original_id,
template: null,
template_gtype_name: null,
};
}

function makeSignalHandler(
Expand Down
57 changes: 57 additions & 0 deletions src/Previewer/TemplateResource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Gio from "gi://Gio";
import { encode } from "../util.js";

function files(demoDirectory) {
return {
template_ui: demoDirectory.get_child("workbench_template.ui"),
gresource: demoDirectory.get_child("workbench_demo.xml"),
};
}

function generateGresource(templateName) {
return `<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/re/sonny/Workbench/demo">
<file>${templateName}</file>
</gresource>
</gresources>`;
}

/**
* Utilities for writing composite templates to the demo session directory and generating a gresource file for it.
* This can be used by external previewers that need this information at compile time and can not retrieve the
* template via D-Bus + the previewer process.
*/
const template_resource = {
async generateTemplateResourceFile(sessionDirectory) {
const { template_ui, gresource } = files(sessionDirectory);
await gresource.replace_contents_async(
encode(generateGresource(template_ui.get_basename())),
null,
false,
Gio.FileCreateFlags.NONE,
null,
);
return gresource.get_path();
},
async writeTemplateUi(sessionDirectory, templateContents) {
if (templateContents === null) {
// If we don't have a template, we still generate an empty file to (try to) avoid confusing compiler errors if
// the user tries to access the file via gresource.
templateContents = encode(
'<?xml version="1.0" encoding="UTF-8"?><interface></interface>',
);
}

const { template_ui } = files(sessionDirectory);
await template_ui.replace_contents_async(
encode(templateContents),
null,
false,
Gio.FileCreateFlags.NONE,
null,
);
},
};

export default template_resource;
9 changes: 8 additions & 1 deletion src/Previewer/previewer.vala
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ namespace Workbench {
typeof (WebKit.WebView).ensure();
}

public void update_ui (string content, string target_id, string original_id = "") {
public void update_ui (string content, string target_id, string original_id = "", string template_gtype_name = "", string template = "") {
// we don't use template: This is compiled as a gresource for Vala and for Rust it's read directly
// and compiled from a path to the UI file. See Rust/Vala compiler.

this.ensure_types();
this.builder = new Gtk.Builder.from_string (content, content.length);
var target = this.builder.get_object (target_id) as Gtk.Widget;
Expand Down Expand Up @@ -188,6 +191,10 @@ namespace Workbench {
Gtk.Window.set_interactive_debugging (enabled);
}

public void preview() {

}

public Adw.ColorScheme ColorScheme { get; set; default = Adw.ColorScheme.DEFAULT; }

public signal void window_open (bool open);
Expand Down
2 changes: 2 additions & 0 deletions src/Previewer/previewer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<arg type="s" name="content" direction="in"/>
<arg type="s" name="target_id" direction="in"/>
<arg type="s" name="original_id" direction="in"/>
<arg type="s" name="template_gtype_name" direction="in"/>
<arg type="s" name="template" direction="in"/>
</method>
<method name="UpdateCss">
<arg type="s" name="content" direction="in"/>
Expand Down
28 changes: 26 additions & 2 deletions src/langs/python/python-previewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class Previewer:
builder: Gtk.Builder | None
target: Gtk.Widget | None
css: Gtk.CssProvider | None
template: str | None
template_gtype_name: str | None
uri = str | None
style_manager: Adw.StyleManager

Expand All @@ -60,11 +62,22 @@ def __init__(self):
self.window = None
self.builder = None
self.target = None
self.template = None
self.template_gtype_name = None
self.uri = None

@DBusTemplate.Method()
def update_ui(self, content: str, target_id: str, original_id: str = ""):
def update_ui(
self,
content: str,
target_id: str,
original_id: str = "",
template_gtype_name: str = "",
template: str = "",
):
self.builder = Gtk.Builder.new_from_string(content, len(content))
self.template = template
self.template_gtype_name = template_gtype_name
target = self.builder.get_object(target_id)
if target is None:
print(
Expand Down Expand Up @@ -233,6 +246,11 @@ def on_css_parsing_error(self, _css, section: Gtk.CssSection, error: GLib.Error)
def resolve(self, path: str):
return Gio.File.new_for_uri(self.uri).resolve_relative_path(path).get_uri()

def preview(self, widget: Gtk.Widget):
self.target = widget
self.ensure_window()
self.window.set_child(widget)


# 3. API for demos
# ----------------
Expand All @@ -247,7 +265,7 @@ def __init__(self, previewer: Previewer):
self._previewer = previewer

def __getattr__(self, name):
# Getting `window` or `builder` will transparently forward to calls
# Getting `window` `builder`, etc. will transparently forward to calls
# `window`/`builder` attributes of the previewer.

# We do this to make the API in the demos a bit simpler. Just using a normal module's dict and
Expand All @@ -261,6 +279,12 @@ def __getattr__(self, name):
return self._previewer.builder
if name == "resolve":
return self._previewer.resolve
if name == "template":
return self._previewer.template
if name == "template_gtype_name":
return self._previewer.template_gtype_name
if name == "preview":
return self._previewer.preview
raise KeyError


Expand Down
Loading