Chapter 23. Search fields

Index

Creating a search field
The search callback thread
Aborting searches
Processing search results
A search input field.

Combo-boxes provide efficient means for choosing or picking an item from a large list of options; but at some point the list of items becomes too large to be useful. A search field combines the feature of an input field and a combo-box.

searchinputfield.C demonstrates how to implement a search field. This is done by creating an input field with a callback that executes as characters get typed into the input field. The application implements this callback as a callable object or a lambda that searches for potential matches for the partial contents of the input field, and the callback returns the list of potential matches.

This list of matches returned from the callback gets displayed a popup below (or above) the input field. Clicking on one of the popped-up results, or using Cursor-Down to move the cursor into the popup to select a match result, copies the matching result into the input field:

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

#include "config.h"
#include "close_flag.H"

#include <x/exception.H>
#include <x/destroy_callback.H>
#include <x/ondestroy.H>
#include <x/appid.H>

#include <x/w/main_window.H>
#include <x/w/listlayoutmanager.H>
#include <x/w/gridlayoutmanager.H>
#include <x/w/gridfactory.H>
#include <x/w/label.H>
#include <x/w/text_param_literals.H>
#include <x/w/font_literals.H>
#include <x/w/input_field.H>
#include <x/w/input_field_lock.H>
#include <x/w/container.H>
#include <x/w/button.H>

#include <courier-unicode.h>
#include <string>
#include <iostream>

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

