Changeset - r27623:94a2107d67c7
[Not reviewed]
master
0 1 0
PeterN - 16 months ago 2023-06-20 19:34:05
peter1138@openttd.org
Fix #11043: Don't choose toolbar dropdown option if focus is lost. (#11044)

Since dropdown menus now get closed if they lose focus, 'instant close' dropdowns (i.e. the toolbar dropdowns) should no longer execute their action to avoid unintended actions.
1 file changed with 4 insertions and 1 deletions:
0 comments (0 inline, 0 general)
src/widgets/dropdown.cpp
Show inline comments
 
@@ -10,385 +10,388 @@
 
#include "../stdafx.h"
 
#include "../window_gui.h"
 
#include "../string_func.h"
 
#include "../strings_func.h"
 
#include "../window_func.h"
 
#include "../zoom_func.h"
 
#include "../timer/timer.h"
 
#include "../timer/timer_window.h"
 
#include "dropdown_type.h"
 

	
 
#include "dropdown_widget.h"
 

	
 
#include "../safeguards.h"
 

	
 

	
 
void DropDownListItem::Draw(const Rect &r, bool sel, Colours bg_colour) const
 
{
 
	int c1 = _colour_gradient[bg_colour][3];
 
	int c2 = _colour_gradient[bg_colour][7];
 

	
 
	int mid = CenterBounds(r.top, r.bottom, 0);
 
	GfxFillRect(r.left, mid - WidgetDimensions::scaled.bevel.bottom, r.right, mid - 1, c1);
 
	GfxFillRect(r.left, mid, r.right, mid + WidgetDimensions::scaled.bevel.top - 1, c2);
 
}
 

	
 
uint DropDownListStringItem::Width() const
 
{
 
	return GetStringBoundingBox(this->String()).width + WidgetDimensions::scaled.dropdowntext.Horizontal();
 
}
 

	
 
void DropDownListStringItem::Draw(const Rect &r, bool sel, Colours bg_colour) const
 
{
 
	Rect ir = r.Shrink(WidgetDimensions::scaled.dropdowntext);
 
	DrawString(ir.left, ir.right, r.top, this->String(), sel ? TC_WHITE : TC_BLACK);
 
}
 

	
 
/**
 
 * Natural sorting comparator function for DropDownList::sort().
 
 * @param first Left side of comparison.
 
 * @param second Right side of comparison.
 
 * @return true if \a first precedes \a second.
 
 * @warning All items in the list need to be derivates of DropDownListStringItem.
 
 */
 
/* static */ bool DropDownListStringItem::NatSortFunc(std::unique_ptr<const DropDownListItem> const &first, std::unique_ptr<const DropDownListItem> const &second)
 
{
 
	std::string str1 = GetString(static_cast<const DropDownListStringItem*>(first.get())->String());
 
	std::string str2 = GetString(static_cast<const DropDownListStringItem*>(second.get())->String());
 
	return StrNaturalCompare(str1, str2) < 0;
 
}
 

	
 
StringID DropDownListParamStringItem::String() const
 
{
 
	for (uint i = 0; i < lengthof(this->decode_params); i++) SetDParam(i, this->decode_params[i]);
 
	return this->string;
 
}
 

	
 
StringID DropDownListCharStringItem::String() const
 
{
 
	SetDParamStr(0, this->raw_string);
 
	return this->string;
 
}
 

	
 
DropDownListIconItem::DropDownListIconItem(SpriteID sprite, PaletteID pal, StringID string, int result, bool masked) : DropDownListParamStringItem(string, result, masked), sprite(sprite), pal(pal)
 
{
 
	this->dim = GetSpriteSize(sprite);
 
	this->sprite_y = dim.height;
 
}
 

	
 
uint DropDownListIconItem::Height(uint width) const
 
{
 
	return std::max(this->dim.height, (uint)FONT_HEIGHT_NORMAL);
 
}
 

	
 
uint DropDownListIconItem::Width() const
 
{
 
	return DropDownListParamStringItem::Width() + this->dim.width + WidgetDimensions::scaled.hsep_wide;
 
}
 

	
 
void DropDownListIconItem::Draw(const Rect &r, bool sel, Colours bg_colour) const
 
