From 2af1065c4e3790989d7bcd1549f70428e144a10e Mon Sep 17 00:00:00 2001 From: Andrii Dokhniak Date: Sun, 30 Jun 2024 16:52:50 +0200 Subject: [PATCH 1/2] Codechange: Add the universal selector widget. --- src/CMakeLists.txt | 2 + src/lang/english.txt | 14 +- src/selector_gui.cpp | 375 ++++++++++++++++++++++++++++++++++ src/selector_gui.h | 95 +++++++++ src/widgets/CMakeLists.txt | 1 + src/widgets/selector_widget.h | 25 +++ 6 files changed, 505 insertions(+), 7 deletions(-) create mode 100644 src/selector_gui.cpp create mode 100644 src/selector_gui.h create mode 100644 src/widgets/selector_widget.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5f7847ff8a..e015544b5e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -378,6 +378,8 @@ add_files( screenshot_gui.h screenshot.cpp screenshot.h + selector_gui.cpp + selector_gui.h settings.cpp settings_cmd.h settings_func.h diff --git a/src/lang/english.txt b/src/lang/english.txt index 87a8d3c594..65fd0c6cd8 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -619,18 +619,18 @@ STR_GRAPH_CARGO_PAYMENT_RATES_CAPTION :{WHITE}Cargo Pa STR_GRAPH_CARGO_PAYMENT_RATES_DAYS :{TINY_FONT}{BLACK}Days in transit STR_GRAPH_CARGO_PAYMENT_RATES_SECONDS :{TINY_FONT}{BLACK}Seconds in transit STR_GRAPH_CARGO_PAYMENT_RATES_TITLE :{TINY_FONT}{BLACK}Payment for delivering 10 units (or 10,000 litres) of cargo a distance of 20 squares -STR_GRAPH_CARGO_ENABLE_ALL :{TINY_FONT}{BLACK}Enable all -STR_GRAPH_CARGO_DISABLE_ALL :{TINY_FONT}{BLACK}Disable all -STR_GRAPH_CARGO_TOOLTIP_ENABLE_ALL :{BLACK}Display all cargoes on the cargo payment rates graph -STR_GRAPH_CARGO_TOOLTIP_DISABLE_ALL :{BLACK}Display no cargoes on the cargo payment rates graph -STR_GRAPH_CARGO_PAYMENT_TOGGLE_CARGO :{BLACK}Toggle graph of this cargo type -STR_GRAPH_CARGO_PAYMENT_CARGO :{TINY_FONT}{BLACK}{STRING} + +# Selector widget +STR_SELECTOR_WIDGET_ALL :{BLACK}All +STR_SELECTOR_WIDGET_NONE :{BLACK}None +STR_SELECTOR_WIDGET_TOOLTIP_ALL :{BLACK}Display all of the items in the list +STR_SELECTOR_WIDGET_TOOLTIP_NONE :{BLACK}Display none of the items in the list STR_GRAPH_PERFORMANCE_DETAIL_TOOLTIP :{BLACK}Show detailed performance ratings # Graph key window STR_GRAPH_KEY_CAPTION :{WHITE}Key to company graphs -STR_GRAPH_KEY_COMPANY_SELECTION_TOOLTIP :{BLACK}Toggle graph of this company +STR_GRAPH_CARGO_KEY_CAPTION :{WHITE}Key to cargo graphs # Company league window STR_COMPANY_LEAGUE_TABLE_CAPTION :{WHITE}Company League Table diff --git a/src/selector_gui.cpp b/src/selector_gui.cpp new file mode 100644 index 0000000000..574c2fa918 --- /dev/null +++ b/src/selector_gui.cpp @@ -0,0 +1,375 @@ +/* + * 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_gui.cpp A composable selector widget. */ + +#include "stdafx.h" +#include "company_base.h" +#include "company_gui.h" +#include "strings_func.h" +#include "table/sprites.h" +#include "selector_gui.h" +#include "widget_type.h" +#include "widgets/selector_widget.h" +#include "zoom_func.h" + +#include "safeguards.h" + +/** + * 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 parent 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_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_SELECTOR_SHOWALL), + SetDataTip(STR_SELECTOR_WIDGET_ALL, STR_SELECTOR_WIDGET_TOOLTIP_ALL), SetResize(1, 0), SetFill(1, 0), + NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_SELECTOR_HIDEALL), + SetDataTip(STR_SELECTOR_WIDGET_NONE, STR_SELECTOR_WIDGET_TOOLTIP_NONE), SetResize(1, 0), SetFill(1, 0), + NWidget(WWT_RESIZEBOX, COLOUR_BROWN, WID_SELECTOR_RESIZE), SetDataTip(RWV_SHOW_BEVEL, STR_TOOLTIP_RESIZE), SetResize(0, 0), + EndContainer(), + EndContainer(), + }; + return MakeNWidgets(widget_ui, nullptr); +} + +/** + * Selector widget initialization function + * This function is meant to be called after CreateNestedTree() of the parent window + * + * @param w The parent window of this widget. + */ +void SelectorWidget::Init(Window *w) +{ + this->parent_window = 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's OnClick event handler + * This function is meant to be called from the parent'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 these parameters should be passed straight from the parent's window's OnClick + * Event handler, without any changes, or conditional checks + * + * @see Window::OnClick() + */ +void SelectorWidget::OnClick(Point pt, WidgetID widget, [[maybe_unused]]int click_count) +{ + switch (widget) { + case WID_SELECTOR_HIDEALL: + for (uint i = 0; i < this->shown.size(); i++) { + this->shown[i] = false; + } + this->OnChanged(); + this->parent_window->InvalidateData(0, true); + break; + + case WID_SELECTOR_SHOWALL: + for (uint i = 0; i < this->shown.size(); i++) { + this->shown[i] = true; + } + this->OnChanged(); + this->parent_window->InvalidateData(0, true); + break; + + case WID_SELECTOR_MATRIX: { + size_t pos = vscroll->GetScrolledRowFromWidget(pt.y, this->parent_window, widget); + + /* Check if the click was out of range. */ + if (pos >= filtered_list.size()) return; + int id = this->filtered_list[pos]; + + this->shown[id].flip(); + this->OnChanged(); + this->parent_window->InvalidateData(0, true); + break; + } + } +} + +void SelectorWidget::OnMouseOver(Point pt, WidgetID widget) +{ + if (widget != WID_SELECTOR_MATRIX) return; + + auto it = vscroll->GetScrolledItemFromWidget(this->filtered_list, pt.y, this->parent_window, widget); + /* Check if the hover was out of range. */ + if (it == this->filtered_list.end()) return; + + /* Check if the selection actually changed. */ + if (*it == this->selected_id) return; + + this->selected_id = *it; + + this->OnChanged(); + this->parent_window->InvalidateData(0, true); +} + +/** + * Selector widget's OnInvalidateData event handler + * This function is meant to be called from the parent's window's OnInvalidateData + * Event handler. This function will do nothing when runnning ouside of the gui scope. + * + * @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 these parameters should be passed straight from the parent'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->SetCount(this->filtered_list.size()); + this->vscroll->SetCapacityFromWidget(parent_window, WID_SELECTOR_MATRIX); + + /* This does not assume that the IDs must be contiguous. */ + auto it = std::find(std::begin(this->filtered_list), std::end(this->filtered_list), this->selected_id); + + if (it != std::end(this->filtered_list)) { + int selected_pos = std::distance(std::begin(this->filtered_list), it); + this->vscroll->ScrollTowards(selected_pos); + } +} + +/** + * Selector widget's UpdateWidgetSize method + * This function is meant to be called from the parent'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 these parameters should be passed straight from the parent'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; + + const int min_rows = 11; ///< The minimal number of rows shown + const int min_width = ScaleGUITrad(100); ///< The minimal width of the widget + + + this->row_height = GetCharacterHeight(FS_NORMAL) + padding.height; + + size.height = this->row_height * min_rows; + size.width = min_width; + 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 meant to be called from the parent's window's OnResize + * Event handler + * + * All of these parameters should be passed straight from the parent's window's OnResize + * Event handler, without any changes, or conditional checks + * + * @see Window::OnResize() + */ +void SelectorWidget::OnResize() +{ + this->vscroll->SetCapacityFromWidget(parent_window, WID_SELECTOR_MATRIX); +} + +/** + * Selector widget's OnEditboxChanged event handler + * This function is meant to be called from the parent's window's OnEditboxChanged + * Event handler + * + * @param wid The widget id of the editbox. + * + * All of these parameters should be passed straight from the parent'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->parent_window->InvalidateData(0); + this->vscroll->SetCount(this->filtered_list.size()); +} + +/** + * Selector widget's DrawWidget method + * This function is meant to be called from the parent'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 these parameters should be passed straight from the parent'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); + + 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(line, COLOUR_BROWN, FR_LOWERED); + } + this->DrawSection(*it, ir.Shrink(WidgetDimensions::scaled.matrix)); + + line = line.Translate(0, row_height); + } +} + +/** + * 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->PopulateList(); + uint32_t max = *std::max_element(std::begin(this->list), std::end(this->list)); + + this->shown.reserve(max); + for (size_t i = this->shown.size(); i <= max ; i++) { + this->shown.push_back(true); + } +} + +/* virtual */ void CargoSelectorWidget::PopulateList() +{ + for (const CargoSpec *cargo : _sorted_standard_cargo_specs){ + this->list.push_back(cargo->Index()); + if (this->string_filter.IsEmpty()) { + this->filtered_list.push_back(cargo->Index()); + continue; + } + this->string_filter.ResetState(); + + SetDParam(0, cargo->name); + this->string_filter.AddLine(GetString(STR_JUST_STRING)); + + if (this->string_filter.GetState()) { + this->filtered_list.push_back(cargo->Index()); + } + } +} + +/* virtual */ void CargoSelectorWidget::DrawSection(uint id, const Rect &r) +{ + const CargoID cargo_id = static_cast(id); ///< CargoID of the current row's cargo + const CargoSpec *cargo = CargoSpec::Get(id); ///< CargoSpec of the current row's cargo + + const bool rtl = _current_text_dir == TD_RTL; + + int legend_height = GetCharacterHeight(FS_SMALL); + int legend_width = legend_height * 9 / 6; + + Rect cargo_swatch = r.WithWidth(legend_width, rtl); + cargo_swatch.top = CenterBounds(r.top, r.bottom, legend_height) - 1; + cargo_swatch.bottom = cargo_swatch.top + legend_height; + + /* Cargo-colour box with outline. */ + GfxFillRect(cargo_swatch, PC_BLACK); + GfxFillRect(cargo_swatch.Shrink(WidgetDimensions::scaled.bevel), cargo->legend_colour); + + /* Cargo name. */ + SetDParam(0, cargo->name); + const Rect text = r.Indent(legend_width + WidgetDimensions::scaled.hsep_wide, rtl); + + const TextColour colour = (this->selected_id.value_or(INVALID_OWNER) == cargo_id) ? TC_WHITE : TC_BLACK; + DrawString(text.left, text.right, CenterBounds(text.top, text.bottom, GetCharacterHeight(FS_NORMAL)), STR_JUST_STRING, colour); +} + + +/* virtual */ void CompanySelectorWidget::PopulateList() +{ + for (const Company *c: Company::Iterate()) { + this->list.push_back(c->index); + if (this->string_filter.IsEmpty()) { + this->filtered_list.push_back(c->index); + continue; + } + this->string_filter.ResetState(); + + SetDParam(0, c->index); + this->string_filter.AddLine(GetString(STR_COMPANY_NAME)); + + if (this->string_filter.GetState()) { + this->filtered_list.push_back(c->index); + } + } +} + +/* virtual */ void CompanySelectorWidget::DrawSection(uint id, const Rect &r) +{ + const CompanyID cid = static_cast(id); + assert(Company::IsValidID(cid)); + + const bool rtl = _current_text_dir == TD_RTL; + const Dimension icon_size = GetSpriteSize(SPR_COMPANY_ICON); + + DrawCompanyIcon(cid, rtl ? r.right - icon_size.width : r.left, CenterBounds(r.top, r.bottom, icon_size.height)); + + const Rect text = r.Indent(icon_size.width + WidgetDimensions::scaled.hsep_wide, rtl); + + SetDParam(0, cid); + + const TextColour colour = (this->selected_id.value_or(INVALID_OWNER) == cid) ? TC_WHITE : TC_BLACK; + DrawString(text.left, text.right, CenterBounds(text.top, text.bottom, GetCharacterHeight(FS_NORMAL)), STR_COMPANY_NAME, colour); +} diff --git a/src/selector_gui.h b/src/selector_gui.h new file mode 100644 index 0000000000..8e31c5bb7c --- /dev/null +++ b/src/selector_gui.h @@ -0,0 +1,95 @@ +/* + * 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_gui.h Functions/types etc. related to the selector widget. */ + +#ifndef SELECTOR_GUI_H +#define SELECTOR_GUI_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" + +struct SelectorWidget { + + 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 wether 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 Init(Window *w); + void OnClick(Point pt, WidgetID widget, int click_count); + void OnMouseOver(Point pt, WidgetID widget); + 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(); + + virtual ~SelectorWidget() {}; + +private: + Window *parent_window; ///< The parent window pointer + + int row_height; ///< The height of 1 row in the WID_SELECTOR_MATRIX widget + + Scrollbar *vscroll; ///< The vertical scrollbar + + /** A QueryString modifiable by the WID_SELECTOR_EDITBOX widget */ + QueryString editbox{MAX_LENGTH_COMPANY_NAME_CHARS * MAX_CHAR_LENGTH, MAX_LENGTH_COMPANY_NAME_CHARS}; + + /** + * Draws "section" (line) of the scrollable list. + * Called by #DrawWidget + * @param id The "id" of the item to be drawn, can mean different things depending on what the selector widget is displaying. + * 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 CargoSelectorWidget::DrawSection() + * @see CompanySelectorWidget::DrawSection() + * @see SelectorWidget::PopulateList() + * @see SelectorWidget::DrawWidget() + */ + virtual void DrawSection(uint id, const Rect &r) = 0; + + /** + * Repopulates the list that the widget uses to keep track of different selectable items + * @see CargoSelectorWidget::PopulateList() + * @see CompanySelectorWidget::PopulateList() + */ + virtual void PopulateList() = 0; + + /** + * Called when the widget selection or visibility of some item is changed. + * Used to update things that depend on the selector widget's data. + */ + virtual void OnChanged() = 0; +}; + +struct CargoSelectorWidget : SelectorWidget { + virtual void DrawSection(uint id, const Rect &r) override; + virtual void PopulateList() override; +}; + +struct CompanySelectorWidget : SelectorWidget { + virtual void DrawSection(uint id, const Rect &r) override; + virtual void PopulateList() override; +}; + +#endif /* SELECTOR_GUI_H */ diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index a69f7e7b16..e21af2869b 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -45,6 +45,7 @@ add_files( screenshot_widget.h script_widget.h settings_widget.h + selector_widget.h sign_widget.h smallmap_widget.h station_widget.h diff --git a/src/widgets/selector_widget.h b/src/widgets/selector_widget.h new file mode 100644 index 0000000000..f6e403c0df --- /dev/null +++ b/src/widgets/selector_widget.h @@ -0,0 +1,25 @@ +/* + * 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 Types related to the selector widget widgets. */ + +#ifndef WIDGETS_SELECTOR_WIDGET_H +#define WIDGETS_SELECTOR_WIDGET_H + +/** Widgets of the #SelectorWidget class. */ +enum SelectorClassWindowWidgets : WidgetID { + WID_SELECTOR_START = (1 << 16) + 1000, ///< Dummy to ensure widgets don't overlap. + + WID_SELECTOR_MATRIX, ///< Item list. + WID_SELECTOR_SCROLLBAR, ///< Vertical scrollbar. + WID_SELECTOR_EDITBOX, ///< Editbox filter. + WID_SELECTOR_HIDEALL, ///< Hide all button. + WID_SELECTOR_SHOWALL, ///< Show all button. + WID_SELECTOR_RESIZE, ///< Resize handle +}; + +#endif /* WIDGETS_SELECTOR_WIDGET_H */ From 0516772aaff270f103c213f34f98473b7e2d335d Mon Sep 17 00:00:00 2001 From: Andrii Dokhniak Date: Tue, 9 Jul 2024 22:40:30 +0200 Subject: [PATCH 2/2] Feature: Make graphs use the universal selector widget. --- src/company_cmd.cpp | 6 +- src/graph_gui.cpp | 656 ++++++++++++++++++------------------- src/widgets/graph_widget.h | 13 - src/window_type.h | 6 +- 4 files changed, 336 insertions(+), 345 deletions(-) diff --git a/src/company_cmd.cpp b/src/company_cmd.cpp index 74ad2893f4..891d3e036c 100644 --- a/src/company_cmd.cpp +++ b/src/company_cmd.cpp @@ -41,6 +41,7 @@ #include "timer/timer_game_tick.h" #include "widgets/statusbar_widget.h" +#include "window_type.h" #include "table/strings.h" @@ -90,7 +91,7 @@ Company::~Company() */ void Company::PostDestructor(size_t index) { - InvalidateWindowData(WC_GRAPH_LEGEND, 0, (int)index); + InvalidateWindowData(WC_GRAPH_LEGEND, WN_GRAPH_SELECTOR_WINDOW_COMPANY, (int)index); InvalidateWindowData(WC_PERFORMANCE_DETAIL, 0, (int)index); InvalidateWindowData(WC_COMPANY_LEAGUE, 0, 0); InvalidateWindowData(WC_LINKGRAPH_LEGEND, 0); @@ -622,7 +623,7 @@ Company *DoStartupNewCompany(bool is_ai, CompanyID company = INVALID_COMPANY) GeneratePresidentName(c); - SetWindowDirty(WC_GRAPH_LEGEND, 0); + InvalidateWindowData(WC_GRAPH_LEGEND, WN_GRAPH_SELECTOR_WINDOW_COMPANY); InvalidateWindowData(WC_CLIENT_LIST, 0); InvalidateWindowData(WC_LINKGRAPH_LEGEND, 0); BuildOwnerLegend(); @@ -1131,6 +1132,7 @@ CommandCost CmdSetCompanyColour(DoCommandFlag flags, LiveryScheme scheme, bool p InvalidateWindowData(WC_PERFORMANCE_HISTORY, 0); InvalidateWindowData(WC_COMPANY_VALUE, 0); InvalidateWindowData(WC_LINKGRAPH_LEGEND, 0); + InvalidateWindowData(WC_GRAPH_LEGEND, WN_GRAPH_SELECTOR_WINDOW_COMPANY); /* The smallmap owner view also stores the company colours. */ BuildOwnerLegend(); InvalidateWindowData(WC_SMALLMAP, 0, 1); diff --git a/src/graph_gui.cpp b/src/graph_gui.cpp index 9424144c4c..4f0b658e94 100644 --- a/src/graph_gui.cpp +++ b/src/graph_gui.cpp @@ -9,21 +9,16 @@ #include "stdafx.h" #include "graph_gui.h" -#include "window_gui.h" #include "company_base.h" #include "company_gui.h" #include "economy_func.h" -#include "cargotype.h" #include "strings_func.h" +#include "widget_type.h" #include "window_func.h" -#include "gfx_func.h" -#include "core/geometry_func.hpp" #include "currency.h" #include "timer/timer.h" #include "timer/timer_window.h" -#include "timer/timer_game_tick.h" -#include "timer/timer_game_calendar.h" -#include "timer/timer_game_economy.h" +#include "window_type.h" #include "zoom_func.h" #include "widgets/graph_widget.h" @@ -31,11 +26,9 @@ #include "table/strings.h" #include "table/sprites.h" -#include "safeguards.h" +#include "selector_gui.h" -/* Bitmasks of company and cargo indices that shouldn't be drawn. */ -static CompanyMask _legend_excluded_companies; -static CargoTypes _legend_excluded_cargo; +#include "safeguards.h" /* Apparently these don't play well with enums. */ static const OverflowSafeInt64 INVALID_DATAPOINT(INT64_MAX); // Value used for a datapoint that shouldn't be drawn. @@ -44,121 +37,156 @@ static const uint INVALID_DATAPOINT_POS = UINT_MAX; // Used to determine if the constexpr double INT64_MAX_IN_DOUBLE = static_cast(INT64_MAX - 512); ///< The biggest double that when cast to int64_t still fits in a int64_t. static_assert(static_cast(INT64_MAX_IN_DOUBLE) < INT64_MAX); -/****************/ -/* GRAPH LEGEND */ -/****************/ +/** Contains the interval of a graph's data. */ +struct ValuesInterval { + OverflowSafeInt64 highest; ///< Highest value of this interval. Must be zero or greater. + OverflowSafeInt64 lowest; ///< Lowest value of this interval. Must be zero or less. +}; -struct GraphLegendWindow : Window { - GraphLegendWindow(WindowDesc &desc, WindowNumber window_number) : Window(desc) +/** The cargo selector widget for the #GraphLegendWindow and graphs. */ +struct CargoGraphSelectorWidget : CargoSelectorWidget { + void OnChanged() override { - this->InitNested(window_number); - - for (CompanyID c = COMPANY_FIRST; c < MAX_COMPANIES; c++) { - if (!HasBit(_legend_excluded_companies, c)) this->LowerWidget(WID_GL_FIRST_COMPANY + c); - - this->OnInvalidateData(c); - } + InvalidateWindowData(WC_PAYMENT_RATES, 0); } +}; - void DrawWidget(const Rect &r, WidgetID widget) const override +/** The company selector widget for the #GraphLegendWindow and graphs. */ +struct CompanyGraphSelectorWidget : CompanySelectorWidget { + void OnChanged() override { - if (!IsInsideMM(widget, WID_GL_FIRST_COMPANY, WID_GL_FIRST_COMPANY + MAX_COMPANIES)) return; - - CompanyID cid = (CompanyID)(widget - WID_GL_FIRST_COMPANY); - - if (!Company::IsValidID(cid)) return; - - bool rtl = _current_text_dir == TD_RTL; - - const Rect ir = r.Shrink(WidgetDimensions::scaled.framerect); - Dimension d = GetSpriteSize(SPR_COMPANY_ICON); - DrawCompanyIcon(cid, rtl ? ir.right - d.width : ir.left, CenterBounds(ir.top, ir.bottom, d.height)); - - const Rect tr = ir.Indent(d.width + WidgetDimensions::scaled.hsep_normal, rtl); - SetDParam(0, cid); - SetDParam(1, cid); - DrawString(tr.left, tr.right, CenterBounds(tr.top, tr.bottom, GetCharacterHeight(FS_NORMAL)), STR_COMPANY_NAME_COMPANY_NUM, HasBit(_legend_excluded_companies, cid) ? TC_BLACK : TC_WHITE); - } - - void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override - { - if (!IsInsideMM(widget, WID_GL_FIRST_COMPANY, WID_GL_FIRST_COMPANY + MAX_COMPANIES)) return; - - ToggleBit(_legend_excluded_companies, widget - WID_GL_FIRST_COMPANY); - this->ToggleWidgetLoweredState(widget); - this->SetDirty(); InvalidateWindowData(WC_INCOME_GRAPH, 0); InvalidateWindowData(WC_OPERATING_PROFIT, 0); InvalidateWindowData(WC_DELIVERED_CARGO, 0); InvalidateWindowData(WC_PERFORMANCE_HISTORY, 0); InvalidateWindowData(WC_COMPANY_VALUE, 0); } +}; - /** - * Some data on this window has become invalid. - * @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. - */ - void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override +static CompanyGraphSelectorWidget _company_selector; ///< The company selector widget, used inside #GraphLegendWindow +static CargoGraphSelectorWidget _cargo_selector; ///< The cargo selector widget, used inside #GraphLegendWindow + +/****************/ +/* GRAPH LEGEND */ +/****************/ + +struct GraphLegendWindow : Window { + SelectorWidget *selector; ///< The selector for the current window + + GraphLegendWindow(WindowDesc &desc, WindowNumber window_number) : Window(desc) { - if (!gui_scope) return; - if (Company::IsValidID(data)) return; + this->CreateNestedTree(); + if (window_number == WN_GRAPH_SELECTOR_WINDOW_COMPANY) { + this->selector = &_company_selector; + } else { + this->selector = &_cargo_selector; + } + this->selector->Init(this); + this->FinishInitNested(window_number); + } - SetBit(_legend_excluded_companies, data); - this->RaiseWidget(data + WID_GL_FIRST_COMPANY); + void DrawWidget(const Rect &r, WidgetID widget) const override + { + this->selector->DrawWidget(r, widget); + } + + void OnClick(Point pt, WidgetID widget, int click_count) override + { + this->selector->OnClick(pt, widget, click_count); + this->SetDirty(); + } + + void OnInvalidateData(int data = 0, bool gui_scope = true) override + { + this->selector->OnInvalidateData(data, gui_scope); + this->SetDirty(); + + /* Check if this selector window is no longer needed and close it. */ + if (this->window_number == WN_GRAPH_SELECTOR_WINDOW_COMPANY) { + /* Close if no company graph windows exist. */ + if (FindWindowByClass(WC_INCOME_GRAPH) != nullptr) return; + if (FindWindowByClass(WC_OPERATING_PROFIT) != nullptr) return; + if (FindWindowByClass(WC_DELIVERED_CARGO) != nullptr) return; + if (FindWindowByClass(WC_PERFORMANCE_HISTORY) != nullptr) return; + if (FindWindowByClass(WC_COMPANY_VALUE) != nullptr) return; + } else { + /* Close if no cargo graph windows exist. */ + if (FindWindowByClass(WC_PAYMENT_RATES) != nullptr) return; + } + this->Close(); + } + + void OnResize() override + { + this->selector->OnResize(); + } + + void UpdateWidgetSize(WidgetID widget, Dimension &size, const Dimension &padding, Dimension &fill, Dimension &resize) override + { + this->selector->UpdateWidgetSize(widget, size, padding, fill, resize); + } + + void OnEditboxChanged(WidgetID wid) override + { + this->selector->OnEditboxChanged(wid); + } + + void OnMouseOver(Point pt, WidgetID widget) override + { + this->selector->OnMouseOver(pt, widget); } }; -/** - * Construct a vertical list of buttons, one for each company. - * @return Panel with company buttons. - */ -static std::unique_ptr MakeNWidgetCompanyLines() -{ - auto vert = std::make_unique(NC_EQUALSIZE); - vert->SetPadding(2, 2, 2, 2); - uint sprite_height = GetSpriteSize(SPR_COMPANY_ICON, nullptr, ZOOM_LVL_NORMAL).height; - - for (WidgetID widnum = WID_GL_FIRST_COMPANY; widnum <= WID_GL_LAST_COMPANY; widnum++) { - auto panel = std::make_unique(WWT_PANEL, COLOUR_BROWN, widnum); - panel->SetMinimalSize(246, sprite_height + WidgetDimensions::unscaled.framerect.Vertical()); - panel->SetMinimalTextLines(1, WidgetDimensions::unscaled.framerect.Vertical(), FS_NORMAL); - panel->SetFill(1, 1); - panel->SetDataTip(0x0, STR_GRAPH_KEY_COMPANY_SELECTION_TOOLTIP); - vert->Add(std::move(panel)); - } - return vert; -} - -static constexpr NWidgetPart _nested_graph_legend_widgets[] = { +static constexpr NWidgetPart _nested_graph_company_legend_widgets[] = { NWidget(NWID_HORIZONTAL), NWidget(WWT_CLOSEBOX, COLOUR_BROWN), NWidget(WWT_CAPTION, COLOUR_BROWN), SetDataTip(STR_GRAPH_KEY_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), NWidget(WWT_SHADEBOX, COLOUR_BROWN), NWidget(WWT_STICKYBOX, COLOUR_BROWN), EndContainer(), - NWidget(WWT_PANEL, COLOUR_BROWN, WID_GL_BACKGROUND), - NWidgetFunction(MakeNWidgetCompanyLines), - EndContainer(), + NWidgetFunction(SelectorWidget::MakeSelectorWidgetUI), }; -static WindowDesc _graph_legend_desc( - WDP_AUTO, "graph_legend", 0, 0, +static constexpr NWidgetPart _nested_graph_cargo_legend_widgets[] = { + NWidget(NWID_HORIZONTAL), + NWidget(WWT_CLOSEBOX, COLOUR_BROWN), + NWidget(WWT_CAPTION, COLOUR_BROWN), SetDataTip(STR_GRAPH_CARGO_KEY_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), + NWidget(WWT_SHADEBOX, COLOUR_BROWN), + NWidget(WWT_STICKYBOX, COLOUR_BROWN), + EndContainer(), + NWidgetFunction(SelectorWidget::MakeSelectorWidgetUI), +}; + +static WindowDesc _graph_legend_desc_comp( + WDP_AUTO, "graph_company_legend", 0, 0, WC_GRAPH_LEGEND, WC_NONE, 0, - _nested_graph_legend_widgets + _nested_graph_company_legend_widgets ); -static void ShowGraphLegend() +static WindowDesc _graph_legend_desc_cargo( + WDP_AUTO, "graph_cargo_legend", 0, 0, + WC_GRAPH_LEGEND, WC_NONE, + 0, + _nested_graph_cargo_legend_widgets +); + +/** + * Show a GraphLegendWindow with #CompanyGraphSelectorWidget inside. + */ +static void ShowGraphCompanyLegend() { - AllocateWindowDescFront(_graph_legend_desc, 0); + AllocateWindowDescFront(_graph_legend_desc_comp, WN_GRAPH_SELECTOR_WINDOW_COMPANY); +} + +/** + * Show a GraphLegendWindow with #CargoGraphSelectorWidget inside. + */ +static void ShowGraphCargoLegend() +{ + AllocateWindowDescFront(_graph_legend_desc_cargo, WN_GRAPH_SELECTOR_WINDOW_CARGO); } -/** Contains the interval of a graph's data. */ -struct ValuesInterval { - OverflowSafeInt64 highest; ///< Highest value of this interval. Must be zero or greater. - OverflowSafeInt64 lowest; ///< Lowest value of this interval. Must be zero or less. -}; /******************/ /* BASE OF GRAPHS */ @@ -182,7 +210,6 @@ protected: static const int MIN_GRAPH_NUM_LINES_Y = 9; ///< Minimal number of horizontal lines to draw. static const int MIN_GRID_PIXEL_SIZE = 20; ///< Minimum distance between graph lines. - uint64_t excluded_data; ///< bitmask of the datasets that shouldn't be displayed. uint8_t num_dataset; uint8_t num_on_x_axis; uint8_t num_vert_lines; @@ -202,6 +229,9 @@ protected: uint8_t colours[GRAPH_MAX_DATASETS]; OverflowSafeInt64 cost[GRAPH_MAX_DATASETS][GRAPH_NUM_MONTHS]; ///< Stored costs for the last #GRAPH_NUM_MONTHS months + mutable std::optional click; ///< The coordinate of the previous click + mutable SelectorWidget *selector; ///< The selector widget that can hide/show data, or select a line + /** * Get the interval that contains the graph's data. Excluded data is ignored to show smaller values in * better detail when disabling higher ones. @@ -217,7 +247,7 @@ protected: current_interval.lowest = INT64_MAX; for (int i = 0; i < this->num_dataset; i++) { - if (HasBit(this->excluded_data, i)) continue; + if (!this->selector->shown[i]) continue; for (int j = 0; j < this->num_on_x_axis; j++) { OverflowSafeInt64 datapoint = this->cost[i][j]; @@ -298,9 +328,99 @@ protected: return max_width; } + /** + * Draw one graph plot and all its data points. + * It also determines the minimal distance between the mouse click (this->click), and the current data point + * @param id The ID of the plot about to be drawn. + * @param r Bounds of the graph. + * @param x_sep Separation between data points on the x axis. + * @param x_axis_offset Distance between the top of the graph and the x axis. + * @param interval_size Absolute difference between the highest and the lowest data point. + * @param draw_selected Controls the drawing of the highlighted selection. + * @return The minimal distance between all the data points in the current plot and the click point. + */ + int DrawLineAndDots(int id, Rect r, int x_sep, int x_axis_offset, int interval_size, int draw_selected) const + { + int x, y; + uint linewidth = _settings_client.gui.graph_line_thickness; + uint pointoffs1 = (linewidth + 1) / 2; + uint pointoffs2 = linewidth + 1 - pointoffs1; + uint min_dist = UINT_MAX; + + /* Ignore the hidden graph lines. */ + if (!this->selector->shown[id]) return min_dist; + + /* Centre the dot between the grid lines. */ + x = r.left + (x_sep / 2); + + uint8_t colour = this->colours[id]; + uint prev_x = INVALID_DATAPOINT_POS; + uint prev_y = INVALID_DATAPOINT_POS; + + for (int j = 0; j < this->num_on_x_axis; j++) { + OverflowSafeInt64 datapoint = this->cost[id][j]; + + if (datapoint != INVALID_DATAPOINT) { + /* + * Check whether we need to reduce the 'accuracy' of the + * data point value and the highest value to split overflows. + * And when 'drawing' 'one million' or 'one million and one' + * there is no significant difference, so the least + * significant bits can just be removed. + * + * If there are more bits needed than would fit in a 32 bits + * integer, so at about 31 bits because of the sign bit, the + * least significant bits are removed. + */ + int mult_range = FindLastBit(x_axis_offset) + FindLastBit(abs(datapoint)); + int reduce_range = std::max(mult_range - 31, 0); + + /* Handle negative values differently (don't shift sign) */ + if (datapoint < 0) { + datapoint = -(abs(datapoint) >> reduce_range); + } else { + datapoint >>= reduce_range; + } + y = r.top + x_axis_offset - ((r.bottom - r.top) * datapoint) / (interval_size >> reduce_range); + + if (click.has_value()) { + const Point click_pt = click.value(); + + /* Calculate the (Manhattan) distance between the click and the current data point */ + const uint dist = abs(click_pt.x - x) + abs(click_pt.y - y); + if (dist < min_dist) { + min_dist = dist; + } + } + if (draw_selected == 1) { + /* Draw the thick dark blue outline */ + GfxFillRect(x - pointoffs1 - 3, y - pointoffs1 - 3, x + pointoffs2 + 3, y + pointoffs2 + 3, COLOUR_DARK_BLUE); + if (prev_x != INVALID_DATAPOINT_POS) GfxDrawLine(prev_x, prev_y, x, y, COLOUR_DARK_BLUE, linewidth + 3); + } else if (draw_selected == 2) { + /* Draw the thicker then normal graph */ + GfxFillRect(x - pointoffs1 - 2, y - pointoffs1 - 2, x + pointoffs2 + 2, y + pointoffs2 + 2, colour); + if (prev_x != INVALID_DATAPOINT_POS) GfxDrawLine(prev_x, prev_y, x, y, colour, linewidth + 1); + } else { + /* Draw the normal graph */ + GfxFillRect(x - pointoffs1, y - pointoffs1, x + pointoffs2, y + pointoffs2, colour); + if (prev_x != INVALID_DATAPOINT_POS) GfxDrawLine(prev_x, prev_y, x, y, colour, linewidth); + } + prev_x = x; + prev_y = y; + } else { + prev_x = INVALID_DATAPOINT_POS; + prev_y = INVALID_DATAPOINT_POS; + } + + x += x_sep; + } + return min_dist; + }; + /** * Actually draw the graph. - * @param r the rectangle of the data field of the graph + * Right after a click 1 draw call is "sacrificed" to determine the new selected id (plot) + * @param r The rectangle of the data field of the graph. */ void DrawGraph(Rect r) const { @@ -434,61 +554,45 @@ protected: } } + uint min_dist = UINT_MAX; + uint min_id = 0; ///< ID of the closest point to the previous click. /* draw lines and dots */ - uint linewidth = _settings_client.gui.graph_line_thickness; - uint pointoffs1 = (linewidth + 1) / 2; - uint pointoffs2 = linewidth + 1 - pointoffs1; - for (int i = 0; i < this->num_dataset; i++) { - if (!HasBit(this->excluded_data, i)) { - /* Centre the dot between the grid lines. */ - x = r.left + (x_sep / 2); - - uint8_t colour = this->colours[i]; - uint prev_x = INVALID_DATAPOINT_POS; - uint prev_y = INVALID_DATAPOINT_POS; - - for (int j = 0; j < this->num_on_x_axis; j++) { - OverflowSafeInt64 datapoint = this->cost[i][j]; - - if (datapoint != INVALID_DATAPOINT) { - /* - * Check whether we need to reduce the 'accuracy' of the - * datapoint value and the highest value to split overflows. - * And when 'drawing' 'one million' or 'one million and one' - * there is no significant difference, so the least - * significant bits can just be removed. - * - * If there are more bits needed than would fit in a 32 bits - * integer, so at about 31 bits because of the sign bit, the - * least significant bits are removed. - */ - int mult_range = FindLastBit(x_axis_offset) + FindLastBit(abs(datapoint)); - int reduce_range = std::max(mult_range - 31, 0); - - /* Handle negative values differently (don't shift sign) */ - if (datapoint < 0) { - datapoint = -(abs(datapoint) >> reduce_range); - } else { - datapoint >>= reduce_range; - } - y = r.top + x_axis_offset - ((r.bottom - r.top) * datapoint) / (interval_size >> reduce_range); - - /* Draw the point. */ - GfxFillRect(x - pointoffs1, y - pointoffs1, x + pointoffs2, y + pointoffs2, colour); - - /* Draw the line connected to the previous point. */ - if (prev_x != INVALID_DATAPOINT_POS) GfxDrawLine(prev_x, prev_y, x, y, colour, linewidth); - - prev_x = x; - prev_y = y; - } else { - prev_x = INVALID_DATAPOINT_POS; - prev_y = INVALID_DATAPOINT_POS; - } - - x += x_sep; - } + for (const int id : this->selector->list) { + /* Skip the selected line, because it will be drawn separately later, + * and when determining the new selection, we don't want to select the already selected plot */ + if (this->selector->selected_id.value_or(-1) == id) { + continue; } + uint res = DrawLineAndDots(id, r, x_sep, x_axis_offset, interval_size, 0); + /* Determine the line with the closest data point to the click (if exists) */ + if (res < min_dist) { + min_dist = res; + min_id = id; + } + } + this->click = {}; + + /* If the distance between the closest data point and the click is small enough, change the selection */ + if (min_dist < 20) { + this->selector->selected_id = min_id; + /* One draw call is sacrificed right after a click, if the selection is determined to change */ + InvalidateWindowData(WC_GRAPH_LEGEND, WN_GRAPH_SELECTOR_WINDOW_COMPANY); + InvalidateWindowData(WC_GRAPH_LEGEND, WN_GRAPH_SELECTOR_WINDOW_CARGO); + + InvalidateWindowData(WC_PAYMENT_RATES, 0); + InvalidateWindowData(WC_INCOME_GRAPH, 0); + InvalidateWindowData(WC_OPERATING_PROFIT, 0); + InvalidateWindowData(WC_DELIVERED_CARGO, 0); + InvalidateWindowData(WC_PERFORMANCE_HISTORY, 0); + InvalidateWindowData(WC_COMPANY_VALUE, 0); + return; + } + + /* Draw the highlighted selected line over all other lines */ + if (this->selector->selected_id.has_value()) { + const int selected = this->selector->selected_id.value(); + DrawLineAndDots(selected, r, x_sep, x_axis_offset, interval_size, 1); + DrawLineAndDots(selected, r, x_sep, x_axis_offset, interval_size, 2); } } @@ -497,11 +601,19 @@ protected: Window(desc), format_str_y_axis(format_str_y_axis) { - SetWindowDirty(WC_GRAPH_LEGEND, 0); this->num_vert_lines = 24; } - void InitializeWindow(WindowNumber number) + ~BaseGraphWindow() { + InvalidateWindowData(WC_GRAPH_LEGEND, 0); + InvalidateWindowData(WC_GRAPH_LEGEND, 1); + } + + /** + * Initialize a graph window that visualizes some statistics about companies + * @param number Window number of the current graph window. + */ + void InitializeCompanyGraphWindow(WindowNumber number) { /* Initialise the dataset */ this->UpdateStatistics(true); @@ -565,10 +677,12 @@ public: return INVALID_DATAPOINT; } - void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override + void OnClick(Point pt, WidgetID widget, [[maybe_unused]] int click_count) override { /* Clicked on legend? */ - if (widget == WID_GRAPH_KEY_BUTTON) ShowGraphLegend(); + if (widget == WID_GRAPH_KEY_BUTTON) ShowGraphCompanyLegend(); + this->click = pt; + this->SetDirty(); } void OnGameTick() override @@ -581,8 +695,9 @@ public: * @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. */ - void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override + void OnInvalidateData([[maybe_unused]] int data = 0, bool gui_scope = true) override { + this->SetDirty(); if (!gui_scope) return; this->UpdateStatistics(true); } @@ -593,13 +708,6 @@ public: */ void UpdateStatistics(bool initialize) { - CompanyMask excluded_companies = _legend_excluded_companies; - - /* Exclude the companies which aren't valid */ - for (CompanyID c = COMPANY_FIRST; c < MAX_COMPANIES; c++) { - if (!Company::IsValidID(c)) SetBit(excluded_companies, c); - } - uint8_t nums = 0; for (const Company *c : Company::Iterate()) { nums = std::min(this->num_vert_lines, std::max(nums, c->num_valid_stat_ent)); @@ -612,13 +720,11 @@ public: mo += 12; } - if (!initialize && this->excluded_data == excluded_companies && this->num_on_x_axis == nums && - this->year == yr && this->month == mo) { + if (!initialize && this->num_on_x_axis == nums && this->year == yr && this->month == mo) { /* There's no reason to get new stats */ return; } - this->excluded_data = excluded_companies; this->num_on_x_axis = nums; this->year = yr; this->month = mo; @@ -655,13 +761,16 @@ struct OperatingProfitGraphWindow : BaseGraphWindow { OperatingProfitGraphWindow(WindowDesc &desc, WindowNumber window_number) : BaseGraphWindow(desc, STR_JUST_CURRENCY_SHORT) { + this->selector = &_company_selector; + _company_selector.RebuildList(); + this->num_on_x_axis = GRAPH_NUM_MONTHS; this->num_vert_lines = GRAPH_NUM_MONTHS; this->x_values_start = ECONOMY_QUARTER_MINUTES; this->x_values_increment = ECONOMY_QUARTER_MINUTES; this->draw_dates = !TimerGameEconomy::UsingWallclockUnits(); - this->InitializeWindow(window_number); + this->InitializeCompanyGraphWindow(window_number); } OverflowSafeInt64 GetGraphData(const Company *c, int j) override @@ -714,13 +823,16 @@ struct IncomeGraphWindow : BaseGraphWindow { IncomeGraphWindow(WindowDesc &desc, WindowNumber window_number) : BaseGraphWindow(desc, STR_JUST_CURRENCY_SHORT) { + this->selector = &_company_selector; + _company_selector.RebuildList(); + this->num_on_x_axis = GRAPH_NUM_MONTHS; this->num_vert_lines = GRAPH_NUM_MONTHS; this->x_values_start = ECONOMY_QUARTER_MINUTES; this->x_values_increment = ECONOMY_QUARTER_MINUTES; this->draw_dates = !TimerGameEconomy::UsingWallclockUnits(); - this->InitializeWindow(window_number); + this->InitializeCompanyGraphWindow(window_number); } OverflowSafeInt64 GetGraphData(const Company *c, int j) override @@ -771,13 +883,16 @@ struct DeliveredCargoGraphWindow : BaseGraphWindow { DeliveredCargoGraphWindow(WindowDesc &desc, WindowNumber window_number) : BaseGraphWindow(desc, STR_JUST_COMMA) { + this->selector = &_company_selector; + _company_selector.RebuildList(); + this->num_on_x_axis = GRAPH_NUM_MONTHS; this->num_vert_lines = GRAPH_NUM_MONTHS; this->x_values_start = ECONOMY_QUARTER_MINUTES; this->x_values_increment = ECONOMY_QUARTER_MINUTES; this->draw_dates = !TimerGameEconomy::UsingWallclockUnits(); - this->InitializeWindow(window_number); + this->InitializeCompanyGraphWindow(window_number); } OverflowSafeInt64 GetGraphData(const Company *c, int j) override @@ -828,13 +943,16 @@ struct PerformanceHistoryGraphWindow : BaseGraphWindow { PerformanceHistoryGraphWindow(WindowDesc &desc, WindowNumber window_number) : BaseGraphWindow(desc, STR_JUST_COMMA) { + this->selector = &_company_selector; + _company_selector.RebuildList(); + this->num_on_x_axis = GRAPH_NUM_MONTHS; this->num_vert_lines = GRAPH_NUM_MONTHS; this->x_values_start = ECONOMY_QUARTER_MINUTES; this->x_values_increment = ECONOMY_QUARTER_MINUTES; this->draw_dates = !TimerGameEconomy::UsingWallclockUnits(); - this->InitializeWindow(window_number); + this->InitializeCompanyGraphWindow(window_number); } OverflowSafeInt64 GetGraphData(const Company *c, int j) override @@ -842,8 +960,9 @@ struct PerformanceHistoryGraphWindow : BaseGraphWindow { return c->old_economy[j].performance_history; } - void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override + void OnClick(Point pt, WidgetID widget, int click_count) override { + if (widget == WID_GRAPH_KEY_BUTTON) ShowGraphCompanyLegend(); if (widget == WID_PHG_DETAILED_PERFORMANCE) ShowPerformanceRatingDetail(); this->BaseGraphWindow::OnClick(pt, widget, click_count); } @@ -892,13 +1011,16 @@ struct CompanyValueGraphWindow : BaseGraphWindow { CompanyValueGraphWindow(WindowDesc &desc, WindowNumber window_number) : BaseGraphWindow(desc, STR_JUST_CURRENCY_SHORT) { + this->selector = &_company_selector; + _company_selector.RebuildList(); + this->num_on_x_axis = GRAPH_NUM_MONTHS; this->num_vert_lines = GRAPH_NUM_MONTHS; this->x_values_start = ECONOMY_QUARTER_MINUTES; this->x_values_increment = ECONOMY_QUARTER_MINUTES; this->draw_dates = !TimerGameEconomy::UsingWallclockUnits(); - this->InitializeWindow(window_number); + this->InitializeCompanyGraphWindow(window_number); } OverflowSafeInt64 GetGraphData(const Company *c, int j) override @@ -946,13 +1068,12 @@ void ShowCompanyValueGraph() /*****************/ struct PaymentRatesGraphWindow : BaseGraphWindow { - uint line_height; ///< Pixel height of each cargo type row. - Scrollbar *vscroll; ///< Cargo list scrollbar. - uint legend_width; ///< Width of legend 'blob'. - PaymentRatesGraphWindow(WindowDesc &desc, WindowNumber window_number) : BaseGraphWindow(desc, STR_JUST_CURRENCY_SHORT) { + this->selector = &_cargo_selector; + _cargo_selector.RebuildList(); + this->num_on_x_axis = 20; this->num_vert_lines = 20; this->draw_dates = false; @@ -961,8 +1082,6 @@ struct PaymentRatesGraphWindow : BaseGraphWindow { this->x_values_increment = (TimerGameEconomy::UsingWallclockUnits() ? PAYMENT_GRAPH_X_STEP_SECONDS : PAYMENT_GRAPH_X_STEP_DAYS); this->CreateNestedTree(); - this->vscroll = this->GetScrollbar(WID_CPR_MATRIX_SCROLLBAR); - this->vscroll->SetCount(_sorted_standard_cargo_specs.size()); auto *wid = this->GetWidget(WID_GRAPH_FOOTER); wid->SetDataTip(TimerGameEconomy::UsingWallclockUnits() ? STR_GRAPH_CARGO_PAYMENT_RATES_SECONDS: STR_GRAPH_CARGO_PAYMENT_RATES_DAYS, STR_NULL); @@ -971,121 +1090,8 @@ struct PaymentRatesGraphWindow : BaseGraphWindow { this->UpdatePaymentRates(); this->FinishInitNested(window_number); - } - - void OnInit() override - { - /* Width of the legend blob. */ - this->legend_width = GetCharacterHeight(FS_SMALL) * 9 / 6; - } - - void UpdateExcludedData() - { - this->excluded_data = 0; - - int i = 0; - for (const CargoSpec *cs : _sorted_standard_cargo_specs) { - if (HasBit(_legend_excluded_cargo, cs->Index())) SetBit(this->excluded_data, i); - i++; - } - } - - void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override - { - if (widget != WID_CPR_MATRIX) { - BaseGraphWindow::UpdateWidgetSize(widget, size, padding, fill, resize); - return; - } - - size.height = GetCharacterHeight(FS_SMALL) + WidgetDimensions::scaled.framerect.Vertical(); - - for (const CargoSpec *cs : _sorted_standard_cargo_specs) { - SetDParam(0, cs->name); - Dimension d = GetStringBoundingBox(STR_GRAPH_CARGO_PAYMENT_CARGO); - d.width += this->legend_width + WidgetDimensions::scaled.hsep_normal; // colour field - d.width += WidgetDimensions::scaled.framerect.Horizontal(); - d.height += WidgetDimensions::scaled.framerect.Vertical(); - size = maxdim(d, size); - } - - this->line_height = size.height; - size.height = this->line_height * 11; /* Default number of cargo types in most climates. */ - resize.width = 0; - resize.height = this->line_height; - } - - void DrawWidget(const Rect &r, WidgetID widget) const override - { - if (widget != WID_CPR_MATRIX) { - BaseGraphWindow::DrawWidget(r, widget); - return; - } - - bool rtl = _current_text_dir == TD_RTL; - - auto [first, last] = this->vscroll->GetVisibleRangeIterators(_sorted_standard_cargo_specs); - - Rect line = r.WithHeight(this->line_height); - for (auto it = first; it != last; ++it) { - const CargoSpec *cs = *it; - - bool lowered = !HasBit(_legend_excluded_cargo, cs->Index()); - - /* Redraw frame if lowered */ - if (lowered) DrawFrameRect(line, COLOUR_BROWN, FR_LOWERED); - - const Rect text = line.Shrink(WidgetDimensions::scaled.framerect); - - /* Cargo-colour box with outline */ - const Rect cargo = text.WithWidth(this->legend_width, rtl); - GfxFillRect(cargo, PC_BLACK); - GfxFillRect(cargo.Shrink(WidgetDimensions::scaled.bevel), cs->legend_colour); - - /* Cargo name */ - SetDParam(0, cs->name); - DrawString(text.Indent(this->legend_width + WidgetDimensions::scaled.hsep_normal, rtl), STR_GRAPH_CARGO_PAYMENT_CARGO); - - line = line.Translate(0, this->line_height); - } - } - - void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override - { - switch (widget) { - case WID_CPR_ENABLE_CARGOES: - /* Remove all cargoes from the excluded lists. */ - _legend_excluded_cargo = 0; - this->excluded_data = 0; - this->SetDirty(); - break; - - case WID_CPR_DISABLE_CARGOES: { - /* Add all cargoes to the excluded lists. */ - int i = 0; - for (const CargoSpec *cs : _sorted_standard_cargo_specs) { - SetBit(_legend_excluded_cargo, cs->Index()); - SetBit(this->excluded_data, i); - i++; - } - this->SetDirty(); - break; - } - - case WID_CPR_MATRIX: { - auto it = this->vscroll->GetScrolledItemFromWidget(_sorted_standard_cargo_specs, pt.y, this, WID_CPR_MATRIX); - if (it != _sorted_standard_cargo_specs.end()) { - ToggleBit(_legend_excluded_cargo, (*it)->Index()); - this->UpdateExcludedData(); - this->SetDirty(); - } - break; - } - } - } - - void OnResize() override - { - this->vscroll->SetCapacityFromWidget(this, WID_CPR_MATRIX); + this->InvalidateData(); + ShowGraphCargoLegend(); } void OnGameTick() override @@ -1098,10 +1104,11 @@ struct PaymentRatesGraphWindow : BaseGraphWindow { * @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. */ - void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override + void OnInvalidateData([[maybe_unused]] int data = 0,bool gui_scope = true) override { if (!gui_scope) return; this->UpdatePaymentRates(); + this->SetDirty(); } /** Update the payment rates on a regular interval. */ @@ -1114,17 +1121,21 @@ struct PaymentRatesGraphWindow : BaseGraphWindow { */ void UpdatePaymentRates() { - this->UpdateExcludedData(); - - int i = 0; - for (const CargoSpec *cs : _sorted_standard_cargo_specs) { - this->colours[i] = cs->legend_colour; + for (const int i : selector->list) { + const CargoSpec *cs = CargoSpec::Get(i); + this->colours[cs->Index()] = cs->legend_colour; for (uint j = 0; j != this->num_on_x_axis; j++) { - this->cost[i][j] = GetTransportedGoodsIncome(10, 20, j * 4 + 4, cs->Index()); + this->cost[cs->Index()][j] = GetTransportedGoodsIncome(10, 20, j * 4 + 4, cs->Index()); } - i++; } - this->num_dataset = i; + this->num_dataset = (uint8_t)selector->list.size(); + } + + void OnClick(Point pt, WidgetID widget, [[maybe_unused]] int click_count) override + { + if (widget == WID_GRAPH_KEY_BUTTON) ShowGraphCargoLegend(); + this->click = pt; + this->SetDirty(); } }; @@ -1132,36 +1143,25 @@ static constexpr NWidgetPart _nested_cargo_payment_rates_widgets[] = { NWidget(NWID_HORIZONTAL), NWidget(WWT_CLOSEBOX, COLOUR_BROWN), NWidget(WWT_CAPTION, COLOUR_BROWN), SetDataTip(STR_GRAPH_CARGO_PAYMENT_RATES_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), + NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_GRAPH_KEY_BUTTON), SetMinimalSize(50, 0), SetDataTip(STR_GRAPH_KEY_BUTTON, STR_GRAPH_KEY_TOOLTIP), NWidget(WWT_SHADEBOX, COLOUR_BROWN), NWidget(WWT_DEFSIZEBOX, COLOUR_BROWN), NWidget(WWT_STICKYBOX, COLOUR_BROWN), EndContainer(), - NWidget(WWT_PANEL, COLOUR_BROWN, WID_GRAPH_BACKGROUND), SetMinimalSize(568, 128), - NWidget(NWID_HORIZONTAL), - NWidget(NWID_SPACER), SetFill(1, 0), SetResize(1, 0), - NWidget(WWT_TEXT, COLOUR_BROWN, WID_GRAPH_HEADER), SetMinimalSize(0, 6), SetPadding(2, 0, 2, 0), SetDataTip(STR_GRAPH_CARGO_PAYMENT_RATES_TITLE, STR_NULL), - NWidget(NWID_SPACER), SetFill(1, 0), SetResize(1, 0), - EndContainer(), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_EMPTY, COLOUR_BROWN, WID_GRAPH_GRAPH), SetMinimalSize(495, 0), SetFill(1, 1), SetResize(1, 1), - NWidget(NWID_VERTICAL), - NWidget(NWID_SPACER), SetMinimalSize(0, 24), SetFill(0, 1), - NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_CPR_ENABLE_CARGOES), SetDataTip(STR_GRAPH_CARGO_ENABLE_ALL, STR_GRAPH_CARGO_TOOLTIP_ENABLE_ALL), SetFill(1, 0), - NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_CPR_DISABLE_CARGOES), SetDataTip(STR_GRAPH_CARGO_DISABLE_ALL, STR_GRAPH_CARGO_TOOLTIP_DISABLE_ALL), SetFill(1, 0), - NWidget(NWID_SPACER), SetMinimalSize(0, 4), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_MATRIX, COLOUR_BROWN, WID_CPR_MATRIX), SetFill(1, 0), SetResize(0, 2), SetMatrixDataTip(1, 0, STR_GRAPH_CARGO_PAYMENT_TOGGLE_CARGO), SetScrollbar(WID_CPR_MATRIX_SCROLLBAR), - NWidget(NWID_VSCROLLBAR, COLOUR_BROWN, WID_CPR_MATRIX_SCROLLBAR), - EndContainer(), - NWidget(NWID_SPACER), SetMinimalSize(0, 24), SetFill(0, 1), + NWidget(NWID_HORIZONTAL), + NWidget(WWT_PANEL, COLOUR_BROWN, WID_GRAPH_BACKGROUND), SetMinimalSize(568, 128), + NWidget(NWID_HORIZONTAL), + NWidget(NWID_SPACER), SetFill(1, 0), SetResize(1, 0), + NWidget(WWT_TEXT, COLOUR_BROWN, WID_GRAPH_HEADER), SetMinimalSize(0, 6), SetPadding(2, 0, 2, 0), SetDataTip(STR_GRAPH_CARGO_PAYMENT_RATES_TITLE, STR_NULL), + NWidget(NWID_SPACER), SetFill(1, 0), SetResize(1, 0), + EndContainer(), + NWidget(WWT_EMPTY, COLOUR_BROWN, WID_GRAPH_GRAPH), SetMinimalSize(495, 0), SetFill(1, 1), SetResize(1, 1), + NWidget(NWID_HORIZONTAL), + NWidget(NWID_SPACER), SetMinimalSize(12, 0), SetFill(1, 0), SetResize(1, 0), + NWidget(WWT_TEXT, COLOUR_BROWN, WID_GRAPH_FOOTER), SetMinimalSize(0, 6), SetPadding(2, 0, 2, 0), SetDataTip(STR_NULL, STR_NULL), + NWidget(NWID_SPACER), SetFill(1, 0), SetResize(1, 0), + NWidget(WWT_RESIZEBOX, COLOUR_BROWN, WID_GRAPH_RESIZE), SetDataTip(RWV_HIDE_BEVEL, STR_TOOLTIP_RESIZE), EndContainer(), - NWidget(NWID_SPACER), SetMinimalSize(5, 0), SetFill(0, 1), SetResize(0, 1), - EndContainer(), - NWidget(NWID_HORIZONTAL), - NWidget(NWID_SPACER), SetMinimalSize(12, 0), SetFill(1, 0), SetResize(1, 0), - NWidget(WWT_TEXT, COLOUR_BROWN, WID_GRAPH_FOOTER), SetMinimalSize(0, 6), SetPadding(2, 0, 2, 0), SetDataTip(STR_NULL, STR_NULL), - NWidget(NWID_SPACER), SetFill(1, 0), SetResize(1, 0), - NWidget(WWT_RESIZEBOX, COLOUR_BROWN, WID_GRAPH_RESIZE), SetDataTip(RWV_HIDE_BEVEL, STR_TOOLTIP_RESIZE), EndContainer(), EndContainer(), }; @@ -1376,7 +1376,7 @@ struct PerformanceRatingDetailWindow : Window { /** * Some data on this window has become invalid. - * @param data the company ID of the company that is going to be removed + * @param data The company ID of the company that is going to be removed. * @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. */ void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override @@ -1475,6 +1475,4 @@ void ShowPerformanceRatingDetail() void InitializeGraphGui() { - _legend_excluded_companies = 0; - _legend_excluded_cargo = 0; } diff --git a/src/widgets/graph_widget.h b/src/widgets/graph_widget.h index 0511446578..0aa8c2a5b4 100644 --- a/src/widgets/graph_widget.h +++ b/src/widgets/graph_widget.h @@ -13,14 +13,6 @@ #include "../economy_type.h" #include "../company_type.h" -/** Widgets of the #GraphLegendWindow class. */ -enum GraphLegendWidgets : WidgetID { - WID_GL_BACKGROUND, ///< Background of the window. - - WID_GL_FIRST_COMPANY, ///< First company in the legend. - WID_GL_LAST_COMPANY = WID_GL_FIRST_COMPANY + MAX_COMPANIES - 1, ///< Last company in the legend. -}; - /** Widgets of the #BaseGraphWindow class and derived classes. */ enum GraphWidgets : WidgetID { WID_GRAPH_KEY_BUTTON, ///< Key button. @@ -31,11 +23,6 @@ enum GraphWidgets : WidgetID { WID_GRAPH_FOOTER, ///< Footer. WID_PHG_DETAILED_PERFORMANCE, ///< Detailed performance. - - WID_CPR_ENABLE_CARGOES, ///< Enable cargoes button. - WID_CPR_DISABLE_CARGOES, ///< Disable cargoes button. - WID_CPR_MATRIX, ///< Cargo list. - WID_CPR_MATRIX_SCROLLBAR,///< Cargo list scrollbar. }; /** Widget of the #PerformanceRatingDetailWindow class. */ diff --git a/src/window_type.h b/src/window_type.h index 0896d5ff6f..7a7fa155b8 100644 --- a/src/window_type.h +++ b/src/window_type.h @@ -38,6 +38,9 @@ enum WindowNumberEnum { WN_NETWORK_STATUS_WINDOW_JOIN = 0, ///< Network join status. WN_NETWORK_STATUS_WINDOW_CONTENT_DOWNLOAD, ///< Network content download status. + + WN_GRAPH_SELECTOR_WINDOW_COMPANY = 0, ///< Company selector window for graphs. + WN_GRAPH_SELECTOR_WINDOW_CARGO, ///< Cargo selector window for graphs. }; /** %Window classes. */ @@ -516,7 +519,8 @@ enum WindowClass { /** * Legend for graphs; %Window numbers: - * - 0 = #GraphLegendWidgets + * - 0 = #CompanyGraphLegendWidgets + * - 1 = #CargoGraphLegendWidgets */ WC_GRAPH_LEGEND,