GridView expandable sections

Hi,

I’m trying to show my data in a grid like appearance, and being able to group them by an arbitrary category, so I tried both FlowBox and GridView. The group title would be an Expaander with the FlowBox/GridView as a child.

With FlowBox, I could have the exact result I wanted however I hit performance issues when there is more than 500 items (or 10,000 if I don’t load images).

With GridView, I couldn’t achieve the result I want since I have to put them inside a ScrolledWindow and each GridView would have his own scrollbar. So in the end, there would be one scrollbar for the Window with the list of groups, and a scrollbar for each gridview. I heard about the scroll policy GTK_POLICY_EXTERNAL but it just hided the scrollbar, didn’t merge with the parent one. How would it be possible to have a list of gridview with a single scrollbar (if ever possible)?

I’m, not sure if it’s the correct approach since it would then require lots of event handling to support multi selection across the grids, arrow navigation, etc.

I initially asked for guidance on the following reddit post and was redirected here and thanks to the help of u/Netblock.

Any help would be highly appreciated. I’m stuck on this for a while and can’t find solutions.

Here is the current testing in workbench

using Gtk 4.0;
Box box {}
import GObject from "gi://GObject";
import Gio from "gi://Gio";
import Gtk from "gi://Gtk";

class GamesGroupClass extends GObject.Object {
  title;
  games;

  constructor(title, games) {
    super();

    this.title = title;
    this.games = games;
  }
}
const GamesGroup = GObject.registerClass(
  {
    GTypeName: "GamesGroup",
  },
  GamesGroupClass,
);

class GameClass extends GObject.Object {
  title;

  constructor(title) {
    super();
    this.title = title;
  }
}
const Game = GObject.registerClass(
  {
    GTypeName: "Game",
  },
  GameClass,
);

function generateModels() {
  const groups = [];

  const startCharCode = "A".charCodeAt(0);
  const endCharCode = "Z".charCodeAt(0);

  for (let charCode = startCharCode; charCode <= endCharCode; charCode++) {
    const title = String.fromCharCode(charCode);
    const items = [];
    for (let i = 1; i <= 1000; i++) {
      items.push(new Game(`Item ${title}${i}`));
    }
    const itemsModel = new Gio.ListStore();
    itemsModel.splice(0, 0, items);

    const gamesGroup = new GamesGroup(title, itemsModel);
    groups.push(gamesGroup);
  }

  const model = new Gio.ListStore();
  model.splice(0, 0, groups);

  return model;
}

const model = generateModels();
const noSelectionModel = new Gtk.NoSelection({ model: model });
const listView = new Gtk.ListView();
const listFactory = new Gtk.SignalListItemFactory();
listFactory.connect("setup", (_, listItem) => {
  const gridView = new Gtk.GridView();
  const gridFactory = new Gtk.SignalListItemFactory();
  gridFactory.connect("setup", (_, listItem) => {
    const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL });
    box.append(new Gtk.Picture({ height_request: 300, width_request: 200 }));
    box.append(new Gtk.Label());
    listItem.child = box;
  });
  gridFactory.connect("bind", (_, listItem) => {
    const game = listItem.item;
    const box = listItem.child;
    const picture = box.get_first_child();
    const label = box.get_last_child();
    label.label = game.title;
  });
  gridView.factory = gridFactory;

  const expander = new Gtk.Expander({
    child: new Gtk.ScrolledWindow({
      child: gridView,
      // If I didn't give a height, it would be like 20px height
      height_request: 600,
    }),
    expanded: true,
  });
  listItem.child = expander;
});
listFactory.connect("bind", (_, listItem) => {
  const gamesGroup = listItem.item;
  const expander = listItem.child;
  expander.label = gamesGroup.title;

  const scrolledWindow = expander.child;
  const gridView = scrolledWindow.child;
  gridView.model = new Gtk.MultiSelection({ model: gamesGroup.games });

  // Here it would load the image async (and cancel it in the unbind)
});
listView.factory = listFactory;
listView.model = noSelectionModel;

const box = workbench.builder.get_object("box");
box.append(
  new Gtk.ScrolledWindow({
    child: listView,
    hexpand: true,
    halign: Gtk.Align.FILL,
  }),
);

I allow myself to bump this thread since it’s been 3 weeks. I couldn’t do any progress in this. If someone happened to achieve it in the future, please let me know :slight_smile:

Okay, this question is relatively complex because it involves several elements. I did some tests and you can add a GridView directly as a child of an Expander without having to place it inside a ScrolledWindow.

In your code, you used a ListView as a base and at the moment I don’t have time to do that but using a simple Box as a base widget it is possible to make the following structure to have the GridView using the scroll bar of the general window:

Gtk.Window → Gtk.ScrolledWindow → Gtk.Box (playing the role of the ListView in my test) → Gtk.Expander → Gtk.GridView

And to ensure that the GridView will expand, GridView.set_vexpand(True). In my test this worked, despite a small positioning bug, probably related to the use of Gtk.Box.

Thanks for the answer.

Are you sure the GridView share the scroll of the general window by doing that and doesn’t have the items hided?

If we take the example I sent and just remove the scrolledwindow for the gridview and set vexpand to true, it has the issue mentionned in the post where the gridview doesn’t scroll (it would show only the first 120 items and the 880 others won’t be accessible) (I tried to replace the ListView by a box as you tried in case it was something specific to listview)

I haven’t tried with that many items, a thousand items seems like a lot for someone to scroll through on one page. Maybe you should consider paginating the display of these items instead of trying to display them in one continuous scroll.

I don’t think having Pagination would be really good in that use case. Like that would mean every group would need it’s own pagination. The 20k items usecase is really rare but it does exist that’s why I want to have my application not crash when it happens. Most people would have between 100 and 2000 items.

The gridview works decently to show 20k items, but that’s really the grouping that I’m stuck. I love that feature, for example on a game library, I would group by completion status, genre, and other data, since it’s very visual (I have 500 items so it’s not long to scroll).

With a filter panel, we can almost do the same and edit the filter to change “group” but it’s not as handy as having the group directly shown.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.