static void search_function(const x::w::input_field_search_info &search_info)
{
	static const std::u32string lorem_ipsum[]=
		{
		 U"Lorem Ipsum",
		 U"dolor sit amet",
		 U"consectetur adipisicing elit",
		 U"sed do eiusmod tempor",
		 U"incididunt ut labore",
		 U"et dolore magna aliqua",
		 U"Ut enim ad minim veniam",
		 U"quis nostrud exercitation",
		 U"ullamco laboris nisi",
		 U"ut aliquip ex ea commodo",
		 U"consequat",
		 U"Duis aute irure dolor",
		 U"in reprehenderit",
		 U"in voluptate velit",
		 U"esse cillum dolore eu",
		 U"fugiat nulla pariatur",
		 U"Excepteur sint occaecat cupidatat non proident",
		 U"sunt in culpa qui officia deserunt mollit anim",
		 U"id est laborum",
		};

	// A separate execution thread invokes the search callback. Each time
	// a character gets added to the end of the input field, this search
	// callback gets invoked. The search callback does not get invoked
	// if something gets edited in the middle of the input field, only
	// when more characters are added, or the last character in the input
	// field gets deleted.
	//
	// Simulate a slow search when more than two characters are searched
	// for, by sleeping for one second before returning.

	if (search_info.search_string.size() > 6)
	{
		sleep(1);
	}
	//
	// Type-ahead input continues in the input field, while we're
	// "searching" (sleeping). After the existing search returns, the
	// execution thread calls it again, this time passing in the
	// additional typed-in text.

	else if (search_info.search_string.size() > 2)
	{
		// Let's do something more sophisticated than sleeping for
		// a second. Let's make use of the abort mcguffin. The main
		// library execution thread releases its reference on the
		// mcguffin when it aborts the current search in progress.
		// This could be because the keyboard focus left the search
		// field, or additional text was added to, or removed from,
		// the search field; the current "slow running search" is
		// obsolete. There are no means to forcibly terminate a
		// different execution thread, so the main library execution
		// thread releases its reference on the object as the means
		// of indicating the aborted search, and any results returned
		// by the search thread get ignored.
		//
		// What we'll do is attach a destructor callback to the
		// abort mcguffin.
		//
		// We happen to have a convenient thread-safe bool flag
		// available to us, in the form of a close_flag_ref.

		auto abort_flag=close_flag_ref::create();

		// The destructor callback captures the abort flag, and sets
		// it when the mcguffin gets destroyed.

		search_info.get_abort_mcguffin()->ondestroy
			([abort_flag]
			 {
				 abort_flag->close();
			 });

		// Wait for a second, or until the close flag gets set.
		// Using this approach makes it possible to detect when the
		// currently-running "slow search" can be bailed out of.
		//
		// Note that even in the case of an aborted search, any
		// resulted that eventually get returned from this search
		// callback may or may not end up in the popup, so the
		// search callback is not required to return without setting
		// any results. If the search callback detects an aborted
		// search after "finding" some partial results, it's fine to
		// save the partial results and return them.

		x::mpcobj<bool>::lock lock{abort_flag->flag};

		lock.wait_for(std::chrono::seconds(1),
			      [&]
			      {
				      return *lock;
			      });

		if (*lock)
			return;
	}

	for (const auto &search:lorem_ipsum)
	{
		// search_info.search_string is what's being searched.
		//
		// Case-insensitive unicode search, here.

		auto iter=std::search(search.begin(), search.end(),
				      search_info.search_string.begin(),
				      search_info.search_string.end(),
				      []
				      (const auto &a,
				       const auto &b)
				      {
					      return unicode_uc(a) ==
						      unicode_uc(b);
				      });

		if (iter==search.end())
			continue;

		// Found a "search result". Each individual "search result"
		// gets recorded in two different ways. Firstly, the
		// std::u32string representing the matching search result
		// goes into search_info.search_string.

		search_info.search_results.push_back(search);

		auto iter_end=iter+search_info.search_string.size();

		// Then each search result goes into search_info.search_items,
		// which is a list_item_param. At this time, the only
		// documented list_item_param is a text_param, which is
		// a unicode string with meta font and color mark-ups.

		x::w::text_param t;

		// Each search result may appear in the search popup with
		// custom font and color. We use this to show the portion of
		// each found "search result" that matches the search string
		// in bold font and underlined.
		//
		// First, any initial part of each "search result" is
		// just the default theme font: plain "sans_serif" font.

		if (iter != search.begin())
		{
			t("sans_serif"_theme_font);
			t(std::u32string{search.begin(), iter});
		}

		// And now the matching portion, bolded and underlined

		t("sans_serif;weight=bold"_theme_font);
		t(x::w::text_decoration::underline);
		t(std::u32string{iter, iter_end});

		// If there's anything after the matching portion of each
		// "search result", go back to the plain font.

		if (iter_end != search.end())
		{
			t("sans_serif"_theme_font);
			t(x::w::text_decoration::none);
			t(std::u32string{iter_end, search.end()});
		}
		search_info.search_items.push_back(t);
	}

	// Alternatively, if no special markup is required:
	//
	// std::vector<std::u32string> results;
	//
	// search_info.results(results);
	//
	// results() is a helper function that sets both
	// search_info.search_results and search_info.search_items.
	//
	// NOTE: it is up to the search callback to limit the size of the
	// search results. All "search results" get shown by the popup.
	// The "search results" should be limited to some maximum number
	// of results. Also, if the width of each individual search results
	// is bigger than the popup's width, it gets cut-off. It is the
	// search callback's responsibility to enforce some reasonable limits.
}

void create_mainwindow(const x::w::main_window &main_window,
		       const close_flag_ref &close_flag)
{
	auto layout=main_window->gridlayout();

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

	x::w::input_field_config search_config{30};

	// Install the search callback.
	search_config.input_field_search.emplace(
		search_function,
		x::w::bidi_format::none
	);

	// Add some padding, to make the window bigger.

	// Align the input field and the button in the middle of the row,
	// vertically.
	auto field=factory->left_padding(10)
		.top_padding(10)
		.bottom_padding(10)
		.valign(x::w::valign::middle)
		.create_input_field("", search_config);

	// Use the on_validate callback to "report" search results.
	// on_validate() normally gets invoked to validate the input field
	// after it gets tabbed out of. With a search callback, on_validate()
	// also gets invoked after picking something from the search popup.
	//
	// Note that what's manually typed in may not match anything that
	// the search found, if the popup does not get used.

	field->on_validate
		([]
		 (ONLY IN_THREAD,
		  x::w::input_lock &lock,
		  const x::w::callback_trigger_t &trigger)
		 {
			 std::cout << "Search found: "
				   << lock.get()
				   << std::endl;
			 return true;
		 });

	auto ok=factory->right_padding(10)
		.top_padding(10)
		.bottom_padding(10)
		.create_button({"Ok"}, {
				x::w::default_button(),
				x::w::shortcut{'\n'},
			});

	ok->on_activate([close_flag]
			(ONLY IN_THREAD,
			 const x::w::callback_trigger_t &trigger,
			 const x::w::busy &ignore)
			{
				close_flag->close();
			});
}

