Workbench icon indicating copy to clipboard operation
Workbench copied to clipboard

Define Gtk closure expressions in code when the UI file is NOT a template.

Open rolandlo opened this issue 2 years ago • 9 comments

Suppose we have a .blp-file that defines a template. Then we can use Gtk closure expression defined in the (say javascript) code like this for example:

UI file main.blp

using Gtk 4.0;
using Adw 1;

template $CustomWidget : Adw.PreferencesGroup {
  Adw.ComboRow cb {
    title: _("Make a selection:");
    
    model:
    StringList {
      strings ["One", "Two", "Three", "Four"]
    };
  }
  
  Adw.SwitchRow sw1 {  // should only be visible when option "One", "Two" or "Three" is selected.
    title: _("Switch one");
    active: true;
    visible: bind $onlyOneTwoThree(cb.selected-item) as <bool>;
  }
  
  Adw.SwitchRow sw2 { // should only be visible when option "Three" or "Four" is selected
    title: _("Switch two");
    visible: bind $onlyThreeFour(cb.selected-item) as <bool>;
  }
  
  Adw.ActionRow {
    title: _("Status:");
    Label { // should display some status message depending on sw1.visible, sw2.visible and cb.selected-item
      label: bind $msg(sw1.visible, sw2.visible, cb.selected-item) as <string>;
    }
  }
 }

Code file main.js

import GObject from "gi://GObject";
import Gtk from "gi://Gtk";
import Adw from "gi://Adw";

const CustomWidget = GObject.registerClass(
  {
    GTypeName: "CustomWidget",
    Template: workbench.template,
  },
  class CustomWidget extends Adw.PreferencesGroup {
    onlyOneTwoThree(_self, item) {
      const str = item.string;
      return str === "One" || str === "Two" || str === "Three";
    }
    onlyThreeFour(_self, item) {
      const str = item.string;
      return str === "Three" || str === "Four";
    }
    msg(_self, active1, active2, item) {
      const str = item.string;
      if (active1 && active2) {
        return `"${str}" was selected, 2 visible`;
      } else if (active1 || active2) {
        return `"${str}" was selected, 1 visible`;
      } else {
        return "0 active. Discard selection";
      }
    }
  },
);

const box = new Gtk.Box({ orientation: "vertical" });
box.append(new CustomWidget());
workbench.preview(box);

Now suppose I want to have the same behavior in case that the UI file does NOT define a template (remove

template $CustomWidget : 

from the fourth line of main.blp)

How can we define the Gtk closure expressions onlyOneTwoThree, onlyThreeFour and msg in the code? There doesn't seem to be an easy way to do so. According to @andyholmes (conversation in the matrix channel) these closures should be added to Workbench's builder scope in some way (compare gjs guide), yet apparently Workbench is not set up to use closures at the moment.

So I'd like to request Workbench to support closures in a way that it is easy to use and documented (maybe in a Library demo).

rolandlo avatar Oct 03 '23 21:10 rolandlo

Thanks for filing a clear issue. This is something I've wanted as well.

Related, during a Workshop someone was confused about the lack of signal handlers. https://floss.social/@sonny/110816126000858204

The main issues are:

  • We need a simple API for it and it doesn't exist in GTK currently
  • I would like this API to be exactly or as close as possible to what you would use once moving away from Workbench

I've been proposing the following for GJS https://gitlab.gnome.org/GNOME/gjs/-/merge_requests/853

Let's move forward, here is a plan:

  1. [x] Add the API in Workbench but don't use it in Library and make it log a warning "Experimental stuff"
  2. [ ] Make the API available in Rust and Vala as well
  3. [ ] See where we go from there and how we can "upstream" this API
  4. [ ] Expose the API and use it in Library

sonnyp avatar Oct 14 '23 13:10 sonnyp

@rolandlo are you able to test / use Workbench devel (branch main)?

If you're gonna use it, please do share feedback and thoughts. Thanks!

