Type ahead dropdown box


#1

Does anyone know of a good dropdown control that works like the
Windows Forms dropdown control with type ahead feature, Where user can type
in a value and it automatically displays the like values in the listbox, or
user can click on the down arrow to bring a list of all items.

I have tried on the combox box in which I am catching the textEditorTextChanged method of Label and showing a PopupMenu with the list populated for the text typed inside the TextEditor. But the problem is that the PopupMenu grabs the keyboard focus from the combo box when it shows up and hence restrict me from typing anything more. Am I doing wrong? Can anything be done with the PopupMenu to get the keyboard focus back to combo box?


#2

That could certainly be done, but it’d require some hacking of the PopupMenu class to achieve it!

If I had time to implement this, I think the best place to put it would be to make it a feature of the ComboBox, letting you supply a callback to dynamically generate the items as you type…


#3

I’m trying to make exactly such control without changing the Juce code. And looks like there are limits to have it exactly how I want without tweaking the Juce code.

At the end I want to have a ComboEx component that behaves similar to URL address component in any modern webbrowser. I plan to provide such Model to the control as input:

class ComboExModel { public: //! called to set filtering, by default not filter is set, //! @returns true if filter was accepted by the model virtual bool setFilter(const String& sFilter) { return false; }; //! returns amount of items with currently set filter virtual int getNumItems() = 0; //! returns text to be shown for the item, index is zero based virtual String getItemText(int idx) = 0; //! returns value for the selected item, index is zero based virtual var getItemValue(int idx) { return getItemText(idx); }; };

And the control will do:

  1. auto-completion during typing (with auto-selecting the rest of untyped yet text),
  2. showing the dropdown list (via pop-up menu) with list of filtered values, limited to reasonable set of values (because model may have ~1000 items, only first ~20 should be present in the list).

To achieve this (at the moment) I have to:

  • or hack insides of ComboBox, Label and TextEditor (to 1) get grip on keyboard events, 2) be able to alter parts of text and 3) select pieces of text during typing). And that is long way that will turn ComboBox to something bigger than it is now.

  • or skip some logic (selection of parts of text), and just hack the ComboBox::getText() by extending it with parameter “const bool returnActiveEditorContents” to be passed to Label inside, and have a timer that will constantly check changes of the ComboBox content and recreate pop-up menu with new items. This way is shorter, but less elegant and will not allow to put and pre-select text after the curet.

So questions are:

  1. Are the described options are reasonable ways?

  2. Is it already planned to have more rich ComboBox with described features in the Juce (so I will not need to do such hacks)? If yes - when it is planned to be available?

Thanks!


#4

Both a valid approaches, I’ve no idea what would be best, it’s one of those things where there might be some kind of unforeseen problem once you start writing it. I would like to add it to the library, but have so many other things to do that it’s not a high priority, I’m afraid!


#5

Hello,

I am looking for this exact functionality, I have a Combox with too many items and I would like the user to be able to type ahead so a popup menu is filled as he types.

I am geting ready to implement something similar and I was wondering if since this comment was done something was already done and is available for this.

Thank you,

 


#6

Would be great if we had a searchable JUCE module repository ;)

If you can't find an example on the forum/web, I'd go ahead and implement yourself, then create a JUCE module for it and share it !
 


#7

I really need this widget. Anybody implement it yet?


#8

Okay - so I need it too. However the PopupMenu is the problem. It’s internals aren’t exposed enough. Here’s a working skeleton using a ListBox instead.

