diff --git a/data/language/en-GB.txt b/data/language/en-GB.txt index 01bcb7a1db..a39784b3f1 100644 --- a/data/language/en-GB.txt +++ b/data/language/en-GB.txt @@ -3839,3 +3839,5 @@ STR_6797 :No preview STR_6798 :Screenshot STR_6799 :Mini map STR_7000 :or +STR_7001 :Ride name +STR_7002 :{STRINGID} {STRINGID} diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 91c5bba430..df04209894 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -1,5 +1,6 @@ 0.4.26 (in development) ------------------------------------------------------------------------ +- Improved: [#24893] The ride list now has headers, and can be sorted in both directions. - Fix: [#16988] AppImage version does not show changelog. 0.4.25 (2025-08-03) diff --git a/src/openrct2-ui/UiStringIds.h b/src/openrct2-ui/UiStringIds.h index 574890bc31..c5d11309aa 100644 --- a/src/openrct2-ui/UiStringIds.h +++ b/src/openrct2-ui/UiStringIds.h @@ -1771,6 +1771,8 @@ namespace OpenRCT2 STR_RELIABILITY = 1832, STR_RELIABILITY_LABEL = 1839, STR_RIDES = 974, + STR_RIDE_LIST_HEADER_FORMAT = 7002, + STR_RIDE_LIST_NAME = 7001, STR_RIDE_LIST_AGE = 5772, STR_RIDE_LIST_BUILT_LAST_YEAR_LABEL = 5777, STR_RIDE_LIST_BUILT_THIS_YEAR_LABEL = 5776, diff --git a/src/openrct2-ui/windows/RideList.cpp b/src/openrct2-ui/windows/RideList.cpp index a8b79a2c8d..07251d4ca4 100644 --- a/src/openrct2-ui/windows/RideList.cpp +++ b/src/openrct2-ui/windows/RideList.cpp @@ -7,10 +7,9 @@ * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ -#include "../interface/Theme.h" - #include #include +#include #include #include #include @@ -50,12 +49,12 @@ namespace OpenRCT2::Ui::Windows WIDX_CLOSE, WIDX_PAGE_BACKGROUND, WIDX_OPEN_CLOSE_ALL, - WIDX_CURRENT_INFORMATION_TYPE, - WIDX_INFORMATION_TYPE_DROPDOWN, - WIDX_SORT, WIDX_TAB_1, WIDX_TAB_2, WIDX_TAB_3, + WIDX_HEADER_NAME, + WIDX_HEADER_OTHER, + WIDX_HEADER_CUSTOMISE, WIDX_LIST, WIDX_CLOSE_LIGHT, WIDX_OPEN_LIGHT, @@ -67,12 +66,12 @@ namespace OpenRCT2::Ui::Windows makeWindowShim(kWindowTitle, kWindowSize), makeWidget({ 0, 43}, {340, 197}, WidgetType::resize, WindowColour::secondary ), // tab page background makeWidget({315, 60}, { 24, 24}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_TOGGLE_OPEN_CLOSE), STR_OPEN_OR_CLOSE_ALL_RIDES ), // open / close all toggle - makeWidget({150, 46}, {124, 12}, WidgetType::dropdownMenu, WindowColour::secondary ), // current information type - makeWidget({262, 47}, { 11, 10}, WidgetType::button, WindowColour::secondary, STR_DROPDOWN_GLYPH, STR_RIDE_LIST_INFORMATION_TYPE_TIP), // information type dropdown button - makeWidget({280, 46}, { 54, 12}, WidgetType::button, WindowColour::secondary, STR_SORT, STR_RIDE_LIST_SORT_TIP ), // sort button makeTab ({ 3, 17}, STR_LIST_RIDES_TIP ), // tab 1 makeTab ({ 34, 17}, STR_LIST_SHOPS_AND_STALLS_TIP ), // tab 2 makeTab ({ 65, 17}, STR_LIST_KIOSKS_AND_FACILITIES_TIP ), // tab 3 + makeWidget({ 3, 47}, {150, 14}, WidgetType::tableHeader, WindowColour::secondary ), // name header + makeWidget({153, 47}, {147, 14}, WidgetType::tableHeader, WindowColour::secondary ), // other header + makeWidget({300, 47}, { 14, 14}, WidgetType::button, WindowColour::secondary, STR_DROPDOWN_GLYPH ), // customise button makeWidget({ 3, 60}, {334, 177}, WidgetType::scroll, WindowColour::secondary, SCROLL_VERTICAL ), // list makeWidget({320, 62}, { 14, 14}, WidgetType::imgBtn, WindowColour::secondary, ImageId(SPR_G2_RCT1_CLOSE_BUTTON_0) ), makeWidget({320, 76}, { 14, 14}, WidgetType::imgBtn, WindowColour::secondary, ImageId(SPR_G2_RCT1_OPEN_BUTTON_0) ), @@ -80,7 +79,7 @@ namespace OpenRCT2::Ui::Windows ); // clang-format on - enum + enum InformationType { INFORMATION_TYPE_STATUS, INFORMATION_TYPE_POPULARITY, @@ -164,7 +163,8 @@ namespace OpenRCT2::Ui::Windows { private: bool _quickDemolishMode = false; - int32_t _windowRideListInformationType = INFORMATION_TYPE_STATUS; + InformationType _windowRideListInformationType = INFORMATION_TYPE_STATUS; + bool _windowListSortDescending = false; struct RideListEntry { @@ -178,16 +178,14 @@ namespace OpenRCT2::Ui::Windows { SetWidgets(_rideListWidgets); WindowInitScrollWidgets(*this); - WindowSetResize(*this, { 340, 240 }, { 400, 700 }); + WindowSetResize(*this, kWindowSize, kWindowSize * 2); page = PAGE_RIDES; - selected_list_item = -1; frame_no = 0; + list_information_type = INFORMATION_TYPE_STATUS; RefreshList(); - list_information_type = 0; - _windowRideListInformationType = INFORMATION_TYPE_STATUS; _quickDemolishMode = false; } @@ -209,12 +207,6 @@ namespace OpenRCT2::Ui::Windows height = min_height; } - widgets[WIDX_SORT].left = width - 60; - widgets[WIDX_SORT].right = width - 60 + 54; - - auto dropdownStart = widgets[WIDX_CURRENT_INFORMATION_TYPE].top; - ResizeDropdown(WIDX_CURRENT_INFORMATION_TYPE, { 100, dropdownStart }, { width - 166, kDropdownHeight }); - // Refreshing the list can be a very intensive operation // owing to its use of ride_has_any_track_elements(). // This makes sure it's only refreshed every 64 ticks. @@ -235,9 +227,28 @@ namespace OpenRCT2::Ui::Windows case WIDX_CLOSE: Close(); break; - case WIDX_SORT: - list_information_type = _windowRideListInformationType; - selected_list_item = -1; + case WIDX_HEADER_NAME: + if (list_information_type != INFORMATION_TYPE_STATUS) + { + list_information_type = INFORMATION_TYPE_STATUS; + _windowListSortDescending = false; + } + else + { + _windowListSortDescending ^= true; + } + RefreshList(); + break; + case WIDX_HEADER_OTHER: + if (list_information_type != _windowRideListInformationType) + { + list_information_type = _windowRideListInformationType; + _windowListSortDescending = true; + } + else + { + _windowListSortDescending ^= true; + } RefreshList(); break; case WIDX_TAB_1: @@ -247,7 +258,6 @@ namespace OpenRCT2::Ui::Windows { page = widgetIndex - WIDX_TAB_1; frame_no = 0; - selected_list_item = -1; if (page != PAGE_RIDES && _windowRideListInformationType > INFORMATION_TYPE_RUNNING_COST) { _windowRideListInformationType = INFORMATION_TYPE_STATUS; @@ -289,10 +299,8 @@ namespace OpenRCT2::Ui::Windows WindowDropdownShowText( { windowPos.x + widget.left, windowPos.y + widget.top }, widget.height(), colours[1], 0, 2); } - else if (widgetIndex == WIDX_INFORMATION_TYPE_DROPDOWN) + else if (widgetIndex == WIDX_HEADER_CUSTOMISE) { - const auto& widget = widgets[widgetIndex - 1]; - int32_t lastType; if (page == PAGE_RIDES) lastType = INFORMATION_TYPE_NAUSEA; @@ -321,9 +329,14 @@ namespace OpenRCT2::Ui::Windows numItems++; } + const auto& headerWidget = widgets[WIDX_HEADER_OTHER]; + const auto& customWidget = widgets[WIDX_HEADER_CUSTOMISE]; + auto totalWidth = headerWidget.width() + customWidget.width(); + WindowDropdownShowTextCustomWidth( - { windowPos.x + widget.left, windowPos.y + widget.top }, widget.height(), colours[1], 0, - Dropdown::Flag::StayOpen, numItems, widget.width() - 3); + { windowPos.x + headerWidget.left, windowPos.y + headerWidget.top }, headerWidget.height(), colours[1], 0, + Dropdown::Flag::StayOpen, numItems, totalWidth); + if (selectedIndex != -1) { Dropdown::SetChecked(selectedIndex, true); @@ -350,7 +363,7 @@ namespace OpenRCT2::Ui::Windows Invalidate(); } - else if (widgetIndex == WIDX_INFORMATION_TYPE_DROPDOWN) + else if (widgetIndex == WIDX_HEADER_CUSTOMISE) { if (dropdownIndex == -1) return; @@ -365,8 +378,15 @@ namespace OpenRCT2::Ui::Windows } } - _windowRideListInformationType = informationType; + _windowRideListInformationType = InformationType(informationType); Invalidate(); + + // Automatically change sort if we're sorting by the custom/info column + if (list_information_type != INFORMATION_TYPE_STATUS) + { + list_information_type = _windowRideListInformationType; + RefreshList(); + } } } @@ -389,15 +409,7 @@ namespace OpenRCT2::Ui::Windows ScreenSize OnScrollGetSize(int32_t scrollIndex) override { const auto newHeight = static_cast(_rideList.size() * kScrollableRowHeight); - if (selected_list_item != -1) - { - selected_list_item = -1; - Invalidate(); - } - - auto top = newHeight - widgets[WIDX_LIST].bottom + widgets[WIDX_LIST].top + 21; - if (top < 0) - top = 0; + auto top = std::max(0, newHeight - widgets[WIDX_LIST].bottom + widgets[WIDX_LIST].top + 21); if (top < scrolls[0].contentOffsetY) { scrolls[0].contentOffsetY = top; @@ -453,7 +465,8 @@ namespace OpenRCT2::Ui::Windows */ void OnPrepareDraw() override { - widgets[WIDX_CURRENT_INFORMATION_TYPE].text = ride_info_type_string_mapping[_windowRideListInformationType]; + auto ft = Formatter(); + ft.Add(STR_UP); // Set correct active tab for (int32_t i = 0; i < 3; i++) @@ -469,6 +482,17 @@ namespace OpenRCT2::Ui::Windows widgets[WIDX_LIST].right = width - 26; widgets[WIDX_LIST].bottom = height - 15; + + widgets[WIDX_HEADER_CUSTOMISE].right = widgets[WIDX_LIST].right - 1; + widgets[WIDX_HEADER_CUSTOMISE].left = widgets[WIDX_HEADER_CUSTOMISE].right - 14; + + auto columnWidth = (widgets[WIDX_LIST].width() - widgets[WIDX_HEADER_CUSTOMISE].width()) / 2; + + widgets[WIDX_HEADER_OTHER].right = widgets[WIDX_HEADER_CUSTOMISE].left - 1; + widgets[WIDX_HEADER_OTHER].left = widgets[WIDX_HEADER_OTHER].right - columnWidth + 1; + + widgets[WIDX_HEADER_NAME].right = widgets[WIDX_HEADER_NAME].left + columnWidth - 1; + widgets[WIDX_OPEN_CLOSE_ALL].right = width - 2; widgets[WIDX_OPEN_CLOSE_ALL].left = width - 25; widgets[WIDX_CLOSE_LIGHT].right = width - 7; @@ -527,6 +551,26 @@ namespace OpenRCT2::Ui::Windows WindowDrawWidgets(*this, rt); DrawTabImages(rt); + const auto drawButtonCaption = [rt, this](Widget& widget, StringId strId, InformationType sortType) { + StringId indicatorId = kStringIdNone; + if (list_information_type == sortType && !(strId == STR_STATUS && sortType == INFORMATION_TYPE_STATUS)) + indicatorId = _windowListSortDescending ? STR_DOWN : STR_UP; + + auto ft = Formatter(); + ft.Add(strId); + ft.Add(indicatorId); + + auto cdpi = const_cast(rt); + DrawTextEllipsised( + cdpi, windowPos + ScreenCoordsXY{ widget.left + 1, widget.top + 1 }, widget.width(), + STR_RIDE_LIST_HEADER_FORMAT, ft, { colours[1] }); + }; + + drawButtonCaption(widgets[WIDX_HEADER_NAME], STR_RIDE_LIST_NAME, INFORMATION_TYPE_STATUS); + + auto otherStringId = ride_info_type_string_mapping[_windowRideListInformationType]; + drawButtonCaption(widgets[WIDX_HEADER_OTHER], otherStringId, _windowRideListInformationType); + // Draw number of attractions on bottom auto ft = Formatter(); ft.Add(static_cast(_rideList.size())); @@ -569,7 +613,9 @@ namespace OpenRCT2::Ui::Windows // Ride name auto ft = Formatter(); ridePtr->formatNameTo(ft); - DrawTextEllipsised(rt, { 0, y - 1 }, 159, format, ft); + + auto& nameHeader = widgets[WIDX_HEADER_NAME]; + DrawTextEllipsised(rt, { 0, y - 1 }, nameHeader.width() - 2, format, ft); // Ride information ft = Formatter(); @@ -758,6 +804,8 @@ namespace OpenRCT2::Ui::Windows ft.Add(ridePtr->ratings.nausea); } break; + default: + break; } if (formatSecondaryEnabled) @@ -765,7 +813,9 @@ namespace OpenRCT2::Ui::Windows ft.Rewind(); ft.Add(formatSecondary); } - DrawTextEllipsised(rt, { 160, y - 1 }, 157, format, ft); + + auto infoHeader = widgets[WIDX_HEADER_OTHER]; + DrawTextEllipsised(rt, { infoHeader.left - 4, y - 1 }, infoHeader.width() - 2, format, ft); y += kScrollableRowHeight; } } @@ -813,21 +863,25 @@ namespace OpenRCT2::Ui::Windows template void SortListByPredicate(const TSortPred& pred) { - std::sort(_rideList.begin(), _rideList.end(), [&pred](const auto& lhs, const auto& rhs) { + bool desc = _windowListSortDescending; + std::stable_sort(_rideList.begin(), _rideList.end(), [desc, pred](const auto& lhs, const auto& rhs) { const Ride* rideLhs = GetRide(lhs.Id); const Ride* rideRhs = GetRide(rhs.Id); if (rideLhs == nullptr || rideRhs == nullptr) { return rideLhs != nullptr; } - return !pred(*rideLhs, *rideRhs); + bool result = pred(*rideLhs, *rideRhs); + return desc ? !result : result; }); } void SortListByName() { - std::sort(_rideList.begin(), _rideList.end(), [](const auto& lhs, const auto& rhs) { - return !(0 <= String::logicalCmp(lhs.Name.c_str(), rhs.Name.c_str())); + bool desc = _windowListSortDescending; + std::stable_sort(_rideList.begin(), _rideList.end(), [desc](const auto& lhs, const auto& rhs) { + bool result = (0 > String::logicalCmp(lhs.Name.c_str(), rhs.Name.c_str())); + return desc ? !result : result; }); }