The experimental workbench.build API can be use like so:

Blueprint

using Gtk 4.0;
using Adw 1;

Adw.PreferencesGroup preferences_group {
  Adw.ComboRow cb {
    title: _("Make a selection:");
    
    model:
    StringList {
      strings ["One", "Two", "Three", "Four"]
    };
  }
  
  Adw.SwitchRow sw1 {  // should only be visible when option "One", "Two" or "Three" is selected.
    title: _("Switch one");
    active: true;
    visible: bind $onlyOneTwoThree(cb.selected-item) as <bool>;
  }
  
  Adw.SwitchRow sw2 { // should only be visible when option "Three" or "Four" is selected
    title: _("Switch two");
    visible: bind $onlyThreeFour(cb.selected-item) as <bool>;
  }
  
  Adw.ActionRow {
    title: _("Status:");
    Label { // should display some status message depending on sw1.visible, sw2.visible and cb.selected-item
      label: bind $msg(sw1.visible, sw2.visible, cb.selected-item) as <string>;
    }
  }
  
  Button {
    label: "Signal handler";
    clicked => $onButtonClicked();
  }
}

JavaScript

import GObject from "gi://GObject";
import Gtk from "gi://Gtk";
import Adw from "gi://Adw";

const { preferences_group } = workbench.build({
  onlyOneTwoThree(_self, item) {
    const str = item.string;
    return str === "One" || str === "Two" || str === "Three";
  },
  onlyThreeFour(_self, item) {
    const str = item.string;
    return str === "Three" || str === "Four";
  },
  msg(_self, active1, active2, item) {
    log("msg");
    const str = item.string;
    if (active1 && active2) {
      return `"${str}" was selected, 2 visible`;
    } else if (active1 || active2) {
      return `"${str}" was selected, 1 visible`;
    } else {
      return "0 active. Discard selection";
    }
  },
  onButtonClicked(...args) {
    console.log(...args);
  },
});

workbench.preview(preferences_group);

sonnyp avatar Oct 21 '23 11:10 sonnyp

@rolandlo are you able to test / use Workbench devel (branch main)?

Yes, I can test the main branch. It works great in the example. The syntax is nice. However, how would the JS code look for a Blueprint file like this:

using Gtk 4.0;
using Adw 1;

Adw.PreferencesWindow preferences_window {
  Adw.PreferencesGroup preferences_group {  
    Button {
      label: "Signal handler";
      clicked => $onButtonClicked();
    }
  }
}

With the obvious looking

import GObject from "gi://GObject";
import Gtk from "gi://Gtk";
import Adw from "gi://Adw";

const { preferences_window } = workbench.build({
  onButtonClicked,
});

function onButtonClicked(...args) {
  console.log(...args);
}

workbench.preview(preferences_window);

I get a message grafik when clicking on the button.

rolandlo avatar Oct 23 '23 16:10 rolandlo

You need to press the "Run" button first. Related https://github.com/sonnyp/Workbench/issues/643

sonnyp avatar Oct 23 '23 16:10 sonnyp

I did, but that doesn't help. Note that when I change Adw.PreferencesWindow to Box it also works.

rolandlo avatar Oct 23 '23 16:10 rolandlo

I'm not sure why that is but preferences_window.present() instead of workbench.preview(preferences_window) seems to work.

sonnyp avatar Oct 23 '23 16:10 sonnyp

Right, it does. I will do further tests then.

rolandlo avatar Oct 23 '23 16:10 rolandlo

Everything works fine now with what I wanted to achieve with this feature request. See the redesigned Xournal++ preferences window code/UI in case you'd like to check it out.

rolandlo avatar Oct 24 '23 08:10 rolandlo

@rolandlo that's great to hear.

I'm glad Workbench is useful to Xournal++

I'll leave this open as there are a couple of things left to do here.

sonnyp avatar Nov 05 '23 13:11 sonnyp