Chapter 36. Generating UI from an XML theme file

uigenerator1.C is a version of combobox.C that gives an example of creating display elements using an XML-specified layout, a theme file. The XML content of the theme file gets effectively executed as a list of instructions for invoking the various methods of layout managers and factories. Instead of invoking various methods grid rows, factories, and display elements and their various grid-related attributes (borders, padding, etc...), these methods gets executed via the theme file. The default appearance of LibCXXW windows comes from the default theme file, and this is the same mechanism. The application provides only the code to create the individual display elements themselves: in this case a label, a combo-box, and a set of buttons.

This approach does not really end up saving much code, but it makes it possible to adjust the appearance of display elements quickly, just by editing and changing the theme file, and without recompiling.

/*
** Copyright 2017-2019 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/pidinfo.H>
#include <x/w/main_window.H>
#include <x/w/gridlayoutmanager.H>
#include <x/w/standard_comboboxlayoutmanager.H>
#include <x/w/editable_comboboxlayoutmanager.H>
#include <x/w/input_field_lock.H>
#include <x/w/focusable_container.H>
#include <x/w/gridfactory.H>
#include <x/w/label.H>
#include <x/w/button.H>
#include <x/w/uielements.H>
#include <x/w/uigenerators.H>
#include <string>
#include <iostream>
#include <sstream>
#include <algorithm>

#include "close_flag.H"

static inline auto
create_standard_combobox(const x::w::factory &factory,
			 void (*creator)(const x::w::focusable_container &))
{

	x::w::new_standard_comboboxlayoutmanager nclm{

		// Default callback that gets invoked whenever any combobox
		// list item gets selected or unselected.

		[]
		(ONLY IN_THREAD,
		 const x::w::standard_combobox_selection_changed_info_t &i)
		{
			if (!i.list_item_status_info.selected)
				return;

			std::cout << "Selected item #"
				  << i.list_item_status_info.item_number
				  << std::endl;
		}};

	return factory->create_focusable_container(creator, nclm);
}


std::vector<x::w::list_item_param> days_of_week()
{
	return {"Sunday", "Monday", "Tuesday",	"Wednesday",
			"Thursday", "Friday", "Saturday"};
}

static inline void create_main_window(const x::w::main_window &main_window)
{
	std::string me=x::exename(); // My path.
	size_t p=me.rfind('/');

	// Load "uigenerator1.xml" from the same directory as me

	x::w::const_uigenerators generator=
		x::w::const_uigenerators::create(me.substr(0, ++p) +
						 "uigenerator1.xml");

	x::w::containerptr combobox;
	x::w::buttonptr append_row_button;
	x::w::buttonptr insert_row_button;
	x::w::buttonptr replace_row_button;
	x::w::buttonptr delete_row_button;
	x::w::buttonptr reset_button;
	x::w::buttonptr shuffle_button;

	x::w::uielements element_factory
		{
		 {
		  {"combobox-label",
		   []
		   (const x::w::factory &factory)
		   {
			   factory->create_label("Days of the week (or else):");
		   }
		  },

		  {"combobox-element",
		   [&]
		   (const x::w::factory &factory)
		   {
			   combobox=create_standard_combobox
				   (factory,
				    []
				    (const auto &container)
				    {
					    x::w::standard_comboboxlayoutmanager
						    lm=container
						    ->get_layoutmanager();

					    lm->replace_all_items
						    (days_of_week());
				    });
		   },
		  },
		  {"append-row-button",
		   [&]
		   (const x::w::factory &factory)
		   {
			   append_row_button=
				   factory->create_button("Append row");
		   },
		  },
		  {"insert-row-button",
		   [&]
		   (const x::w::factory &factory)
		   {
			   insert_row_button=
				   factory->create_button("Insert row");
		   },
		  },
		  {"replace-row-button",
		   [&]
		   (const x::w::factory &factory)
		   {
			   replace_row_button=
				   factory->create_button("Replace row");
		   },
		  },
		  {"delete-row-button",
		   [&]
		   (const x::w::factory &factory)
		   {
			   delete_row_button=
				   factory->create_button("Delete row");
		   },
		  },
		  {"reset-button",
		   [&]
		   (const x::w::factory &factory)
		   {
			   reset_button=
				   factory->create_button("Reset");
		   },
		  },
		  {"shuffle-button",
		   [&]
		   (const x::w::factory &factory)
		   {
			   shuffle_button=
				   factory->create_button("Shuffle");
		   },
		  },
		 }
		};

	x::w::gridlayoutmanager layout=main_window->get_layoutmanager();

	layout->generate("main-window-grid",
			 generator, element_factory);

	append_row_button->on_activate
		([=, counter=0]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &ignore)
		 mutable
		 {
			 std::ostringstream o;

			 o << "Append " << ++counter;

			 x::w::standard_comboboxlayoutmanager lm=
				 combobox->get_layoutmanager();

			 lm->append_items(IN_THREAD, {o.str()});
		 });

	insert_row_button->on_activate
		([=, counter=0]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &ignore)
		 mutable
		 {
			 std::ostringstream o;

			 o << "Insert " << ++counter << std::endl;

			 x::w::standard_comboboxlayoutmanager lm=
				 combobox->get_layoutmanager();

			 lm->insert_items(IN_THREAD, 0, {o.str()});
		 });

	replace_row_button->on_activate
		([=, counter=0]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &ignore)
		 mutable
		 {
			 x::w::standard_comboboxlayoutmanager lm=
				 combobox->get_layoutmanager();

			 x::w::standard_combobox_lock lock{lm};

			 if (lm->size() == 0)
				 return;

			 std::ostringstream o;

			 o << "Replace " << ++counter << std::endl;

			 lm->replace_items(IN_THREAD, 0, {o.str()});
		 });

	delete_row_button->on_activate
		([combobox]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &ignore)
		 {
			 x::w::standard_comboboxlayoutmanager lm=
				 combobox->get_layoutmanager();

			 if (lm->size() == 0)
				 return;

			 lm->remove_item(IN_THREAD, 0);
		 });

	reset_button->on_activate
		([combobox]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &ignore)
		 {
			 x::w::standard_comboboxlayoutmanager lm=
				 combobox->get_layoutmanager();

			 lm->replace_all_items(IN_THREAD,
					       days_of_week());
		 });

	shuffle_button->on_activate
		([combobox]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &ignore)
		 {
			 x::w::standard_comboboxlayoutmanager lm=
				 combobox->get_layoutmanager();

			 std::vector<size_t> n;

			 n.resize(lm->size());

			 std::generate(n.begin(), n.end(),
				       [n=0]
				       ()
				       mutable
				       {
					       return n++;
				       });

			 std::random_shuffle(n.begin(), n.end());

			 lm->resort_items(IN_THREAD, n);
		 });

	main_window->appdata=combobox;
}

void uigenerator1()
{
	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_class("main",
				      "uigenerator1@examples.w.libcxx.com");

	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();

	x::w::focusable_container combobox=main_window->appdata;

	x::w::standard_comboboxlayoutmanager lm=
		combobox->get_layoutmanager();

	std::optional<size_t> selection=lm->selected();

	if (selection)
	{
		std::cout << "Final selection was day #" << selection.value()
			  << std::endl;
	}
	else
	{
		std::cout << "No combo-box selection."
			  << std::endl;
	}
}

int main(int argc, char **argv)
{
	try {
		uigenerator1();
	} catch (const x::exception &e)
	{
		e->caught();
		exit(1);
	}
	return 0;
}
<?xml version="1.0" encoding="utf-8"?>
<theme version="1"
       xmlns:xi="http://www.w3.org/2003/XInclude">

  <layout type="grid" id="main-window-grid">
    <valign>
      <row>0</row>
      <valign>middle</valign>
    </valign>

    <append_row>
      <name>main-window-combobox-row</name>
    </append_row>

    <append_row>
      <name>main-window-append-row-button</name>
    </append_row>

    <append_row>
      <name>main-window-insert-row-button</name>
    </append_row>

    <append_row>
      <name>main-window-replace-row-button</name>
    </append_row>

    <append_row>
      <name>main-window-delete-row-button</name>
    </append_row>

    <append_row>
      <name>main-window-reset-button</name>
    </append_row>

    <append_row>
      <name>main-window-shuffle-button</name>
    </append_row>
  </layout>

  <factory type="grid" id="main-window-combobox-row">
    <element>
      <name>combobox-label</name>
    </element>
    <element>
      <name>combobox-element</name>
    </element>
  </factory>

  <factory type="grid" id="main-window-append-row-button">
    <colspan>
      <columns>2</columns>
    </colspan>
    <element>
      <name>append-row-button</name>
    </element>
  </factory>

  <factory type="grid" id="main-window-insert-row-button">
    <colspan>
      <columns>2</columns>
    </colspan>
    <element>
      <name>insert-row-button</name>
    </element>
  </factory>

  <factory type="grid" id="main-window-replace-row-button">
    <colspan>
      <columns>2</columns>
    </colspan>
    <element>
      <name>replace-row-button</name>
    </element>
  </factory>

  <factory type="grid" id="main-window-delete-row-button">
    <colspan>
      <columns>2</columns>
    </colspan>
    <element>
      <name>delete-row-button</name>
    </element>
  </factory>

  <factory type="grid" id="main-window-reset-button">
    <colspan>
      <columns>2</columns>
    </colspan>
    <element>
      <name>reset-button</name>
    </element>
  </factory>

  <factory type="grid" id="main-window-shuffle-button">
    <colspan>
      <columns>2</columns>
    </colspan>
    <element>
      <name>shuffle-button</name>
    </element>
  </factory>
</theme>

Creating a x::w::uigenerators object loads specifications of layout managers and factories from the theme file. A single theme file may contain specifications for more than one layout and factory. Each layout has a unique identifier. The first parameter to a grid layout manager's generate() names a layout from the theme file that gets executed:

layout->generate("main-window-grid",
                 generator, element_factory);

This generates the layout specified by:

<layout type="grid" id="main-window-grid">

<!-- ... -->

</layout>

Appendix B, LibCXXW theme files provides a complete specification of the theme files. uigenerator2.C is another example that uses a theme file to create a main window showing book tabs, filling out the entire contents of the window, the book, and the tabs, from the theme file.