Chapter 21. Hierarchical lists

Index

Creating a hierarchical list
List item hierarchical indentation
Image icons in selection lists

It's possible to adjust several configuration settings when creating a selection list so that the end result looks like a hierarchical list of items; with the hierarchy conveyed as increased levels of indentation.

This effect is mostly visual eye candy. This is still a selection list, with consecutively-numbered list items. The look of a hierarchical list comes from setting the indentation level for each list item, accordingly. The list layout manager takes each individual list item's identation level and draws it accordingly; without any further processing. The application is responsible for correctly setting each list item's indentation level.

hierlist.C gives a basic example. Clicking or selecting any item in the list adds four sub-items below it, and clicking or selecting on the item again removes it. Clicking on a sub-item creates sub-items for the sub-item, and so on.

/*
** Copyright 2018-2021 Double Precision, Inc.
** See COPYING for distribution information.
*/

#include "config.h"
#include <x/mpobj.H>
#include <x/exception.H>
#include <x/destroy_callback.H>
#include <x/ref.H>
#include <x/obj.H>
#include <x/appid.H>
#include <x/w/main_window.H>
#include <x/w/gridlayoutmanager.H>
#include <x/w/listlayoutmanager.H>
#include <x/w/focusable_container.H>
#include <x/w/gridfactory.H>
#include <x/w/image_param_literals.H>
#include <string>
#include <iostream>

#include "close_flag.H"

std::string x::appid() noexcept
{
	return "hierlist.examples.w.libcxx.com";
}

static const char * const lorem_ipsum[]={
	"Lorem ipsum",
	"dolor sit amet",
	"consectetur",
	"adipisicing",
	"elit",
	"sed do eiusmod",
	"tempor incididunt",
	"ut labore",
	"et dolore magna"
};

static size_t lorem_ipsum_idx=0;

// next_lorem_ipsum() always gets called from the library's internal
// execution thread (except for the initial list contents, but this doesn't
// count), so we don't need to worry about thread safety for the index
// counter. But even if there was a thread concurrency issue here this
// isn't critical for this example.

static const char *next_lorem_ipsum()
{
	size_t i=(lorem_ipsum_idx+1) %
		(sizeof(lorem_ipsum)/sizeof(lorem_ipsum[0]));

	lorem_ipsum_idx=i;

	return lorem_ipsum[i];
}


static inline void create_main_window(const x::w::main_window &main_window)
{
	auto layout=main_window->gridlayout();

	x::w::gridfactory factory=layout->append_row();

	// Configuring the list layout manager to show a hierarchical "tree".
	//
	// Started with the highlighted list style.
	x::w::new_listlayoutmanager nlm{x::w::highlighted_list};

	// The list layout manager adjusts the width of the list automatically,
	// with a fixed height. A hierarchical tree of items typically grows
	// and shrinks rather actively. Instruct the list layout manager
	// to create a fixed-sized list, 75 millimeters wide and 100 millimeters
	// tall. Scroll-bars get automatically added, as usual, when the list
	// exceeds its britches.

	nlm.width(x::w::dim_axis_arg{75});
	nlm.height(x::w::dim_axis_arg{100});

	// We'll use two columns. For a hierarchical list, the last column,
	// in this cases column #1, should have 100% of the available extra
	// space.

	nlm.columns=2;
	nlm.requested_col_widths={{1, 100}};

	// Column 0 will be a small icon. Have it aligned vertically in the
	// middle of the row.
	nlm.row_alignments={{0, x::w::valign::middle}};

	// The default horizontal padding on the left and the right of each
	// column is too big. We want column 0 to appear next to the main
	// list item, in column 1, so make the padding much smaller, .5
	// millimeters (typically 1 pixel on the left and the right, for a
	// 2 pixel padding).
	//
	// "appearance" is an object that has various settings regarding
	// the list's appearance. It is a constant, cached object, for
	// optimization purposes, an x::w::const_list_appearance.
	// To make changes to it, we use modify() to make a copy of it,
	// and adjust the temporarily-modifiable copy of the object,
	// in the closure. modify() returns a new const_list_appearance
	// that replaces the original one.

	nlm.appearance=nlm.appearance->modify
		([]
		 (const x::w::list_appearance &custom_appearance)
		 {
			 // Change our setting:
			 custom_appearance->h_padding=.5;
		 });

	// The default selection type callback visually selects the list item
	// when it's clicked on. Replace this with a custom callback. We
	// won't draw individual items as selected or unselected. Rather
	// selecting each list item either "expands" this hierachical item
	// by adding sub-items, at a higher indentation level; or removes
	// the previously-added sub-items.

	nlm.selection_type=
		[]
		(ONLY IN_THREAD,
		 const x::w::listlayoutmanager &ll,
		 size_t i,
		 const x::w::callback_trigger_t &trigger,
		 const x::w::busy &mcguffin)
		{
			// Get the indentation level of the selected item.

			size_t i_indent=ll->hierindent(i);

			// Heuristically figure out whether the list item
			// has any sub-items. Look for the following items
			// in the list. See if any list items follow this one
			// which have a higher indentation level.

			size_t s=ll->size();

			size_t e;

			for (e=i; ++e < s; )
			{
				if (ll->hierindent(e) <= i_indent)
					break;
			}

			// If there are any sub-items with a higher
			// indentation level, "collapse" this item by
			// removing those items.
			if (e-i > 1)
			{
				ll->remove_items(i+1, e-i-1);
				return;
			}

			// Add four more items after this one with an
			// increased indentation level.

			x::w::hierindent new_indent{++i_indent};

			// This is a two-column list. The first column is
			// a small image icon, the 2nd column is a random
			// text string. We also specify an indentation level
			// for each new list item.

			ll->insert_items(i+1, {
					new_indent,
					"bullet2.sxg"_image,
					next_lorem_ipsum(),
					new_indent,
					"bullet2.sxg"_image,
					next_lorem_ipsum(),
					new_indent,
					"bullet2.sxg"_image,
					next_lorem_ipsum(),
					new_indent,
					"bullet2.sxg"_image,
					next_lorem_ipsum()
				});
		};

	factory->create_focusable_container
		([]
		 (const auto &fc)
		 {
			 // Initial contents of the list, the initial
			 // item, the top of the fake tree.

			 auto ll=fc->listlayout();

			 ll->append_items({"bullet2.sxg"_image,
					   lorem_ipsum[0]});
		 },
		 nlm);
}