{
 
	bool rtl = _current_text_dir == TD_RTL;
 
	Rect ir = r.Shrink(WidgetDimensions::scaled.dropdowntext);
 
	Rect tr = ir.Indent(this->dim.width + WidgetDimensions::scaled.hsep_normal, rtl);
 
	DrawSprite(this->sprite, this->pal, ir.WithWidth(this->dim.width, rtl).left, CenterBounds(r.top, r.bottom, this->sprite_y));
 
	DrawString(tr.left, tr.right, CenterBounds(r.top, r.bottom, FONT_HEIGHT_NORMAL), this->String(), sel ? TC_WHITE : TC_BLACK);
 
}
 

	
 
void DropDownListIconItem::SetDimension(Dimension d)
 
{
 
	this->dim = d;
 
}
 

	
 
static const NWidgetPart _nested_dropdown_menu_widgets[] = {
 
	NWidget(NWID_HORIZONTAL),
 
		NWidget(WWT_PANEL, COLOUR_END, WID_DM_ITEMS), SetMinimalSize(1, 1), SetScrollbar(WID_DM_SCROLL), EndContainer(),
 
		NWidget(NWID_SELECTION, INVALID_COLOUR, WID_DM_SHOW_SCROLL),
 
			NWidget(NWID_VSCROLLBAR, COLOUR_END, WID_DM_SCROLL),
 
		EndContainer(),
 
	EndContainer(),
 
};
 

	
 
static WindowDesc _dropdown_desc(
 
	WDP_MANUAL, nullptr, 0, 0,
 
	WC_DROPDOWN_MENU, WC_NONE,
 
	WDF_NO_FOCUS,
 
	_nested_dropdown_menu_widgets, lengthof(_nested_dropdown_menu_widgets)
 
);
 

	
 
/** Drop-down menu window */
 
struct DropdownWindow : Window {
 
	int parent_button;            ///< Parent widget number where the window is dropped from.
 
	const DropDownList list;      ///< List with dropdown menu items.
 
	int selected_index;           ///< Index of the selected item in the list.
 
	byte click_delay;             ///< Timer to delay selection.
 
	bool drag_mode;
 
	bool instant_close;           ///< Close the window when the mouse button is raised.
 
	int scrolling;                ///< If non-zero, auto-scroll the item list (one time).
 
	Point position;               ///< Position of the topleft corner of the window.
 
	Scrollbar *vscroll;
 

	
 
	/**
 
	 * Create a dropdown menu.
 
	 * @param parent        Parent window.
 
	 * @param list          Dropdown item list.
 
	 * @param selected      Index of the selected item in the list.
 
	 * @param button        Widget of the parent window doing the dropdown.
 
	 * @param instant_close Close the window when the mouse button is raised.
 
	 * @param position      Topleft position of the dropdown menu window.
 
	 * @param size          Size of the dropdown menu window.
 
	 * @param wi_colour     Colour of the parent widget.
 
	 * @param scroll        Dropdown menu has a scrollbar.
 
	 */
 
	DropdownWindow(Window *parent, DropDownList &&list, int selected, int button, bool instant_close, const Point &position, const Dimension &size, Colours wi_colour, bool scroll)
 
			: Window(&_dropdown_desc), list(std::move(list))
 
	{
 
		assert(this->list.size() > 0);
 

	
 
		this->position = position;
 

	
 
		this->CreateNestedTree();
 

	
 
		this->vscroll = this->GetScrollbar(WID_DM_SCROLL);
 

	
 
		uint items_width = size.width - (scroll ? NWidgetScrollbar::GetVerticalDimension().width : 0);
 
		NWidgetCore *nwi = this->GetWidget<NWidgetCore>(WID_DM_ITEMS);
 
		nwi->SetMinimalSizeAbsolute(items_width, size.height + WidgetDimensions::scaled.fullbevel.Vertical() * 2);
 
		nwi->colour = wi_colour;
 

	
 
		nwi = this->GetWidget<NWidgetCore>(WID_DM_SCROLL);
 
		nwi->colour = wi_colour;
 

	
 
		this->GetWidget<NWidgetStacked>(WID_DM_SHOW_SCROLL)->SetDisplayedPlane(scroll ? 0 : SZSP_NONE);
 

	
 
		this->FinishInitNested(0);
 
		CLRBITS(this->flags, WF_WHITE_BORDER);
 

	
 
		/* Total length of list */
 
		int list_height = 0;
 
		for (const auto &item : this->list) {
 
			list_height += item->Height(items_width);
 
		}
 

	
 
		/* Capacity is the average number of items visible */
 
		this->vscroll->SetCapacity(size.height * this->list.size() / list_height);
 
		this->vscroll->SetCount(this->list.size());
 

	
 
		this->parent           = parent;
 
		this->parent_button    = button;
 
		this->selected_index   = selected;
 
		this->click_delay      = 0;
 
		this->drag_mode        = true;
 
		this->instant_close    = instant_close;
 
	}
 

	
 