class TypeaheadPopupMenu
	:
	public ListBoxModel,
	public Component
{
public:
	TypeaheadPopupMenu()
	{
		setAlwaysOnTop(true); 
		setOpaque(true); 
		list.setModel(this); 
		list.setColour(ListBox::ColourIds::backgroundColourId, Colours::transparentBlack); 
		addAndMakeVisible(list);
	}

	void setItems(const std::vector<String> & options_)
	{
		options = options_;
		list.updateContent();
		setSize(getWidth(), jmin(int(options.size()), 10) * list.getRowHeight()); 
	}

	void resized() override
	{
		list.setBounds(getLocalBounds()); 
	}

	void paint(Graphics & g) override
	{
		// opaque components must have a paint method..
	}

	int getNumRows() override
	{
		return options.size();
	}


	void setActionOnItemSelected(std::function<void(String)> function)
	{
		onItemSelected = function;
	}

	void listBoxItemClicked(int row, const MouseEvent&) override
	{
		if (onItemSelected)
			onItemSelected(options[row]);  // note action may delete this object
	}

	void returnKeyPressed(int lastRowSelected) override
	{
		if (onItemSelected)
			onItemSelected(options[lastRowSelected]);  // note action may delete this object
	}

	void paintListBoxItem(int rowNumber, Graphics& g, int width, int height, bool rowIsSelected) override
	{
		if (rowNumber >= options.size())
			return;

		auto fg = Colours::lightgrey;
		auto bg = Colours::darkgrey;

		if (rowIsSelected)
			std::swap(fg, bg); 

		g.fillAll(bg); 
		g.setColour(fg); 
		g.drawText(options[rowNumber], 0, 0, width, height, Justification::left, true); 
	}

	void setFirstItemFocused()
	{
		toFront(true); 
		list.selectRow(0); 
	}

private:
	std::function<void(String)> onItemSelected;
	ListBox list;
	std::vector<String> options;
};
/**
A text editor component that shows a pop-up menu/combo box below it. 
*/
class TypeaheadEditor
:
	public Component,
	TextEditor::Listener,
	KeyListener,
	Timer,
	MouseListener
{
public:
	TypeaheadEditor()
	{
		addAndMakeVisible(editor); 

		createMockItems();

		editor.addListener(this); 
		editor.addKeyListener(this); 

	}

	void mouseDown(const MouseEvent& event) override
	{
		if (!isParentOf(event.eventComponent))
			dismissMenu();
	}

	~TypeaheadEditor()
	{
		Desktop::getInstance().removeGlobalMouseListener(this);
	}

	void createMockItems()
	{
		options.add("Bananas");
		options.add("Boats");
		options.add("Fish");
		options.add("Nuclear Waste");
		options.add("Apache");
		options.add("Cause and disinfectant");
		options.add("White and black");
		options.add("Jaws of minor death");
		options.add("Sane Asylum");
		options.add("Seat on wheels");
		options.add("Santa Clap");
		options.add("San Jose");
		options.add("Soggy Bottom Boys");
		options.add("Soggy Bottomed Trousers");
		options.add("Sensible Fish");
		options.add("Sanity Towels");
	}

	void showMenu()
	{
		menu = new TypeaheadPopupMenu();
		menu->addToDesktop(ComponentPeer::StyleFlags::windowIsTemporary);
		menu->setVisible(true);
		menu->setBounds(getScreenBounds().translated(0, getHeight()).withHeight(100));

		menu->setActionOnItemSelected([this](String item)
			{
				editor.setText(item, dontSendNotification);
				menu = nullptr;
			});

		menu->addKeyListener(this); 

		Desktop::getInstance().addGlobalMouseListener(this);

		startTimer(200);
	}

	void textEditorTextChanged(TextEditor&) override
	{
		std::vector<String> stringsToShow;

		auto text = editor.getText();
		auto itemId = 0;
		
		for (auto o : options)
		{
			if (o.containsIgnoreCase(text))
				stringsToShow.push_back(o); 

			itemId++;
		}

		if (stringsToShow.size() == 0)
		{
			dismissMenu();
		}
		else
		{
			if (!menu)
				showMenu();

			menu->setItems(stringsToShow); 
		}
	}

	void dismissMenu()
	{
		menu = nullptr;
		stopTimer(); 
		Desktop::getInstance().removeGlobalMouseListener(this);
	}

	void timerCallback() override
	{
		if (!Process::isForegroundProcess())
			dismissMenu();
		
		if (menu)
		{
			if (!hasKeyboardFocus(true) && !menu->hasKeyboardFocus(true))
				dismissMenu();
		}
		else
		{
			if (!hasKeyboardFocus(true))
				dismissMenu();
		}
	}

	bool keyPressed(const KeyPress& key, Component * component) override
	{
		if (component == &editor)
		{
			if (key == KeyPress::downKey)
			{
				if (menu)
					menu->setFirstItemFocused();

				return true;
			}
		}
		else if (component == menu)
		{
			// if the user tries to type into the menu lets move the focus back there and inject the keypress
			editor.toFront(true); 
			return editor.keyPressed(key); 
		}

		return false;
	}

	void focusLost(FocusChangeType cause) override
	{
		if (menu && !menu->hasKeyboardFocus(true))
			menu = nullptr; 
	}

	void resized() override
	{
		editor.setBounds(getLocalBounds()); 
	}

private:
	ScopedPointer<TypeaheadPopupMenu> menu;
	StringArray options;
	TextEditor editor; 
};

#9

And there’s the output. The searching rules could be changed, and the window alignment if there’s no space at the bottom of the screen isn’t dealt with … but should be enough to be useful.


#10

Thanks. I will give it a try.