How to filter nested items in Gtk.TreeListModel in Gtk 4

,

There are few examples on creating Treeviews in Gtk4. I have managed to create the Treeview below in JavaScript with one nesting level. The problem is in the filtering. I don’t want to filter out those items with nested items. And the nested items are not being filtered in the code below.

The basic code is here. How do I implement the above feature in this example.

#! usr/bin/env/gjs -m gjs

import Gtk from "gi://Gtk?version=4.0";
import GLib from "gi://GLib";
import GObject from "gi://GObject";
import Gio from "gi://Gio";

Gtk.init();

const options = {
  GtypeName: "MyObject",
  Properties: {
    string: GObject.ParamSpec.string(
      "string",
      "String",
      "A string",
      GObject.ParamFlags.READWRITE,
      "0"
    ),
    hasChildren: GObject.ParamSpec.boolean(
      "hasChildren",
      "Whether object has children",
      "Whether object has children",
      GObject.ParamFlags.READWRITE,
      0
    ),
  },
  Signals: {},
};

const MyObject = GObject.registerClass(
  options,
  class MyString extends GObject.Object {
    constructor(string = "", hasChildren = false) {
      super();
      this.string = string;
      this.hasChildren = hasChildren;
    }
  }
);

const loop = GLib.MainLoop.new(null, false);
const store = Gio.ListStore.new(MyObject);

for (let i = 0; i < 20; i++) {
  store.append(new MyObject(i.toString(), i % 2 ? false : true));
}

const propertyExpression = Gtk.PropertyExpression.new(MyObject, null, "string");

const stringFilter = Gtk.StringFilter.new(propertyExpression);
const filter = Gtk.FilterListModel.new(store, stringFilter);

const tree = Gtk.TreeListModel.new(filter, false, false, (item) => {
  if (item.hasChildren) {
    const nestedModel = Gio.ListStore.new(MyObject);

    for (const letter of "aei") {
      nestedModel.append(new MyObject(letter));
    }

    return nestedModel;
  }

  return null;
});

const selection = Gtk.SingleSelection.new(tree);

const factory = new Gtk.SignalListItemFactory();

factory.connect("setup", (_, listItem) => {
  listItem.child = new Gtk.TreeExpander({
    child: new Gtk.Label(),
    indent_for_icon: true,
  });
});

factory.connect("bind", (_, listItem) => {
  const listRow = listItem.item;
  const expander = listItem.child;

  expander.list_row = listRow;

  const label = expander.child;
  const object = listRow.item;
  label.label = object.string;
});

const listView = new Gtk.ListView({
  model: selection,
  factory,
  show_separators: false,
});
const scrolledWin = new Gtk.ScrolledWindow({
  child: listView,
  min_content_width: 300,
  min_content_height: 300,
});

const win = new Gtk.Window({
  defaultWidth: 600,
  defaultHeight: 550,
  title: "Tree View",
});

const vBox = new Gtk.Box({
  orientation: Gtk.Orientation.VERTICAL,
  halign: Gtk.Align.CENTER,
  valign: Gtk.Align.CENTER,
  spacing: 18,
  margin_top: 18,
  margin_bottom: 18,
  margin_start: 18,
  margin_end: 18,
});

const entry = new Gtk.Entry({
  primary_icon_name: "edit-find-symbolic",
  placeholder_text: "Search",
});

entry.buffer.connect("notify::length", (buffer) => {
  const text = buffer.get_text();
  console.log(text);
  stringFilter.set_search(text);
});

vBox.append(entry);
vBox.append(scrolledWin);

win.child = vBox;
win.present();

win.connect("close-request", () => {
  loop.quit();
});

loop.run();

If anybody is facing a similar problem, I think the best solution is to use Gtk.CustomFilter instead of Gtk.StringFilter. Below is the modification of the code above to use CustomFilter.

#! usr/bin/env/gjs -m gjs

import Gtk from "gi://Gtk?version=4.0";
import GLib from "gi://GLib";
import GObject from "gi://GObject";
import Gio from "gi://Gio";

Gtk.init();

const options = {
  GtypeName: "MyObject",
  Properties: {
    string: GObject.ParamSpec.string(
      "string",
      "String",
      "A string",
      GObject.ParamFlags.READWRITE,
      "0"
    ),
    hasChildren: GObject.ParamSpec.boolean(
      "hasChildren",
      "Whether object has children",
      "Whether object has children",
      GObject.ParamFlags.READWRITE,
      0
    ),
  },
  Signals: {},
};

const MyObject = GObject.registerClass(
  options,
  class MyString extends GObject.Object {
    constructor(string = "", hasChildren = false) {
      super();
      this.string = string;
      this.hasChildren = hasChildren;
    }
  }
);

const loop = GLib.MainLoop.new(null, false);
const store = Gio.ListStore.new(MyObject);

for (let i = 0; i < 20; i++) {
  store.append(new MyObject(i.toString(), i % 2 ? false : true));
}

const customFilter = Gtk.CustomFilter.new(null);

function getCustomfilter(string) {
  return (item) => {
    if (item.hasChildren) return true;
    return item.string.includes(string);
  };
}

const filter = Gtk.FilterListModel.new(store, customFilter);

const tree = Gtk.TreeListModel.new(filter, false, false, (item) => {
  if (item.hasChildren) {
   
    const nestedStore = Gio.ListStore.new(MyObject)
    const nestedModel =  Gtk.FilterListModel.new(nestedStore, customFilter);

    for (const letter of "aei") {
      nestedModel.model.append(new MyObject(letter));
    }

    return nestedModel;
  }

  return null;
});

const selection = Gtk.SingleSelection.new(tree);

const factory = new Gtk.SignalListItemFactory();

factory.connect("setup", (_, listItem) => {
  listItem.child = new Gtk.TreeExpander({
    child: new Gtk.Label(),
    indent_for_icon: true,
  });
});

factory.connect("bind", (_, listItem) => {
  const listRow = listItem.item;
  const expander = listItem.child;

  expander.list_row = listRow;

  const label = expander.child;
  const object = listRow.item;
  label.label = object.string;
});

const listView = new Gtk.ListView({
  model: selection,
  factory,
  show_separators: false,
});
const scrolledWin = new Gtk.ScrolledWindow({
  child: listView,
  min_content_width: 300,
  min_content_height: 300,
});

const win = new Gtk.Window({
  defaultWidth: 600,
  defaultHeight: 550,
  title: "Tree View",
});

const vBox = new Gtk.Box({
  orientation: Gtk.Orientation.VERTICAL,
  halign: Gtk.Align.CENTER,
  valign: Gtk.Align.CENTER,
  spacing: 18,
  margin_top: 18,
  margin_bottom: 18,
  margin_start: 18,
  margin_end: 18,
});

const entry = new Gtk.Entry({
  primary_icon_name: "edit-find-symbolic",
  placeholder_text: "Search",
});

entry.buffer.connect("notify::length", (buffer) => {
  const text = buffer.get_text();
  customFilter.set_filter_func(getCustomfilter(text));
});

vBox.append(entry);
vBox.append(scrolledWin);

win.child = vBox;
win.present();

win.connect("close-request", () => {
  loop.quit();
});

loop.run();