	void Close() override
 
	{
 
		/* Finish closing the dropdown, so it doesn't affect new window placement.
 
		 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
 
		this->Window::Close();
 

	
 
		Point pt = _cursor.pos;
 
		pt.x -= this->parent->left;
 
		pt.y -= this->parent->top;
 
		this->parent->OnDropdownClose(pt, this->parent_button, this->selected_index, this->instant_close);
 

	
 
		/* Set flag on parent widget to indicate that we have just closed. */
 
		NWidgetCore *nwc = this->parent->GetWidget<NWidgetCore>(this->parent_button);
 
		if (nwc != nullptr) SetBit(nwc->disp_flags, NDB_DROPDOWN_CLOSED);
 
	}
 

	
 
	void OnFocusLost(bool closing) override
 
	{
 
		if (!closing) this->Close();
 
		if (!closing) {
 
			this->instant_close = false;
 
			this->Close();
 
		}
 
	}
 

	
 
	Point OnInitialPosition(int16 sm_width, int16 sm_height, int window_number) override
 
	{
 
		return this->position;
 
	}
 

	
 
	/**
 
	 * Find the dropdown item under the cursor.
 
	 * @param[out] value Selected item, if function returns \c true.
 
	 * @return Cursor points to a dropdown item.
 
	 */
 
	bool GetDropDownItem(int &value)
 
	{
 
		if (GetWidgetFromPos(this, _cursor.pos.x - this->left, _cursor.pos.y - this->top) < 0) return false;
 

	
 
		const Rect &r = this->GetWidget<NWidgetBase>(WID_DM_ITEMS)->GetCurrentRect().Shrink(WidgetDimensions::scaled.fullbevel);
 
		int y     = _cursor.pos.y - this->top - r.top - WidgetDimensions::scaled.fullbevel.top;
 
		int width = r.Width();
 
		int pos   = this->vscroll->GetPosition();
 

	
 
		for (const auto &item : this->list) {
 
			/* Skip items that are scrolled up */
 
			if (--pos >= 0) continue;
 

	
 
			int item_height = item->Height(width);
 

	
 
			if (y < item_height) {
 
				if (item->masked || !item->Selectable()) return false;
 
				value = item->result;
 
				return true;
 
			}
 

	
 
			y -= item_height;
 
		}
 

	
 
		return false;
 
	}
 

	
 
	void DrawWidget(const Rect &r, int widget) const override
 
	{
 
		if (widget != WID_DM_ITEMS) return;
 

	
 
		Colours colour = this->GetWidget<NWidgetCore>(widget)->colour;
 

	
 
		Rect ir = r.Shrink(WidgetDimensions::scaled.fullbevel).Shrink(RectPadding::zero, WidgetDimensions::scaled.fullbevel);
 
		int y = ir.top;
 
		int pos = this->vscroll->GetPosition();
 
		for (const auto &item : this->list) {
 
			int item_height = item->Height(ir.Width());
 

	
 
			/* Skip items that are scrolled up */
 
			if (--pos >= 0) continue;
 

	
 
			if (y + item_height - 1 <= ir.bottom) {
 
				bool selected = (this->selected_index == item->result);
 
				if (selected) GfxFillRect(ir.left, y, ir.right, y + item_height - 1, PC_BLACK);
 

	
 
				item->Draw({ir.left, y, ir.right, y + item_height - 1}, selected, colour);
 

	
 
				if (item->masked) {
 
					GfxFillRect(ir.left, y, ir.right, y + item_height - 1, _colour_gradient[colour][5], FILLRECT_CHECKER);
 
				}
 
			}
 
			y += item_height;
 
		}
 
	}
 

	
 
	void OnClick(Point pt, int widget, int click_count) override
 
	{
 
		if (widget != WID_DM_ITEMS) return;
 
		int item;
 
		if (this->GetDropDownItem(item)) {
 
			this->click_delay = 4;
 
			this->selected_index = item;
 
			this->SetDirty();
 
		}
 
	}
 

	
 
	/** Rate limit how fast scrolling happens. */
 