void searchinputfield()
{
	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_mainwindow(main_window,
								    close_flag);
					  });

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

	guard(main_window->connection_mcguffin());

	main_window->set_window_title("QuackQuackRun!");
	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 {
		searchinputfield();
	} catch (const x::exception &e)
	{
		e->caught();
		exit(1);
	}
	return 0;
}

Creating a search field

#include <x/w/input_field.H>
#include <x/w/input_field_config.H>
#include <x/w/listlayoutmanager.H>

x::w::input_field_config config{30};

config.input_field_search.emplace(
    []
    (const x::w::input_field_search_info &search_info)
    {
          // ...
    });

As explained in the section called “Input fields”, a factory's create_input_field() method creates a new x::w::input_field, using its x::w::input_field_config parameter to set the new input field's options. Initializing its input_field_search members adds a combo-box like popup to the search input field. input_field_search is a small object with two fields: a callback for the actual callable object, and a search_format. This is a x::w::bidi_format value that sets whether the search string received by the callback has bi-directional markers, and defaults to x::w::bidi_format::none.

Alternatively, enable_search() also enables the search popup, and the callback gets installed after the input field gets created:

x::w::input_field_config config{30};

config.enable_search();

x::w::input_field f=factory->create_input_field("", config);

f->on_search(
    {
     []
     (const x::w::input_field_search_info &search_info)
     {
                // ...
     }
    });

The search callback thread

[]
(const x::w::input_field_search_info &search_info)
{
    std::u32string search_string=search_info.search_string;

    // ... Find this string

    std::vector<std::u32string> results;

    // Either plain text results:

    search_info.results(results);

    // ... or plain text results and a text_param with custom fonts or
    // colors:

    std::u32string search_result;
    x::w::text_param search_item;

    search_info.search_results.push_back(search_result);
    search_info.search_items.push_back(search_item);
};

The search callback receives an x::w::input_field_search_info. This parameter contains a search_string, the current contents of the input field, as a Unicode string.

Note

The input_field_search's search_format member defaults to x::w::bidi_format::none. This strips off all bi-directional markers from the search_string, irrespective of the x::w::input_field_config's directional_format setting.

This search callback is not a typical connection thread callback, and does not receive an IN_THREAD parameter. The search callback gets invoked from a separate, independent execution thread that does not block the connection thread. The search callback takes the search string, and places the search results into its x::w::input_field_search_info parameter, and returns. These search results then show up in the search field's popup.

There are two ways to return the list of matches for the search string:

  • Pass a std::vector<std::u32string> to search_info's results() method. This records the list of strings that comprise the found matches, and shows them without any special highlighting or formatting, in the popup.

  • Initialize search_info's search_results and search_items individually. These are two std::vectors. search_results is a x::std::vector<std::u32string> and represents the found matches, as plain text. search_items is a x::std::vector<x::w::list_item_param>.

    Both of these vectors must have the same size. The corresponding value in each vector represents: 1) a single matching search result, as plain text, and 2) a formatted representation of the search result, as a x::w::text_param. The second vector's x::w::list_item_param reveal that they get passed to the search popup's list layout manager; but at this time, the x::w::list_item_params in the search_items can only be x::w::text_params.

    The results() method is simply a shortcut for initializing both vectors from a single vector of Unicode text strings.

The search popup displays the search_items, as is. Selecting one of the search items from the popup copies the corresponding value from search_results into the input field.

searchinputfield.C gives a basic example of a rudimentary search function, that searches a list of canned strings, for a substring that matches the search_string. Each matching string gets copied into the search_results, with search_items formatted so that the matching substring gets shown in bold, and underlined.

Note

Despite it being a standalone execution thread, the search callback is owned by the input field, and the usual rules for capturing references in callbaks apply.