diff --git a/src/object_gui.cpp b/src/object_gui.cpp index 09e9ba53c0..bab9513037 100644 --- a/src/object_gui.cpp +++ b/src/object_gui.cpp @@ -12,6 +12,10 @@ #include "newgrf.h" #include "newgrf_object.h" #include "newgrf_text.h" +#include "querystring_gui.h" +#include "sortlist_type.h" +#include "stringfilter_type.h" +#include "string_func.h" #include "strings_func.h" #include "viewport_func.h" #include "tilehighlight_func.h" @@ -25,23 +29,35 @@ #include "safeguards.h" -static ObjectClassID _selected_object_class; ///< the currently visible object class -static int _selected_object_index; ///< the index of the selected object in the current class or -1 -static uint8 _selected_object_view; ///< the view of the selected object +static const ObjectClass *_selected_object_class; ///< Currently selected available object class. +static int _selected_object_index; ///< Index of the currently selected object if existing, else \c -1. +static uint8 _selected_object_view; ///< the view of the selected object /** The window used for building objects. */ class BuildObjectWindow : public Window { - static const int OBJECT_MARGIN = 4; ///< The margin (in pixels) around an object. - int line_height; ///< The height of a single line. - int info_height; ///< The height of the info box. - Scrollbar *vscroll; ///< The scrollbar. + typedef GUIList GUIObjectClassList; ///< Type definition for the list to hold available object classes. + + static const uint EDITBOX_MAX_SIZE = 16; ///< The maximum number of characters for the filter edit box. + static const int OBJECT_MARGIN = 4; ///< The margin (in pixels) around an object. + + int line_height; ///< The height of a single line. + int info_height; ///< The height of the info box. + Scrollbar* vscroll; ///< The scrollbar. + + static Listing last_sorting; ///< Default sorting of #GUIObjectClassList. + static Filtering last_filtering; ///< Default filtering of #GUIObjectClassList. + static GUIObjectClassList::SortFunction * const sorter_funcs[]; ///< Sort functions of the #GUIObjectClassList. + static GUIObjectClassList::FilterFunction * const filter_funcs[]; ///< Filter functions of the #GUIObjectClassList. + GUIObjectClassList object_classes; ///< Available object classes. + StringFilter string_filter; ///< Filter for available objects. + QueryString filter_editbox; ///< Filter editbox. /** Scroll #WID_BO_CLASS_LIST so that the selected object class is visible. */ void EnsureSelectedObjectClassIsVisible() { uint pos = 0; - for (int i = 0; i < _selected_object_class; i++) { - if (ObjectClass::Get((ObjectClassID) i)->GetUISpecCount() == 0) continue; + for (auto objclass : this->object_classes) { + if (objclass == _selected_object_class) break; pos++; } this->vscroll->ScrollTowards(pos); @@ -55,10 +71,9 @@ class BuildObjectWindow : public Window { { if (_selected_object_index == -1) return false; - ObjectClass *sel_objclass = ObjectClass::Get(_selected_object_class); - if ((int)sel_objclass->GetSpecCount() <= _selected_object_index) return false; + if ((int)_selected_object_class->GetSpecCount() <= _selected_object_index) return false; - return sel_objclass->GetSpec(_selected_object_index)->IsAvailable(); + return _selected_object_class->GetSpec(_selected_object_index)->IsAvailable(); } /** @@ -72,42 +87,132 @@ class BuildObjectWindow : public Window { } public: - BuildObjectWindow(WindowDesc *desc, WindowNumber number) : Window(desc), info_height(1) + BuildObjectWindow(WindowDesc *desc, WindowNumber number) : Window(desc), info_height(1), filter_editbox(EDITBOX_MAX_SIZE) { this->CreateNestedTree(); + this->vscroll = this->GetScrollbar(WID_BO_SCROLLBAR); - this->FinishInitNested(number); - ResetObjectToPlace(); + this->querystrings[WID_BO_FILTER] = &this->filter_editbox; + + this->object_classes.SetListing(this->last_sorting); + this->object_classes.SetFiltering(this->last_filtering); + this->object_classes.SetSortFuncs(this->sorter_funcs); + this->object_classes.SetFilterFuncs(this->filter_funcs); + this->object_classes.ForceRebuild(); + + BuildObjectClassesAvailable(); + SelectClassAndObject(); - this->vscroll->SetPosition(0); - this->vscroll->SetCount(ObjectClass::GetUIClassCount()); + this->FinishInitNested(number); NWidgetMatrix *matrix = this->GetWidget(WID_BO_SELECT_MATRIX); matrix->SetScrollbar(this->GetScrollbar(WID_BO_SELECT_SCROLL)); - this->SelectOtherClass(_selected_object_class); + this->SetFocusedWidget(WID_BO_FILTER); + this->GetWidget(WID_BO_OBJECT_MATRIX)->SetCount(4); + + ResetObjectToPlace(); + + this->vscroll->SetCount((int) this->object_classes.size()); + + EnsureSelectedObjectClassIsVisible(); + + this->InvalidateData(); + } + + /** Sort object classes by class name. */ + static bool NameSorter(const ObjectClass *const& a, const ObjectClass *const& b) + { + char buffer_a[DRAW_STRING_BUFFER]; + GetString(buffer_a, a->name, lastof(buffer_a)); + + char buffer_b[DRAW_STRING_BUFFER]; + GetString(buffer_b, b->name, lastof(buffer_b)); + + return strnatcmp(buffer_a, buffer_b, true) < 0; // Sort by name (natural sorting). + } + + /** Filter object classes by class name. */ + static bool CDECL TagNameFilter(const ObjectClass *const* oc, StringFilter& filter) + { + char buffer[DRAW_STRING_BUFFER]; + GetString(buffer, (*oc)->name, lastof(buffer)); + + filter.ResetState(); + filter.AddLine(buffer); + return filter.GetState(); + } + + /** Builds the filter list of available object classes. */ + void BuildObjectClassesAvailable() + { + if (!this->object_classes.NeedRebuild()) return; + + this->object_classes.clear(); + + for (uint i = 0; i < ObjectClass::GetClassCount(); i++) { + ObjectClass *objclass = ObjectClass::Get((ObjectClassID)i); + if (objclass->GetUISpecCount() == 0) continue; // Is this needed here? + object_classes.push_back(objclass); + } + + this->object_classes.Filter(this->string_filter); + this->object_classes.shrink_to_fit(); + this->object_classes.RebuildDone(); + this->object_classes.Sort(); + + this->vscroll->SetCount((uint)this->object_classes.size()); + } + + /** + * Checks if the previously selected current object class and object + * can be shown as selected to the user when the dialog is opened. + */ + void SelectClassAndObject() + { + assert(!this->object_classes.empty()); // object GUI should be disabled elsewise + if (_selected_object_class == nullptr) { + /* This happens during the first time the window is open during the game life cycle. */ + this->SelectOtherClass(this->object_classes[0]); + } else { + /* Check if the previously selected object class is not available anymore as a + * result of starting a new game without the corresponding NewGRF. */ + bool available = false; + for (uint i = 0; ObjectClass::GetClassCount(); ++i) { + ObjectClass* objclass = ObjectClass::Get((ObjectClassID)i); + if (objclass == _selected_object_class) { + available = true; + break; + } + } + + if (available) { + this->SelectOtherClass(_selected_object_class); + } else { + this->SelectOtherClass(this->object_classes[0]); + } + } + if (this->CanRestoreSelectedObject()) { this->SelectOtherObject(_selected_object_index); } else { this->SelectFirstAvailableObject(true); } - assert(ObjectClass::Get(_selected_object_class)->GetUISpecCount() > 0); // object GUI should be disables elsewise - this->EnsureSelectedObjectClassIsVisible(); - this->GetWidget(WID_BO_OBJECT_MATRIX)->SetCount(4); + assert(_selected_object_class->GetUISpecCount() > 0); // object GUI should be disabled elsewise } void SetStringParameters(int widget) const override { switch (widget) { case WID_BO_OBJECT_NAME: { - const ObjectSpec *spec = ObjectClass::Get(_selected_object_class)->GetSpec(_selected_object_index); + const ObjectSpec *spec = _selected_object_class->GetSpec(_selected_object_index); SetDParam(0, spec != nullptr ? spec->name : STR_EMPTY); break; } case WID_BO_OBJECT_SIZE: { - const ObjectSpec *spec = ObjectClass::Get(_selected_object_class)->GetSpec(_selected_object_index); + const ObjectSpec *spec = _selected_object_class->GetSpec(_selected_object_index); int size = spec == nullptr ? 0 : spec->size; SetDParam(0, GB(size, HasBit(_selected_object_view, 0) ? 4 : 0, 4)); SetDParam(1, GB(size, HasBit(_selected_object_view, 0) ? 0 : 4, 4)); @@ -122,8 +227,7 @@ public: { switch (widget) { case WID_BO_CLASS_LIST: { - for (uint i = 0; i < ObjectClass::GetClassCount(); i++) { - ObjectClass *objclass = ObjectClass::Get((ObjectClassID)i); + for (auto objclass : this->object_classes) { if (objclass->GetUISpecCount() == 0) continue; size->width = std::max(size->width, GetStringBoundingBox(objclass->name).width); } @@ -142,7 +246,7 @@ public: case WID_BO_OBJECT_MATRIX: { /* Get the right amount of buttons based on the current spec. */ - const ObjectSpec *spec = ObjectClass::Get(_selected_object_class)->GetSpec(_selected_object_index); + const ObjectSpec *spec = _selected_object_class->GetSpec(_selected_object_index); if (spec != nullptr) { if (spec->views >= 2) size->width += resize->width; if (spec->views >= 4) size->height += resize->height; @@ -182,7 +286,7 @@ public: } /* Get the right size for the single widget based on the current spec. */ - const ObjectSpec *spec = ObjectClass::Get(_selected_object_class)->GetSpec(_selected_object_index); + const ObjectSpec *spec = _selected_object_class->GetSpec(_selected_object_index); if (spec != nullptr) { if (spec->views >= 2) size->width = size->width / 2 - 1; if (spec->views >= 4) size->height = size->height / 2 - 1; @@ -214,19 +318,20 @@ public: case WID_BO_CLASS_LIST: { int y = r.top; uint pos = 0; - for (uint i = 0; i < ObjectClass::GetClassCount(); i++) { - ObjectClass *objclass = ObjectClass::Get((ObjectClassID)i); + for (auto objclass : this->object_classes) { if (objclass->GetUISpecCount() == 0) continue; if (!this->vscroll->IsVisible(pos++)) continue; DrawString(r.left + WD_MATRIX_LEFT, r.right - WD_MATRIX_RIGHT, y + WD_MATRIX_TOP, objclass->name, - ((int)i == _selected_object_class) ? TC_WHITE : TC_BLACK); + (objclass == _selected_object_class) ? TC_WHITE : TC_BLACK); y += this->line_height; } break; } case WID_BO_OBJECT_SPRITE: { - const ObjectSpec *spec = ObjectClass::Get(_selected_object_class)->GetSpec(_selected_object_index); + if (_selected_object_index == -1) break; + + const ObjectSpec *spec = _selected_object_class->GetSpec(_selected_object_index); if (spec == nullptr) break; /* Height of the selection matrix. @@ -254,10 +359,9 @@ public: } case WID_BO_SELECT_IMAGE: { - ObjectClass *objclass = ObjectClass::Get(_selected_object_class); - int obj_index = objclass->GetIndexFromUI(GB(widget, 16, 16)); + int obj_index = _selected_object_class->GetIndexFromUI(GB(widget, 16, 16)); if (obj_index < 0) break; - const ObjectSpec *spec = objclass->GetSpec(obj_index); + const ObjectSpec *spec = _selected_object_class->GetSpec(obj_index); if (spec == nullptr) break; if (!spec->IsAvailable()) { @@ -282,7 +386,7 @@ public: } case WID_BO_INFO: { - const ObjectSpec *spec = ObjectClass::Get(_selected_object_class)->GetSpec(_selected_object_index); + const ObjectSpec *spec = _selected_object_class->GetSpec(_selected_object_index); if (spec == nullptr) break; /* Get the extra message for the GUI */ @@ -315,12 +419,12 @@ public: /** * Select the specified object class. - * @param object_class_index Object class index to select. + * @param object_class Object class select. */ - void SelectOtherClass(ObjectClassID object_class_index) + void SelectOtherClass(const ObjectClass *object_class) { - _selected_object_class = object_class_index; - this->GetWidget(WID_BO_SELECT_MATRIX)->SetCount(ObjectClass::Get(_selected_object_class)->GetUISpecCount()); + _selected_object_class = object_class; + this->GetWidget(WID_BO_SELECT_MATRIX)->SetCount(_selected_object_class->GetUISpecCount()); } /** @@ -331,7 +435,7 @@ public: { _selected_object_index = object_index; if (_selected_object_index != -1) { - const ObjectSpec *spec = ObjectClass::Get(_selected_object_class)->GetSpec(_selected_object_index); + const ObjectSpec *spec = _selected_object_class->GetSpec(_selected_object_index); _selected_object_view = std::min(_selected_object_view, spec->views - 1); this->ReInit(); } else { @@ -350,7 +454,7 @@ public: if (_selected_object_index == -1) { SetTileSelectSize(1, 1); } else { - const ObjectSpec *spec = ObjectClass::Get(_selected_object_class)->GetSpec(_selected_object_index); + const ObjectSpec *spec = _selected_object_class->GetSpec(_selected_object_index); int w = GB(spec->size, HasBit(_selected_object_view, 0) ? 4 : 0, 4); int h = GB(spec->size, HasBit(_selected_object_view, 0) ? 0 : 4, 4); SetTileSelectSize(w, h); @@ -359,11 +463,11 @@ public: /** * Update buttons to show the selection to the user. - * @param sel_class The class of the selected object. + * @param object_class The class of the selected object. * @param sel_index Index of the object to select, or \c -1 . * @param sel_view View of the object to select. */ - void UpdateButtons(ObjectClassID sel_class, int sel_index, uint sel_view) + void UpdateButtons(const ObjectClass *object_class, int sel_index, uint sel_view) { int view_number, object_number; if (sel_index == -1) { @@ -371,7 +475,7 @@ public: object_number = -1; } else { view_number = sel_view; - object_number = ObjectClass::Get(sel_class)->GetUIFromIndex(sel_index); + object_number = object_class->GetUIFromIndex(sel_index); } this->GetWidget(WID_BO_OBJECT_MATRIX)->SetClicked(view_number); @@ -380,6 +484,13 @@ public: this->SetDirty(); } + void OnInvalidateData(int data = 0, bool gui_scope = true) override + { + if (!gui_scope) return; + + this->BuildObjectClassesAvailable(); + } + void OnResize() override { this->vscroll->SetCapacityFromWidget(this, WID_BO_CLASS_LIST); @@ -390,17 +501,16 @@ public: switch (GB(widget, 0, 16)) { case WID_BO_CLASS_LIST: { int num_clicked = this->vscroll->GetPosition() + (pt.y - this->nested_array[widget]->pos_y) / this->line_height; - if (num_clicked >= (int)ObjectClass::GetUIClassCount()) break; + if (num_clicked >= (int)this->object_classes.size()) break; - this->SelectOtherClass(ObjectClass::GetUIClass(num_clicked)); + this->SelectOtherClass(this->object_classes[num_clicked]); this->SelectFirstAvailableObject(false); break; } case WID_BO_SELECT_IMAGE: { - ObjectClass *objclass = ObjectClass::Get(_selected_object_class); - int num_clicked = objclass->GetIndexFromUI(GB(widget, 16, 16)); - if (num_clicked >= 0 && objclass->GetSpec(num_clicked)->IsAvailable()) this->SelectOtherObject(num_clicked); + int num_clicked = _selected_object_class->GetIndexFromUI(GB(widget, 16, 16)); + if (num_clicked >= 0 && _selected_object_class->GetSpec(num_clicked)->IsAvailable()) this->SelectOtherObject(num_clicked); break; } @@ -415,7 +525,7 @@ public: void OnPlaceObject(Point pt, TileIndex tile) override { - DoCommandP(tile, ObjectClass::Get(_selected_object_class)->GetSpec(_selected_object_index)->Index(), + DoCommandP(tile, _selected_object_class->GetSpec(_selected_object_index)->Index(), _selected_object_view, CMD_BUILD_OBJECT | CMD_MSG(STR_ERROR_CAN_T_BUILD_OBJECT), CcTerraform); } @@ -424,6 +534,14 @@ public: this->UpdateButtons(_selected_object_class, -1, _selected_object_view); } + void OnEditboxChanged(int wid) override + { + string_filter.SetFilterTerm(this->filter_editbox.text.buf); + this->object_classes.SetFilterState(!string_filter.IsEmpty()); + this->object_classes.ForceRebuild(); + this->InvalidateData(); + } + /** * Select the first available object. * @param change_class If true, change the class if no object in the current @@ -432,9 +550,8 @@ public: void SelectFirstAvailableObject(bool change_class) { /* First try to select an object in the selected class. */ - ObjectClass *sel_objclass = ObjectClass::Get(_selected_object_class); - for (uint i = 0; i < sel_objclass->GetSpecCount(); i++) { - const ObjectSpec *spec = sel_objclass->GetSpec(i); + for (uint i = 0; i < _selected_object_class->GetSpecCount(); i++) { + const ObjectSpec *spec = _selected_object_class->GetSpec(i); if (spec->IsAvailable()) { this->SelectOtherObject(i); return; @@ -443,12 +560,11 @@ public: if (change_class) { /* If that fails, select the first available object * from a random class. */ - for (ObjectClassID j = OBJECT_CLASS_BEGIN; j < OBJECT_CLASS_MAX; j++) { - ObjectClass *objclass = ObjectClass::Get(j); + for (auto objclass : this->object_classes) { for (uint i = 0; i < objclass->GetSpecCount(); i++) { const ObjectSpec *spec = objclass->GetSpec(i); if (spec->IsAvailable()) { - this->SelectOtherClass(j); + this->SelectOtherClass(objclass); this->SelectOtherObject(i); return; } @@ -456,11 +572,11 @@ public: } } /* If all objects are unavailable, select nothing... */ - if (ObjectClass::Get(_selected_object_class)->GetUISpecCount() == 0) { + if (_selected_object_class->GetUISpecCount() == 0) { /* ... but make sure that the class is not empty. */ - for (ObjectClassID j = OBJECT_CLASS_BEGIN; j < OBJECT_CLASS_MAX; j++) { - if (ObjectClass::Get(j)->GetUISpecCount() > 0) { - this->SelectOtherClass(j); + for (auto objclass : this->object_classes) { + if (objclass->GetUISpecCount() > 0) { + this->SelectOtherClass(objclass); break; } } @@ -469,6 +585,17 @@ public: } }; +Listing BuildObjectWindow::last_sorting = { false, 0 }; +Filtering BuildObjectWindow::last_filtering = { false, 0 }; + +BuildObjectWindow::GUIObjectClassList::SortFunction* const BuildObjectWindow::sorter_funcs[] = { + &NameSorter, +}; + +BuildObjectWindow::GUIObjectClassList::FilterFunction* const BuildObjectWindow::filter_funcs[] = { + &TagNameFilter, +}; + static const NWidgetPart _nested_build_object_widgets[] = { NWidget(NWID_HORIZONTAL), NWidget(WWT_CLOSEBOX, COLOUR_DARK_GREEN), @@ -478,6 +605,11 @@ static const NWidgetPart _nested_build_object_widgets[] = { NWidget(WWT_PANEL, COLOUR_DARK_GREEN), NWidget(NWID_HORIZONTAL), SetPadding(2, 0, 0, 0), NWidget(NWID_VERTICAL), + NWidget(NWID_HORIZONTAL), SetPadding(0, 5, 2, 5), + NWidget(WWT_TEXT, COLOUR_DARK_GREEN), SetFill(0, 1), SetDataTip(STR_LIST_FILTER_TITLE, STR_NULL), + NWidget(WWT_EDITBOX, COLOUR_GREY, WID_BO_FILTER), SetFill(1, 0), SetResize(1, 0), + SetDataTip(STR_LIST_FILTER_OSKTITLE, STR_LIST_FILTER_TOOLTIP), + EndContainer(), NWidget(NWID_HORIZONTAL), SetPadding(0, 5, 2, 5), NWidget(WWT_MATRIX, COLOUR_GREY, WID_BO_CLASS_LIST), SetFill(1, 0), SetMatrixDataTip(1, 0, STR_OBJECT_BUILD_CLASS_TOOLTIP), SetScrollbar(WID_BO_SCROLLBAR), NWidget(NWID_VSCROLLBAR, COLOUR_GREY, WID_BO_SCROLLBAR), @@ -530,5 +662,5 @@ void ShowBuildObjectPicker() /** Reset all data of the object GUI. */ void InitializeObjectGui() { - _selected_object_class = (ObjectClassID)0; + _selected_object_class = nullptr; } diff --git a/src/widgets/object_widget.h b/src/widgets/object_widget.h index 9ca06e2fea..9afb783c56 100644 --- a/src/widgets/object_widget.h +++ b/src/widgets/object_widget.h @@ -12,6 +12,7 @@ /** Widgets of the #BuildObjectWindow class. */ enum BuildObjectWidgets { + WID_BO_FILTER, ///< The filter text box for the object list. WID_BO_CLASS_LIST, ///< The list with classes. WID_BO_SCROLLBAR, ///< The scrollbar associated with the list. WID_BO_OBJECT_MATRIX, ///< The matrix with preview sprites.