Chapter 8. Filtered input fields

Index

The on_filter() callback
Filtering cursor movements
Using on_default_filter()
on_default_filter() requirements

A input field filter implements custom editing behavior for an input field.

Input validation checks the contents of an input field after it gets edited. The validation callback gets invoked at the conclusion of the input field's editing process, just before the keyboard focus leaves the input field.

An optional input filter callback provides additional means for customizing the input field's behavior. Typical examples of using an input filter include:

filterinput.C gives an example of implementing an input field that accepts numerical input as three groups of three digits, separated by dashes. Letters and other punctuations get silently ignored. The nine digits may be typed in with or without the intermediate dashes. The dashes are quietly ignored, and the cursor automatically skips over the immutable dashes that are always shown.

/*
** Copyright 2019 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/w/main_window.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/input_field.H>
#include <x/w/input_field_lock.H>
#include <x/w/canvas.H>
#include <x/w/container.H>
#include <x/w/button.H>

#include <x/weakcapture.H>

#include <string>
#include <iostream>
#include <sstream>

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

	layout->row_alignment(0, x::w::valign::middle);

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

	factory->create_label("Your ID:");

	// 11 characters in the input field. As always, the input field's
	// width needs to be one more than its maximum size, in order to leave
	// room for the cursor when it's after the last character in the
	// input field, without scrolling.

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

	config.maximum_size=11;

	// Automatically selecting and deselecting everything in the input
	// field is often desirable, for filtered input fields.
	config.autoselect=true;
	config.autodeselect=true;

	// We create the input field with its contents initially "empty", but
	// in reality it's full of spaces, with dashes where the input
	// is separated.
	auto field=factory->create_input_field(U"   -   -   ", config);

	field->on_filter
		// The filter callback needs to have its own input field, and it
		// must be weakly-captured to avoid a circular reference.
		([me=x::make_weak_capture(field)]
		 (ONLY IN_THREAD,
		  const x::w::input_field_filter_info &s)
		 {
			 // Recover a strong reference to my input field.

			 auto got=me.get();

			 if (!got)
				 return;

			 auto &[me]=*got;

			 auto starting_pos=s.starting_pos;
			 auto n_delete=s.n_delete;

			 // If this is cursor movement only: if the cursor
			 // is moved into a position where the dash is,
			 // just keep moving.

			 if (s.type == x::w::input_filter_type::move_only)
			 {
				 // Use original_pos() to find out where the
				 // cursor used to be. This indicates in which
				 // direction the cursor was moved, so we go
				 // one notch in the same direction.
				 switch (starting_pos) {
				 case 3:
					 s.move(s.original_pos() > 3
						? 2:4);
					 return;
				 case 7:
					 s.move(s.original_pos() > 7
						? 6:8);
					 return;
				 }
				 return;
			 }

			 auto current_contents=
				 x::w::input_lock{me}.get_unicode();

			 // If we, allegedly are starting somewhere other
			 // than the logical end of data, nudge things in the
			 // right direction. This happens if you skip ahead of
			 // the spaces, and begin typing in the middle of the
			 // field.

			 for (auto i=starting_pos; i>0; --i)
				 if (current_contents.at(i-1) == ' ')
				 {
					 starting_pos=i-1;
					 n_delete=0;
				 }

			 // Allow stuff to be deleted only at the end of the
			 // input field. s.starting_pos is the starting
			 // location to be deleted. Everything on or after
			 // starting_pos+n_delete should either be a space
			 // or a dash.

			 if (n_delete > 0)
			 {
				 // If the region to delete ends on a dash,
				 // make sure to include it in the region.

				 size_t end_pos=starting_pos+n_delete;

				 if (end_pos == 3 || end_pos == 7)
				 {
					 ++end_pos;
					 ++n_delete;
				 }

				 // Ditto for the starting position, so
				 // backspacing with the cursor just after
				 // the dash deletes the dash, and the preceding
				 // digit.

				 switch (starting_pos) {
				 case 3:
				 case 7:
					 --starting_pos;
					 ++n_delete;
				 }

				 while (end_pos < current_contents.size())
				 {
					 switch(current_contents[end_pos]) {
					 case ' ':
					 case '-':
						 break;
					 default:
						 // Do nothing. Just return.
						 return;
					 }
					 ++end_pos;
				 }
			 }

			 std::u32string new_contents;

			 // s.new_contents is what's new. Ignore everything
			 // except digits. So if a dash gets typed or pasted,
			 // it gets ignored. Instead, we pick off only the
			 // digits, and add the dashes ourselves, in the right
			 // place.

			 new_contents.reserve(s.new_contents.size());

			 for (auto c:s.new_contents)
			 {
				 if (c < '0' || c > '9')
					 continue;

				 switch (starting_pos+new_contents.size()) {
				 case 3:
				 case 7:
					 new_contents.push_back('-');
					 break;
				 }
				 new_contents.push_back(c);
			 }

			 // This is the ending cursor position.

			 auto end_pos=starting_pos+new_contents.size();

			 // However, if n_deleted was more, more stuff is
			 // to be deleted, so pad out new_contents, to
			 // effectively delete it, by overwriting it.

			 while (new_contents.size() < n_delete)
			 {
				 switch (starting_pos+new_contents.size()) {
				 case 3:
				 case 7:
					 new_contents.push_back('-');
					 break;
				 default:
					 new_contents.push_back(' ');
					 break;
				 }
			 }

			 // At this point, we're effectively replacing the
			 // existing contents of the input field, so n_delete
			 // should always be the same as new_contents.size();

			 n_delete=new_contents.size();

			 // Apply the filtered changes to the input field.
			 s.update(starting_pos, n_delete, new_contents);

			 // If we wind up on top of a dash, advance past it.
			 if (end_pos == 3 || end_pos == 7)
				 ++end_pos;

			 // And place the cursor where it should be.
			 s.move(end_pos);
		 });

	factory=layout->append_row();

	factory->create_canvas();

	factory->create_special_button_with_label("Ok")
		->on_activate([close_flag]
			      (ONLY IN_THREAD,
			       const auto &trigger,
			       const auto &mcguffin)
			      {
				      close_flag->close();
			      });

}

void filteredinputfield()
{
	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("Enter your ID");
	main_window->set_window_class("main",
				      "filteredinput@examples.w.libcxx.com");
	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 {
		filteredinputfield();
	} catch (const x::exception &e)
	{
		e->caught();
		exit(1);
	}
	return 0;
}

The on_filter() callback

The callback that gets installed by on_filter() gets invoked to filter every tentative change to the contents of the input field before it gets carried out, and for any reason:

  • Keyboard typing.

  • Cut or paste operations.

  • Modifying the contents of the input field with set().

The callback receives an x::w::input_field_filter_info object that specifies the tentative change. The callback inspects the tentative change's details. Returning without taking any further action results in the tentative change getting quietly ignored, without inserting or removing any text from the input field. Invoking x::w::input_field_filter_info's update() applies the tentative change to the input fields, either as is, or with some adjustments.

A tentative change to the contents of the input field consists of three values:

starting_pos

The index of the tentative change to the input field. Adding or deleting text at the beginning of the input field gets specified as a starting position #0. Removal of the third character in the input field (with a Backspace or Del, for example) sets starting_pos to 2.

n_delete

Number of characters to be deleted. This gets set to 0 if no characters get deleted, and only new text gets inserted.

new_contents

The new text that gets inserted.

It's possible that a single change specifies both a non-0 n_delete and a non-empty new_contents. An example would be highlighting a range of existing text, then executing a paste operation to replace it with different text. This is a single change, deleting the existing text and inserting the replacement text.

new_contents is a std::u32string_view that specifies the new text in Unicode. starting_pos and n_delete also count Unicode characters.

Filtering cursor movements

The type field gives further context as to the nature of the tentative change. A type of x::w::input_filter_type::move_only indicates that the tentative change consists only of moving the cursor in the input field. filteredinput.C uses this to skip over the dashes that separate the groups of digits in the input field.

Note

Retrieving the contents of this input field returns the entire text in the input field, including the dashes. Similarly, using set() to update the contents of the input field requires that the new contents include everything, including the dashes.