From 8495b4c4f625cd98bfcab7ee05919bf3ac624e06 Mon Sep 17 00:00:00 2001 From: Andrii Dokhniak Date: Mon, 24 Jun 2024 09:52:00 +0200 Subject: [PATCH] Codechange: Add the universal selector widget. --- src/selector_widget.cpp | 433 ++++++++++++++++++++++++++++++++++++++++ src/selector_widget.h | 84 ++++++++ 2 files changed, 517 insertions(+) create mode 100644 src/selector_widget.cpp create mode 100644 src/selector_widget.h diff --git a/src/selector_widget.cpp b/src/selector_widget.cpp new file mode 100644 index 0000000000..e6f508e205 --- /dev/null +++ b/src/selector_widget.cpp @@ -0,0 +1,433 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file selector_widget.cpp GUI that shows performance graphs. */ + +#include "stdafx.h" +#include "company_base.h" +#include "company_gui.h" +#include "strings_func.h" +#include "table/sprites.h" +#include "widgets/graph_widget.h" +#include "selector_widget.h" +#include "safeguards.h" +#include + + +/** + * Create a new NWidgetBase with all the widgets needed by the SelectorWidget widget + * This function is made to be passed in NWidgetFunction, when creating UI for a parrent window + * @see NWidgetFunction + */ +std::unique_ptr SelectorWidget::MakeSelectorWidgetUI() { + static constexpr NWidgetPart widget_ui[] = { + NWidget(WWT_PANEL, COLOUR_BROWN), + NWidget(WWT_EDITBOX, COLOUR_BROWN, WID_SELECTOR_EDITBOX), SetFill(1, 0), SetResize(1, 0), SetPadding(2), + SetDataTip(STR_LIST_FILTER_TOOLTIP, STR_LIST_FILTER_TOOLTIP), + EndContainer(), + NWidget(NWID_VERTICAL), + NWidget(NWID_HORIZONTAL), + NWidget(WWT_MATRIX, COLOUR_BROWN, WID_SELECTOR_MATRIX), + SetScrollbar(WID_SELECTOR_SCROLLBAR), SetResize(1, 1), SetMatrixDataTip(1, 0, STR_NULL), SetFill(1, 1), + NWidget(NWID_VSCROLLBAR, COLOUR_BROWN, WID_SELECTOR_SCROLLBAR), + EndContainer(), + NWidget(NWID_VERTICAL, NC_EQUALSIZE), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_SELECTOR_HIDEALL), + SetDataTip(STR_SELECTOR_WIDGET_DISABLE_ALL, STR_SELECTOR_WIDGET_TOOLTIP_DISABLE_ALL), + SetResize(1, 0), SetMinimalSize(20, 12), SetFill(1, 0), + NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_SELECTOR_SHOWALL), + SetDataTip(STR_SELECTOR_WIDGET_ENABLE_ALL, STR_SELECTOR_WIDGET_TOOLTIP_ENABLE_ALL), + SetResize(1, 0), SetMinimalSize(20, 12), SetFill(1, 0), + EndContainer(), + NWidget(NWID_HORIZONTAL), + NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_SELECTOR_TOGGLE), + SetDataTip(STR_SELECTOR_WIDGET_TOGGLE_SELECTED, STR_SELECTOR_WIDGET_TOOLTIP_TOGGLE_SELECTED), + SetResize(1, 0), SetMinimalSize(20, 12), SetFill(1, 0), + NWidget(WWT_RESIZEBOX, COLOUR_BROWN, WID_GRAPH_RESIZE), SetDataTip(RWV_SHOW_BEVEL, STR_TOOLTIP_RESIZE), + SetResize(0, 0), + NWidget(NWID_HORIZONTAL), + EndContainer(), + EndContainer(), + }; + return MakeNWidgets({std::begin(widget_ui), std::end(widget_ui)}, nullptr); +} + +/** + * Selector widget initialization function + * This function is ment to be called after CreateNestedTree() of the window + * + * @param w The parrent window of this widget + * + * @see SelectorWidget::post_init() + */ +void SelectorWidget::Init(Window* w) +{ + this->w = w; + this->vscroll = w->GetScrollbar(WID_SELECTOR_SCROLLBAR); + assert(this->vscroll != nullptr); + RebuildList(); + w->querystrings[WID_SELECTOR_EDITBOX] = &this->editbox; + this->vscroll->SetCount(this->filtered_list.size()); + this->vscroll->SetCapacityFromWidget(w, WID_SELECTOR_MATRIX); +} + +/** + * Selector widget profile selection function + * This function is ment to be called before CreateNestedTree() of the window + * + * @param p The "profile" the selector widget should use + * + * A selector widget profile is just a set of function pointers / + * possibly parameters in the future, that determine how the widget works, + * and help make it more universal. + * + * @see SelectorWidget::init() + */ +void SelectorWidget::PreInit(Profile p) +{ + this->profile = p; +} + +/** + * Selector widget's OnClick event handler + * This function is ment to be called from the parrent's window's OnClick + * Event handler + * + * @param pt The click coordinate + * @param widget The id of the targeted widget + * @param click_count The number of clicks + * + * All of theese parameters should be passed straight from the parrent's window's OnClick + * Event handler, without any changes, or conditional checks + * + * @see Window::OnClick() + */ +void SelectorWidget::OnClick(Point pt, WidgetID widget, int click_count) +{ + if (widget == WID_SELECTOR_HIDEALL){ + for (uint i = 0; i < this->shown.size(); i++) { + this->shown[i] = false; + } + } else if (widget == WID_SELECTOR_SHOWALL) { + for (uint i = 0; i < this->shown.size(); i++) { + this->shown[i] = true; + } + } else if (widget == WID_SELECTOR_TOGGLE) { + if (this->selected_id.has_value()) { + this->shown[this->selected_id.value()].flip(); + } + } else if (widget == WID_SELECTOR_MATRIX) { + const uint selected_row = vscroll->GetScrolledRowFromWidget(pt.y, w, widget); + if (selected_row >= this->filtered_list.size()) { + return; + } + selected_id = this->filtered_list[selected_row]; + if (click_count > 1) { + if (this->selected_id.has_value()) { + this->shown[this->selected_id.value()].flip(); + } + } + } + w->InvalidateData(0, true); +} + +/** + * Selector widget's OnInvalidateData event handler + * This function is ment to be called from the parrent's window's OnInvalidateData + * Event handler + * + * @param data Information about the changed data. + * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details. + * + * All of theese parameters should be passed straight from the parrent's window's OnInvalidateData + * Event handler, without any changes, or conditional checks + * + * @see Window::OnInvalidateData() + */ +void SelectorWidget::OnInvalidateData([[maybe_unused]] int data, bool gui_scope) +{ + if (!gui_scope) return; + RebuildList(); + this->vscroll->SetCapacityFromWidget(w, WID_SELECTOR_MATRIX); + if (this->selected_id.has_value()) { + const uint to_select_id = this->selected_id.value(); + int selected_pos = -1; + for (uint i = 0; i < this->filtered_list.size(); i++) { + if (this->filtered_list[i] == to_select_id) { + selected_pos = i; + break; + } + } + if (selected_pos != -1) { + const int cap = this->vscroll->GetCapacity(); + const int pos = this->vscroll->GetPosition(); + if (selected_pos < pos) { + vscroll->SetPosition(selected_pos); + } else if (selected_pos > pos + cap) { + vscroll->SetPosition(selected_pos - cap + 1); + } + } + } + w->SetDirty(); +} + +/** + * Selector widget's UpdateWidgetSize method + * This function is ment to be called from the parrent's window's UpdateWidgetSize method + * + * @param widget Widget number. + * @param[in,out] size Size of the widget. + * @param padding Recommended amount of space between the widget content and the widget edge. + * @param[in,out] fill Fill step of the widget. + * @param[in,out] resize Resize step of the widget. + * + * All of theese parameters should be passed straight from the parrent's window's UpdateWidgetSize + * method, without any changes, or conditional checks. + * + * @see Window::UpdateWidgetSize() + */ +void SelectorWidget::UpdateWidgetSize(WidgetID widget, Dimension &size, const Dimension &padding, Dimension &fill, Dimension &resize) +{ + if (widget != WID_SELECTOR_MATRIX) return; + this->row_height = GetCharacterHeight(FS_NORMAL) + padding.height; + size.height = this->row_height * 7; + size.width = 300; + resize.width = 1; + resize.height = this->row_height; + fill.width = 1; + fill.height = this->row_height; +} + +/** + * Selector widget's OnResize event handler + * This function is ment to be called from the parrent's window's OnResize + * Event handler + * + * All of theese parameters should be passed straight from the parrent's window's OnResize + * Event handler, without any changes, or conditional checks + * + * @see Window::OnResize() + */ +void SelectorWidget::OnResize() +{ + this->vscroll->SetCapacityFromWidget(w, WID_SELECTOR_MATRIX); +} + +/** + * Selector widget's OnEditboxChanged event handler + * This function is ment to be called from the parrent's window's OnEditboxChanged + * Event handler + * + * @param wid The widget id of the editbox + * + * All of theese parameters should be passed straight from the parrent's window's OnEditboxChanged + * Event handler, without any changes, or conditional checks + * + * @see Window::OnEditboxChanged() + */ +void SelectorWidget::OnEditboxChanged(WidgetID wid) +{ + if (wid != WID_SELECTOR_EDITBOX) { + return; + } + this->string_filter.SetFilterTerm(this->editbox.text.buf); + this->w->InvalidateData(0); + this->vscroll->SetCount(this->filtered_list.size()); +} + +/** + * Selector widget's DrawWidget method + * This function is ment to be called from the parrent's window's DrawWidget + * method + * + * @param widget Widget number. + * @param[in,out] size Size of the widget. + * @param padding Recommended amount of space between the widget content and the widget edge. + * @param[in,out] fill Fill step of the widget. + * @param[in,out] resize Resize step of the widget. + * + * All of theese parameters should be passed straight from the parrent's window's DrawWidget + * Event handler, without any changes, or conditional checks + * + * This function calls a profile defined function this->profile.DrawSection(), + * that actually draws the fields + * + * @see Window::DrawWidget() + * @see SelectorWidget::Profile + */ +void SelectorWidget::DrawWidget(const Rect &r, WidgetID widget) +{ + if (widget != WID_SELECTOR_MATRIX) { + return; + } + Rect line = r.WithHeight(row_height); + + const bool rtl = _current_text_dir == TD_RTL; + + auto [first, last] = this->vscroll->GetVisibleRangeIterators(filtered_list); + + for (auto it = first; it != last; ++it) { + const Rect ir = line.Shrink(WidgetDimensions::scaled.framerect); + + if (!this->shown[*it]) { + DrawFrameRect(ir, COLOUR_BROWN, FR_LOWERED); + } + this->profile.DrawSection(this, *it, ir.Indent(WidgetDimensions::scaled.fullbevel.Horizontal(), rtl)); + + line = line.Translate(0, row_height); + } +} + +/** + * This function is one of the possible functions ment to be used as part of the DrawWidget::Profile + * It draws "section" (line) of the scrollable list + * @param wid The current SelectionWidget object pointer + * @param id the "id" of the item to be drawn, can mean different things depending on the profile + * e.g when this widget displays companies, the id is a unique and valid CompanyID + * @param r the rectangle where the item is to be drawn + * @see SelectorWidget::DrawSection() + * @see SelectorWidget::DrawWidget() + * @see SelectorWidget::Profile + */ +void DrawSectionCompany(SelectorWidget *wid, int id, const Rect &r) +{ + const CompanyID cid = (CompanyID)id; + assert(Company::IsValidID(cid)); + + const bool rtl = _current_text_dir == TD_RTL; + const Dimension d = GetSpriteSize(SPR_COMPANY_ICON); + + DrawCompanyIcon(cid, rtl ? r.right - d.width : r.left, CenterBounds(r.top, r.bottom, d.height)); + + const Rect text = r.Indent(d.width + WidgetDimensions::scaled.hsep_normal, rtl); + + SetDParam(0, cid); + if (wid->selected_id.value_or(INVALID_OWNER) == cid) { + DrawString(text.left, text.right, CenterBounds(text.top, text.bottom, GetCharacterHeight(FS_NORMAL)), STR_COMPANY_NAME, TC_WHITE); + } else { + DrawString(text.left, text.right, CenterBounds(text.top, text.bottom, GetCharacterHeight(FS_NORMAL)), STR_COMPANY_NAME, TC_BLACK); + } +} + +/** + * This function is one of the possible functions ment to be used as part of the DrawWidget::Profile + * It draws "section" (line) of the scrollable list + * @param wid The current SelectionWidget object pointer + * @param id the "id" of the item to be drawn, can mean different things depending on the profile + * e.g when this widget displays companies, the id is a unique and valid CompanyID + * @param r the rectangle where the item is to be drawn + * @see SelectorWidget::DrawSection() + * @see SelectorWidget::DrawWidget() + * @see SelectorWidget::Profile + */ +static void DrawSectionCargo(SelectorWidget *wid, int id, const Rect &r) +{ + const CargoID cargo_id = (CargoID)id; ///< Id of the current row's cargo + const CargoSpec *cargo = CargoSpec::Get(id); ///< cargo spec of the current row's cargo + + const bool rtl = _current_text_dir == TD_RTL; + + const float cargo_rect_scale_factor = 0.85f; + const int legend_width = r.Height() * 9 / 6; + + /* Make a rectangle to display the legend color */ + const Rect cargo_colour = r.Shrink(0, r.Height() * (1.0 - cargo_rect_scale_factor) / 2.0) + .WithWidth(legend_width * cargo_rect_scale_factor, rtl); + + /* Cargo-colour box with outline */ + GfxFillRect(cargo_colour, PC_BLACK); + GfxFillRect(cargo_colour.Shrink(WidgetDimensions::scaled.bevel), cargo->legend_colour); + + /* Cargo name */ + SetDParam(0, cargo->name); + const Rect text = r.Shrink(legend_width + WidgetDimensions::scaled.hsep_normal, rtl); + if (wid->selected_id.value_or(INVALID_OWNER) == cargo_id) { + DrawString(text.left, text.right, CenterBounds(text.top, text.bottom, GetCharacterHeight(FS_NORMAL)), STR_JUST_STRING, TC_WHITE); + } else { + DrawString(text.left, text.right, CenterBounds(text.top, text.bottom, GetCharacterHeight(FS_NORMAL)), STR_JUST_STRING, TC_BLACK); + } +}; + + + +/** + * This function is one of the possible functions ment to be used as part of the DrawWidget::Profile + * It repopulates the list that the widget uses to keep track of different selectable items + * @param wid The current SelectionWidget object pointer + * @see SelectorWidget::RebuildList() + * @see SelectorWidget::Profile + */ +static void RebuildListCompany(SelectorWidget *wid) { + for (const Company *c: Company::Iterate()) { + wid->list.push_back(c->index); + if (wid->string_filter.IsEmpty()) { + wid->filtered_list.push_back(c->index); + continue; + } + wid->string_filter.ResetState(); + + SetDParam(0, c->index); + wid->string_filter.AddLine(GetString(STR_COMPANY_NAME)); + + if (wid->string_filter.GetState()) { + wid->filtered_list.push_back(c->index); + } + } +} + +/** + * This function is one of the possible functions ment to be used as part of the DrawWidget::Profile + * It repopulates the list that the widget uses to keep track of different selectable items + * @param wid The current SelectionWidget object pointer + * @see SelectorWidget::RebuildList() + * @see SelectorWidget::Profile + */ +void RebuildListCargo(SelectorWidget *wid) { + for (const CargoSpec *cargo : _sorted_standard_cargo_specs){ + wid->list.push_back(cargo->Index()); + if (wid->string_filter.IsEmpty()) { + wid->filtered_list.push_back(cargo->Index()); + continue; + } + wid->string_filter.ResetState(); + + SetDParam(0, cargo->Index()); + wid->string_filter.AddLine(GetString(STR_JUST_CARGO)); + + if (wid->string_filter.GetState()) { + wid->filtered_list.push_back(cargo->Index()); + } + } +} + +/** + * A function that updates and rebuilds the list of selectable items + * This function calls a profile defined function this->profile.RebuildList(), + * that actually rebuilds the list of items. + * + * It is also manages the shown items and the filtered_list lists + * + * @see SelectorWidget::Profile + */ +void SelectorWidget::RebuildList() { + this->list.clear(); + this->filtered_list.clear(); + this->profile.RebuildList(this); + uint32_t max = 0; + for (const uint32_t id : list) { + if (id > max) { + max = id; + } + } + for (size_t i = this->shown.size(); i < max + 1; i++) { + shown.push_back(true); + } +} + +const SelectorWidget::Profile SelectorWidget::company_selector_profile = {DrawSectionCompany, RebuildListCompany}; +const SelectorWidget::Profile SelectorWidget::cargo_selector_profile = {DrawSectionCargo, RebuildListCargo}; diff --git a/src/selector_widget.h b/src/selector_widget.h new file mode 100644 index 0000000000..9faa4facb1 --- /dev/null +++ b/src/selector_widget.h @@ -0,0 +1,84 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file selector_widget.h GUI that shows performance graphs. */ + +#ifndef SELECTOR_WIDGET_H +#define SELECTOR_WIDGET_H + +#include "stdafx.h" +#include "querystring_gui.h" +#include "stringfilter_type.h" +#include "strings_type.h" +#include "widget_type.h" +#include "window_gui.h" +#include "window_type.h" +#include "safeguards.h" + +class SelectorWidget { +public: + /** + * This struct is like a config file for a utility, + * it makes the selector widget configurable and extencible. + * In it's current form it stores function pointers to functions + * that rebuild the lists and draw individual items in the matrix widget + */ + struct Profile { + void (*DrawSection)(SelectorWidget *wid, int id, const Rect &r); ///< A function pointer to a function that draws 1 line of the list + void (*RebuildList)(SelectorWidget *wid); ///< A function pointer that rebuilds the item list + }; + + static const Profile company_selector_profile; ///< One prebuilt profile for selection + static const Profile cargo_selector_profile; ///< One prebuilt profile for selection + + std::optional selected_id; ///< Id of the currently selected item + std::vector shown; ///< A vector that determines which items are shown e.g on the graph. Not to be confused with filtered_list + std::vector list; ///< A list of all the items + StringFilter string_filter; ///< The filter that checks weather an item should be displayed based on the editbox + + /** A list of items displayed in WID_SELECTION_MATRIX after the editbox filtering + * It is a strict subset of SelectorWidget::list */ + std::vector filtered_list; + + static std::unique_ptr MakeSelectorWidgetUI(); + + void PreInit(Profile p); + void Init(Window* w); + void OnClick(Point pt, WidgetID widget, int click_count); + void OnInvalidateData(int data, bool gui_scope); + void UpdateWidgetSize(WidgetID widget, Dimension &size, const Dimension &padding, Dimension &fill, Dimension &resize); + void OnResize(); + void OnEditboxChanged(WidgetID wid); + void DrawWidget(const Rect &r, WidgetID widget); + void RebuildList(); +private: + /** + * This enum stores the WidgetIDs of all the widgets that belong to this selector widget. + * It starts at a very high value, not to interfear with other widgets of the parrent window + */ + enum InternalWidgets { + WID_SELECTOR_MATRIX = 42069, + WID_SELECTOR_SCROLLBAR, + WID_SELECTOR_EDITBOX, + WID_SELECTOR_HIDEALL, + WID_SELECTOR_SHOWALL, + WID_SELECTOR_TOGGLE, + }; + + Profile profile; ///< currently selected profile + + Window *w; ///< The parrent window pointer + + int row_height; ///< The height of 1 row in the WID_SELECTOR_MATRIX widget + + Scrollbar* vscroll; ///< A pointer to the WID_SELECTOR_SCROLLBAR widget + + /** A QueryString modifiable by the WID_SELECTOR_EDITBOX widget */ + QueryString editbox{MAX_LENGTH_COMPANY_NAME_CHARS * MAX_CHAR_LENGTH, MAX_LENGTH_COMPANY_NAME_CHARS}; + +}; +#endif /* SELECTOR_WIDGET_H */