	IntervalTimer<TimerWindow> scroll_interval = {std::chrono::milliseconds(30), [this](auto) {
 
		if (this->scrolling == 0) return;
 

	
 
		if (this->vscroll->UpdatePosition(this->scrolling)) this->SetDirty();
 

	
 
		this->scrolling = 0;
 
	}};
 

	
 
	void OnMouseLoop() override
 
	{
 
		if (this->click_delay != 0 && --this->click_delay == 0) {
 
			/* Close the dropdown, so it doesn't affect new window placement.
 
			 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
 
			this->Close();
 
			this->parent->OnDropdownSelect(this->parent_button, this->selected_index);
 
			return;
 
		}
 

	
 
		if (this->drag_mode) {
 
			int item;
 

	
 
			if (!_left_button_clicked) {
 
				this->drag_mode = false;
 
				if (!this->GetDropDownItem(item)) {
 
					if (this->instant_close) this->Close();
 
					return;
 
				}
 
				this->click_delay = 2;
 
			} else {
 
				if (_cursor.pos.y <= this->top + 2) {
 
					/* Cursor is above the list, set scroll up */
 
					this->scrolling = -1;
 
					return;
 
				} else if (_cursor.pos.y >= this->top + this->height - 2) {
 
					/* Cursor is below list, set scroll down */
 
					this->scrolling = 1;
 
					return;
 
				}
 

	
 
				if (!this->GetDropDownItem(item)) return;
 
			}
 

	
 
			if (this->selected_index != item) {
 
				this->selected_index = item;
 
				this->SetDirty();
 
			}
 
		}
 
	}
 
};
 

	
 
/**
 
 * Show a drop down list.
 
 * @param w        Parent window for the list.
 
 * @param list     Prepopulated DropDownList.
 
 * @param selected The initially selected list item.
 
 * @param button   The widget which is passed to Window::OnDropdownSelect and OnDropdownClose.
 
 *                 Unless you override those functions, this should be then widget index of the dropdown button.
 
 * @param wi_rect  Coord of the parent drop down button, used to position the dropdown menu.
 
 * @param instant_close Set to true if releasing mouse button should close the
 
 *                      list regardless of where the cursor is.
 
 */
 
void ShowDropDownListAt(Window *w, DropDownList &&list, int selected, int button, Rect wi_rect, Colours wi_colour, bool instant_close)
 
{
 
	CloseWindowByClass(WC_DROPDOWN_MENU);
 

	
 
	/* The preferred position is just below the dropdown calling widget */
 
	int top = w->top + wi_rect.bottom + 1;
 

	
 
	/* The preferred width equals the calling widget */
 
	uint width = wi_rect.Width();
 

	
 
	/* Longest item in the list */
 
	uint max_item_width = 0;
 

	
 
	/* Total height of list */
 
	uint height = 0;
 

	
 
	for (const auto &item : list) {
 
		height += item->Height(width);
 
		max_item_width = std::max(max_item_width, item->Width());
 
	}
 

	
 
	max_item_width += WidgetDimensions::scaled.fullbevel.Horizontal();
 

	
 
	/* Scrollbar needed? */
 
	bool scroll = false;
 

	
 
	/* Is it better to place the dropdown above the widget? */
 
	bool above = false;
 

	
 
	/* Available height below (or above, if the dropdown is placed above the widget). */
 
	uint available_height = std::max(GetMainViewBottom() - top - (int)WidgetDimensions::scaled.fullbevel.Vertical() * 2, 0);
 

	
 
	/* If the dropdown doesn't fully fit below the widget... */
 
	if (height > available_height) {
 

	
 
		uint available_height_above = std::max(w->top + wi_rect.top - GetMainViewTop() - (int)WidgetDimensions::scaled.fullbevel.Vertical() * 2, 0);
 

	
 
		/* Put the dropdown above if there is more available space. */
 
		if (available_height_above > available_height) {
 
			above = true;
 
			available_height = available_height_above;
 
		}
 

	
 
		/* If the dropdown doesn't fully fit, we need a dropdown. */
 
		if (height > available_height) {
 
			scroll = true;
 
			uint avg_height = height / (uint)list.size();
 

	
 
			/* Fit the list; create at least one row, even if there is no height available. */
 
			uint rows = std::max<uint>(available_height / avg_height, 1);
 
			height = rows * avg_height;
0 comments (0 inline, 0 general)