void hierlist()
{
	x::destroy_callback::base::guard guard;

	auto close_flag=close_flag_ref::create();

	auto main_window=x::w::main_window
		::create([&]
			 (const auto &main_window)
			 {
				 create_main_window(main_window);
			 });

	main_window->set_window_title("Hierarchical list");

	guard(main_window->connection_mcguffin());

	main_window->on_disconnect([]
				   {
					   _exit(1);
				   });

	main_window->on_delete
		([close_flag]
		 (ONLY IN_THREAD,
		  const x::w::busy &ignore)
		 {
			 close_flag->close();
		 });

	main_window->show_all();

	close_flag->wait();
}

int main(int argc, char **argv)
{
	try {
		hierlist();
	} catch (const x::exception &e)
	{
		e->caught();
		exit(1);
	}
	return 0;
}

Creating a hierarchical list

The default settings in a x::w::new_listlayoutmanager create a typical selection list. hierlist.C adjusts several settings to achieve the look of a hierarchial list.

x::w::new_listlayoutmanager nlm{x::w::highlighted_list};

nlm.width(x::w::dim_axis_arg{75});
nlm.height(x::w::dim_axis_arg{100});

The list layout manager's default setting compute the list's width so that it's just wide enough for the largest item in the list, and it's a set number of rows high. Hierarchical lists grow and shrink all the time, varying their width and height.

hierlist.C sets the list's size to 75 millimeters wide and 100 millimeters tall. As usual, scroll-bars appear when the list's contents are too big to fit inside its set size.

nlm.columns=2;
nlm.requested_col_widths={{1, 100}};

This hierarchical list is a multiple column list, a list with two columns. The first column, column #0 is a small bullet icon. The second column, column #1 contains the stock text string.

The sample hierarchical list's width gets fixed as 75 millimeters, wider than its initial contents of one small item. When the list has more width than it needs, the list layout manager gives the extra space to all columns, proportionately. This normally results in extra space between all columns in the list.

requested_col_widths specifies which columns in the list get extra space. hierlist.C sets requested_col_widths to specify that column #1, the 2nd column, gets 100% of the available space. Column #1 won't get all of it, since some real estate goes to column #0, but this is sufficient to have column #1 take up all extra space, and remove the gap between the two columns.

In general, hierarchical lists should always specify 100% of the list's width for the last column in the list.

nlm.h_padding=.5;

The list layout manager defaults to give generous padding for all columns in a multi-column lists. To have the bullet appear next to its label, the requested padding for each column's left and right margin gets reduced to .5 millimeters.

nlm.selection_type=
    []
    (ONLY IN_THREAD,
    const x::w::listlayoutmanager &ll,
    size_t i,
    const x::w::callback_trigger_t &trigger,
    const x::w::busy &mcguffin)
    {
        // ...
    };

The selection_type callback controls what happens when a list item gets selected by clicking on it, or by navigating to the list item using the keyboard and pressing Enter. The normal behavior of a selection list visually highlights the list item and removes the highlight from the previously selected item (or moving a small bullet to the selected item).

hierlist.C replaces this callback with a custom callback that implements the function of expanding a hierarchical list by adding new items after the selected item with a higher indentation level; or by collapsing the list by removing these items.

List item hierarchical indentation

x::w::listlayoutmanager ll;

ll->insert_items(i+1, {
    x::w::hierindent{2},
    "Lorem Ipsum"
});

This is explained in the section called “Modifying the contents of a list”. New items in the list get specified by a vector containing both the new items as well as values that provide additional metadata for the new items. Any metadata values precede the list items they apply to.

A x::w::hierindent parameter specifies the number of indentation levels for the next new item. The default indentation level is 0, no indentation. This chapter's introduction explains how the list layout manager uses the indentation level to show each list item accordingly, and otherwise leaves it up to the application to deal with what the indentation level means.

hierlist.C's custom selection_type callback gets invoked when an existing item in the list gets selected with they keyboard or clicked on. The callback uses the list layout manager's hierindent() method to retrieve the item's indentation level, as well as the indentation level of the items that follow it.

The indention level visually indicates if the list item has any sub-items that follow it. If not, the callback adds four items after the selected item, with a higher indentation level. This has the appearance of expanding an item in the hierarchical list and showing its sub-items. If the selected item has sub-items, the sub-items get removed. This has the appearance of collapsing a hierarchical list.

But all of this is just an illusion. This is just an ordinary selection list, with a flat list of consecutively-numbered items. The appearance of a hierarchical list comes from intelligently adding or removing items from the middle of the list, with specific indentation levels.