diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index e073544f69..24a763966c 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -65,7 +65,7 @@ "_DEBUG", "UNICODE", "_UNICODE", - "__ENABLE_SCRIPTING__" + "ENABLE_SCRIPTING" ], "intelliSenseMode": "msvc-x64", "browse": { diff --git a/src/openrct2-ui/scripting/CustomListView.cpp b/src/openrct2-ui/scripting/CustomListView.cpp new file mode 100644 index 0000000000..5d8b02610d --- /dev/null +++ b/src/openrct2-ui/scripting/CustomListView.cpp @@ -0,0 +1,491 @@ +/***************************************************************************** + * Copyright (c) 2014-2020 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#ifdef ENABLE_SCRIPTING + +# include "CustomListView.h" + +# include "../interface/Window.h" + +# include +# include +# include + +using namespace OpenRCT2::Scripting; +using namespace OpenRCT2::Ui::Windows; + +namespace OpenRCT2::Scripting +{ + template<> ColumnSortOrder FromDuk(const DukValue& d) + { + if (d.type() == DukValue::Type::STRING) + { + auto s = d.as_string(); + if (s == "ascending") + return ColumnSortOrder::Ascending; + if (s == "descending") + return ColumnSortOrder::Descending; + } + return ColumnSortOrder::None; + } + + template<> std::optional FromDuk(const DukValue& d) + { + if (d.type() == DukValue::Type::NUMBER) + { + return d.as_int(); + } + return std::nullopt; + } + + template<> ListViewColumn FromDuk(const DukValue& d) + { + ListViewColumn result; + result.CanSort = AsOrDefault(d["canSort"], false); + result.SortOrder = FromDuk(d["sortOrder"]); + result.Header = AsOrDefault(d["header"], ""); + result.HeaderTooltip = AsOrDefault(d["headerTooltip"], ""); + result.MinWidth = FromDuk>(d["minWidth"]); + result.MaxWidth = FromDuk>(d["maxWidth"]); + result.RatioWidth = FromDuk>(d["ratioWidth"]); + if (d["width"].type() == DukValue::Type::NUMBER) + { + result.MinWidth = d["width"].as_int(); + result.MaxWidth = result.MinWidth; + result.RatioWidth = std::nullopt; + } + else if (!result.RatioWidth) + { + result.RatioWidth = 1; + } + return result; + } + + template<> ListViewItem FromDuk(const DukValue& d) + { + ListViewItem result; + if (d.type() == DukValue::Type::STRING) + { + result = ListViewItem(ProcessString(d)); + } + else if (d.is_array()) + { + std::vector cells; + for (const auto& dukCell : d.as_array()) + { + cells.push_back(ProcessString(dukCell)); + } + result = ListViewItem(std::move(cells)); + } + return result; + } +} // namespace OpenRCT2::Scripting + +void CustomListView::SetItems(const std::vector& items) +{ + Items = items; + SortItems(0, ColumnSortOrder::None); +} + +void CustomListView::SetItems(std::vector&& items) +{ + Items = items; + SortItems(0, ColumnSortOrder::None); +} + +bool CustomListView::SortItem(size_t indexA, size_t indexB, size_t column) +{ + const auto& cellA = Items[indexA].Cells[column]; + const auto& cellB = Items[indexB].Cells[column]; + return strlogicalcmp(cellA.c_str(), cellB.c_str()) < 0; +} + +void CustomListView::SortItems(size_t column) +{ + auto sortOrder = ColumnSortOrder::Ascending; + if (CurrentSortColumn == column) + { + if (CurrentSortOrder == ColumnSortOrder::Ascending) + { + sortOrder = ColumnSortOrder::Descending; + } + else if (CurrentSortOrder == ColumnSortOrder::Descending) + { + sortOrder = ColumnSortOrder::None; + } + } + SortItems(column, sortOrder); +} + +void CustomListView::SortItems(size_t column, ColumnSortOrder order) +{ + // Reset the sorted index map + SortedItems.resize(Items.size()); + for (size_t i = 0; i < SortedItems.size(); i++) + { + SortedItems[i] = i; + } + + if (order != ColumnSortOrder::None) + { + std::sort( + SortedItems.begin(), SortedItems.end(), [this, column](size_t a, size_t b) { return SortItem(a, b, column); }); + if (order == ColumnSortOrder::Descending) + { + std::reverse(SortedItems.begin(), SortedItems.end()); + } + } + + CurrentSortOrder = order; + CurrentSortColumn = column; + Columns[column].SortOrder = order; +} + +void CustomListView::Resize(const ScreenSize& size) +{ + if (size == LastKnownSize) + return; + + LastKnownSize = size; + + // Calculate the total of all ratios + int32_t totalRatio = 0; + for (size_t c = 0; c < Columns.size(); c++) + { + auto& column = Columns[c]; + if (column.RatioWidth) + { + totalRatio += *column.RatioWidth; + } + } + + // Calculate column widths + int32_t widthRemaining = size.width; + for (size_t c = 0; c < Columns.size(); c++) + { + auto& column = Columns[c]; + if (c == Columns.size() - 1) + { + column.Width = widthRemaining; + } + else + { + column.Width = 0; + if (column.RatioWidth && *column.RatioWidth > 0) + { + column.Width = (size.width * *column.RatioWidth) / totalRatio; + } + if (column.MinWidth) + { + column.Width = std::max(column.Width, *column.MinWidth); + } + if (column.MaxWidth) + { + column.Width = std::min(column.Width, *column.MaxWidth); + } + } + widthRemaining -= column.Width; + } +} + +ScreenSize CustomListView::GetSize() +{ + LastHighlightedCell = HighlightedCell; + HighlightedCell = std::nullopt; + ColumnHeaderPressedCurrentState = false; + LastIsMouseDown = IsMouseDown; + IsMouseDown = false; + + ScreenSize result; + result.width = 0; + result.height = static_cast(Items.size() * LIST_ROW_HEIGHT); + return result; +} + +void CustomListView::MouseOver(const ScreenCoordsXY& pos, bool isMouseDown) +{ + auto hitResult = GetItemIndexAt(pos); + if (hitResult) + { + HighlightedCell = hitResult; + if (HighlightedCell != LastHighlightedCell) + { + if (hitResult->Row != HEADER_ROW && OnHighlight.context() != nullptr && OnHighlight.is_function()) + { + auto ctx = OnHighlight.context(); + duk_push_int(ctx, static_cast(HighlightedCell->Row)); + auto dukRow = DukValue::take_from_stack(ctx, -1); + duk_push_int(ctx, static_cast(HighlightedCell->Column)); + auto dukColumn = DukValue::take_from_stack(ctx, -1); + auto& scriptEngine = GetContext()->GetScriptEngine(); + scriptEngine.ExecutePluginCall(Owner, OnHighlight, { dukRow, dukColumn }, false); + } + } + } + + // Update the header currently held down + if (isMouseDown) + { + if (hitResult && hitResult->Row == HEADER_ROW) + { + ColumnHeaderPressedCurrentState = (hitResult->Column == ColumnHeaderPressed); + } + IsMouseDown = true; + } + else + { + if (LastIsMouseDown) + { + MouseUp(pos); + } + IsMouseDown = false; + } +} + +void CustomListView::MouseDown(const ScreenCoordsXY& pos) +{ + auto hitResult = GetItemIndexAt(pos); + if (hitResult) + { + if (hitResult->Row != HEADER_ROW && OnClick.context() != nullptr && OnClick.is_function()) + { + if (CanSelect) + { + SelectedCell = hitResult; + } + + auto ctx = OnClick.context(); + duk_push_int(ctx, static_cast(hitResult->Row)); + auto dukRow = DukValue::take_from_stack(ctx, -1); + duk_push_int(ctx, static_cast(hitResult->Column)); + auto dukColumn = DukValue::take_from_stack(ctx, -1); + auto& scriptEngine = GetContext()->GetScriptEngine(); + scriptEngine.ExecutePluginCall(Owner, OnClick, { dukRow, dukColumn }, false); + } + } + if (hitResult && hitResult->Row == HEADER_ROW) + { + if (Columns[hitResult->Column].CanSort) + { + ColumnHeaderPressed = hitResult->Column; + ColumnHeaderPressedCurrentState = true; + } + } + IsMouseDown = true; +} + +void CustomListView::MouseUp(const ScreenCoordsXY& pos) +{ + auto hitResult = GetItemIndexAt(pos); + if (hitResult && hitResult->Row == HEADER_ROW) + { + if (hitResult->Column == ColumnHeaderPressed) + { + SortItems(hitResult->Column); + } + } + + ColumnHeaderPressed = std::nullopt; + ColumnHeaderPressedCurrentState = false; +} + +void CustomListView::Paint(rct_window* w, rct_drawpixelinfo* dpi, const rct_scroll* scroll) const +{ + auto paletteIndex = ColourMapA[w->colours[1]].mid_light; + gfx_fill_rect(dpi, dpi->x, dpi->y, dpi->x + dpi->width, dpi->y + dpi->height, paletteIndex); + + int32_t y = ShowColumnHeaders ? LIST_ROW_HEIGHT + 1 : 0; + for (size_t i = 0; i < Items.size(); i++) + { + if (y > dpi->y + dpi->height) + { + // Past the scroll view area + break; + } + + if (y + LIST_ROW_HEIGHT >= dpi->y) + { + const auto& itemIndex = static_cast(SortedItems[i]); + const auto& item = Items[itemIndex]; + + // Background colour + auto isStriped = IsStriped && (i & 1); + auto isHighlighted = (HighlightedCell && itemIndex == HighlightedCell->Row); + auto isSelected = (SelectedCell && itemIndex == SelectedCell->Row); + if (isHighlighted) + { + gfx_filter_rect(dpi, dpi->x, y, dpi->x + dpi->width, y + (LIST_ROW_HEIGHT - 1), PALETTE_DARKEN_1); + } + else if (isSelected) + { + // gfx_fill_rect(dpi, dpi->x, y, dpi->x + dpi->width, y + LIST_ROW_HEIGHT - 1, + // ColourMapA[w->colours[1]].dark); + gfx_filter_rect(dpi, dpi->x, y, dpi->x + dpi->width, y + (LIST_ROW_HEIGHT - 1), PALETTE_DARKEN_2); + } + else if (isStriped) + { + gfx_fill_rect( + dpi, dpi->x, y, dpi->x + dpi->width, y + (LIST_ROW_HEIGHT - 1), + ColourMapA[w->colours[1]].lighter | 0x1000000); + } + + // Columns + if (Columns.size() == 0) + { + const auto& text = item.Cells[0]; + if (!text.empty()) + { + ScreenSize cellSize = { std::numeric_limits::max(), LIST_ROW_HEIGHT }; + PaintCell(dpi, { 0, y }, cellSize, text.c_str(), isHighlighted); + } + } + else + { + int32_t x = 0; + for (size_t j = 0; j < Columns.size(); j++) + { + const auto& column = Columns[j]; + if (item.Cells.size() > j) + { + const auto& text = item.Cells[j]; + if (!text.empty()) + { + ScreenSize cellSize = { column.Width, LIST_ROW_HEIGHT }; + PaintCell(dpi, { x, y }, cellSize, text.c_str(), isHighlighted); + } + } + x += column.Width; + } + } + } + + y += LIST_ROW_HEIGHT; + } + + if (ShowColumnHeaders) + { + y = scroll->v_top; + + auto bgColour = ColourMapA[w->colours[1]].mid_light; + gfx_fill_rect(dpi, dpi->x, y, dpi->x + dpi->width, y + 12, bgColour); + + int32_t x = 0; + for (size_t j = 0; j < Columns.size(); j++) + { + const auto& column = Columns[j]; + auto columnWidth = column.Width; + if (columnWidth != 0) + { + auto sortOrder = ColumnSortOrder::None; + if (CurrentSortColumn == j) + { + sortOrder = CurrentSortOrder; + } + + bool isPressed = ColumnHeaderPressed == j && ColumnHeaderPressedCurrentState; + PaintHeading(w, dpi, { x, y }, { column.Width, LIST_ROW_HEIGHT }, column.Header, sortOrder, isPressed); + x += columnWidth; + } + } + } +} + +void CustomListView::PaintHeading( + rct_window* w, rct_drawpixelinfo* dpi, const ScreenCoordsXY& pos, const ScreenSize& size, const std::string& text, + ColumnSortOrder sortOrder, bool isPressed) const +{ + auto boxFlags = 0; + if (isPressed) + { + boxFlags = INSET_RECT_FLAG_BORDER_INSET; + } + gfx_fill_rect_inset(dpi, pos.x, pos.y, pos.x + size.width - 1, pos.y + size.height - 1, w->colours[1], boxFlags); + if (!text.empty()) + { + PaintCell(dpi, pos, size, text.c_str(), false); + } + + if (sortOrder == ColumnSortOrder::Ascending) + { + auto ft = Formatter::Common(); + ft.Add(STR_UP); + gfx_draw_string_right(dpi, STR_BLACK_STRING, gCommonFormatArgs, COLOUR_BLACK, pos.x + size.width - 1, pos.y); + } + else if (sortOrder == ColumnSortOrder::Descending) + { + auto ft = Formatter::Common(); + ft.Add(STR_DOWN); + gfx_draw_string_right(dpi, STR_BLACK_STRING, gCommonFormatArgs, COLOUR_BLACK, pos.x + size.width - 1, pos.y); + } +} + +void CustomListView::PaintCell( + rct_drawpixelinfo* dpi, const ScreenCoordsXY& pos, const ScreenSize& size, const char* text, bool isHighlighted) const +{ + rct_string_id stringId = isHighlighted ? STR_WINDOW_COLOUR_2_STRINGID : STR_BLACK_STRING; + + auto ft = Formatter::Common(); + ft.Add(STR_STRING); + ft.Add(text); + gfx_draw_string_left_clipped(dpi, stringId, gCommonFormatArgs, COLOUR_BLACK, pos.x, pos.y, size.width); +} + +std::optional CustomListView::GetItemIndexAt(const ScreenCoordsXY& pos) +{ + std::optional result; + if (pos.x >= 0) + { + // Check if we pressed the header + if (ShowColumnHeaders && pos.y >= 0 && pos.y < LIST_ROW_HEIGHT) + { + result = RowColumn(); + result->Row = HEADER_ROW; + } + else + { + // Check what row we pressed + int32_t firstY = ShowColumnHeaders ? LIST_ROW_HEIGHT + 1 : 0; + int32_t row = (pos.y - firstY) / LIST_ROW_HEIGHT; + if (row >= 0 && row < static_cast(Items.size())) + { + result = RowColumn(); + result->Row = static_cast(SortedItems[row]); + } + } + + // Check what column we pressed if there are any + if (result && Columns.size() > 0) + { + bool found = false; + int32_t x = 0; + for (size_t c = 0; c < Columns.size(); c++) + { + const auto& column = Columns[c]; + x += column.Width; + if (column.Width != 0) + { + if (pos.x < x) + { + result->Column = static_cast(c); + found = true; + break; + } + } + } + if (!found) + { + // Past all columns + return std::nullopt; + } + } + } + return result; +} + +#endif diff --git a/src/openrct2-ui/scripting/CustomListView.h b/src/openrct2-ui/scripting/CustomListView.h new file mode 100644 index 0000000000..dda7c69825 --- /dev/null +++ b/src/openrct2-ui/scripting/CustomListView.h @@ -0,0 +1,154 @@ +/***************************************************************************** + * Copyright (c) 2014-2020 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#pragma once + +#ifdef ENABLE_SCRIPTING + +# include +# include +# include +# include +# include +# include +# include + +namespace OpenRCT2::Ui::Windows +{ + using namespace OpenRCT2::Scripting; + + enum class ScrollbarType + { + None, + Horizontal, + Vertical, + Both + }; + + enum class ColumnSortOrder + { + None, + Ascending, + Descending, + }; + + struct ListViewColumn + { + bool CanSort{}; + ColumnSortOrder SortOrder; + std::string Header; + std::string HeaderTooltip; + std::optional RatioWidth{}; + std::optional MinWidth{}; + std::optional MaxWidth{}; + int32_t Width{}; + }; + + struct ListViewItem + { + std::vector Cells; + + ListViewItem() = default; + explicit ListViewItem(const std::string_view& text) + { + Cells.emplace_back(text); + } + explicit ListViewItem(std::vector&& cells) + : Cells(cells) + { + } + }; + + struct RowColumn + { + int32_t Row{}; + int32_t Column{}; + + RowColumn() = default; + RowColumn(int32_t row, int32_t column) + : Row(row) + , Column(column) + { + } + + bool operator==(const RowColumn& other) const + { + return Row == other.Row && Column == other.Column; + } + + bool operator!=(const RowColumn& other) const + { + return !(*this == other); + } + }; + + class CustomListView + { + private: + static constexpr int32_t HEADER_ROW = -1; + std::vector Items; + + public: + std::shared_ptr Owner; + std::vector Columns; + std::vector SortedItems; + std::optional HighlightedCell; + std::optional LastHighlightedCell; + std::optional SelectedCell; + std::optional ColumnHeaderPressed; + bool ColumnHeaderPressedCurrentState{}; + bool ShowColumnHeaders{}; + bool IsStriped{}; + ScreenSize LastKnownSize; + ScrollbarType Scrollbars = ScrollbarType::Vertical; + ColumnSortOrder CurrentSortOrder{}; + size_t CurrentSortColumn{}; + bool LastIsMouseDown{}; + bool IsMouseDown{}; + bool CanSelect{}; + + DukValue OnClick; + DukValue OnHighlight; + + void SetItems(const std::vector& items); + void SetItems(std::vector&& items); + bool SortItem(size_t indexA, size_t indexB, size_t column); + void SortItems(size_t column); + void SortItems(size_t column, ColumnSortOrder order); + void Resize(const ScreenSize& size); + ScreenSize GetSize(); + void MouseOver(const ScreenCoordsXY& pos, bool isMouseDown); + void MouseDown(const ScreenCoordsXY& pos); + void MouseUp(const ScreenCoordsXY& pos); + void Paint(rct_window* w, rct_drawpixelinfo* dpi, const rct_scroll* scroll) const; + + private: + void PaintHeading( + rct_window* w, rct_drawpixelinfo* dpi, const ScreenCoordsXY& pos, const ScreenSize& size, const std::string& text, + ColumnSortOrder sortOrder, bool isPressed) const; + void PaintCell( + rct_drawpixelinfo* dpi, const ScreenCoordsXY& pos, const ScreenSize& size, const char* text, + bool isHighlighted) const; + std::optional GetItemIndexAt(const ScreenCoordsXY& pos); + }; +} // namespace OpenRCT2::Ui::Windows + +class DukValue; + +namespace OpenRCT2::Scripting +{ + using namespace OpenRCT2::Ui::Windows; + + template<> ColumnSortOrder FromDuk(const DukValue& d); + template<> std::optional FromDuk(const DukValue& d); + template<> ListViewColumn FromDuk(const DukValue& d); + template<> ListViewItem FromDuk(const DukValue& d); +} // namespace OpenRCT2::Scripting + +#endif diff --git a/src/openrct2-ui/scripting/CustomWindow.cpp b/src/openrct2-ui/scripting/CustomWindow.cpp index 17a2bf7368..992d0d8c7c 100644 --- a/src/openrct2-ui/scripting/CustomWindow.cpp +++ b/src/openrct2-ui/scripting/CustomWindow.cpp @@ -10,6 +10,7 @@ #ifdef ENABLE_SCRIPTING # include "../interface/Dropdown.h" +# include "CustomListView.h" # include "ScUi.hpp" # include "ScWindow.hpp" @@ -22,7 +23,6 @@ # include # include # include -# include # include # include # include @@ -31,125 +31,6 @@ using namespace OpenRCT2; using namespace OpenRCT2::Scripting; -namespace OpenRCT2::Ui::Windows -{ - enum class ScrollbarType - { - None, - Horizontal, - Vertical, - Both - }; - - enum class ColumnSortOrder - { - None, - Ascending, - Descending, - }; - - struct ListViewColumn - { - bool CanSort{}; - ColumnSortOrder SortOrder; - std::string Header; - std::string HeaderTooltip; - std::optional RatioWidth{}; - std::optional MinWidth{}; - std::optional MaxWidth{}; - int32_t Width{}; - }; - - struct ListViewItem - { - std::vector Cells; - - ListViewItem() = default; - explicit ListViewItem(const std::string_view& text) - { - Cells.emplace_back(text); - } - explicit ListViewItem(std::vector&& cells) - : Cells(cells) - { - } - }; -} // namespace OpenRCT2::Ui::Windows - -namespace OpenRCT2::Scripting -{ - static std::string ProcessString(const DukValue& value) - { - if (value.type() == DukValue::Type::STRING) - return language_convert_string(value.as_string()); - return {}; - } - - template<> ColumnSortOrder FromDuk(const DukValue& d) - { - if (d.type() == DukValue::Type::STRING) - { - auto s = d.as_string(); - if (s == "ascending") - return ColumnSortOrder::Ascending; - if (s == "descending") - return ColumnSortOrder::Descending; - } - return ColumnSortOrder::None; - } - - template<> std::optional FromDuk(const DukValue& d) - { - if (d.type() == DukValue::Type::NUMBER) - { - return d.as_int(); - } - return std::nullopt; - } - - template<> ListViewColumn FromDuk(const DukValue& d) - { - ListViewColumn result; - result.CanSort = AsOrDefault(d["canSort"], false); - result.SortOrder = FromDuk(d["sortOrder"]); - result.Header = AsOrDefault(d["header"], ""); - result.HeaderTooltip = AsOrDefault(d["headerTooltip"], ""); - result.MinWidth = FromDuk>(d["minWidth"]); - result.MaxWidth = FromDuk>(d["maxWidth"]); - result.RatioWidth = FromDuk>(d["ratioWidth"]); - if (d["width"].type() == DukValue::Type::NUMBER) - { - result.MinWidth = d["width"].as_int(); - result.MaxWidth = result.MinWidth; - result.RatioWidth = std::nullopt; - } - else if (!result.RatioWidth) - { - result.RatioWidth = 1; - } - return result; - } - - template<> ListViewItem FromDuk(const DukValue& d) - { - ListViewItem result; - if (d.type() == DukValue::Type::STRING) - { - result = ListViewItem(ProcessString(d)); - } - else if (d.is_array()) - { - std::vector cells; - for (const auto& dukCell : d.as_array()) - { - cells.push_back(ProcessString(dukCell)); - } - result = ListViewItem(std::move(cells)); - } - return result; - } -} // namespace OpenRCT2::Scripting - namespace OpenRCT2::Ui::Windows { enum CUSTOM_WINDOW_WIDX @@ -448,462 +329,6 @@ namespace OpenRCT2::Ui::Windows } }; - struct RowColumn - { - int32_t Row{}; - int32_t Column{}; - - RowColumn() = default; - RowColumn(int32_t row, int32_t column) - : Row(row) - , Column(column) - { - } - - bool operator==(const RowColumn& other) const - { - return Row == other.Row && Column == other.Column; - } - - bool operator!=(const RowColumn& other) const - { - return !(*this == other); - } - }; - - class CustomListViewInfo - { - private: - static constexpr int32_t HEADER_ROW = -1; - std::vector Items; - - public: - std::shared_ptr Owner; - std::vector Columns; - std::vector SortedItems; - std::optional HighlightedCell; - std::optional LastHighlightedCell; - std::optional SelectedCell; - std::optional ColumnHeaderPressed; - bool ColumnHeaderPressedCurrentState{}; - bool ShowColumnHeaders{}; - bool IsStriped{}; - ScreenSize LastKnownSize; - ScrollbarType Scrollbars = ScrollbarType::Vertical; - ColumnSortOrder CurrentSortOrder{}; - size_t CurrentSortColumn{}; - bool LastIsMouseDown{}; - bool IsMouseDown{}; - bool CanSelect{}; - - DukValue OnClick; - DukValue OnHighlight; - - void SetItems(const std::vector& items) - { - Items = items; - SortItems(0, ColumnSortOrder::None); - } - - void SetItems(std::vector&& items) - { - Items = items; - SortItems(0, ColumnSortOrder::None); - } - - bool SortItem(size_t indexA, size_t indexB, size_t column) - { - const auto& cellA = Items[indexA].Cells[column]; - const auto& cellB = Items[indexB].Cells[column]; - return strlogicalcmp(cellA.c_str(), cellB.c_str()) < 0; - } - - void SortItems(size_t column) - { - auto sortOrder = ColumnSortOrder::Ascending; - if (CurrentSortColumn == column) - { - if (CurrentSortOrder == ColumnSortOrder::Ascending) - { - sortOrder = ColumnSortOrder::Descending; - } - else if (CurrentSortOrder == ColumnSortOrder::Descending) - { - sortOrder = ColumnSortOrder::None; - } - } - SortItems(column, sortOrder); - } - - void SortItems(size_t column, ColumnSortOrder order) - { - // Reset the sorted index map - SortedItems.resize(Items.size()); - for (size_t i = 0; i < SortedItems.size(); i++) - { - SortedItems[i] = i; - } - - if (order != ColumnSortOrder::None) - { - std::sort(SortedItems.begin(), SortedItems.end(), [this, column](size_t a, size_t b) { - return SortItem(a, b, column); - }); - if (order == ColumnSortOrder::Descending) - { - std::reverse(SortedItems.begin(), SortedItems.end()); - } - } - - CurrentSortOrder = order; - CurrentSortColumn = column; - Columns[column].SortOrder = order; - } - - void Resize(const ScreenSize& size) - { - if (size == LastKnownSize) - return; - - LastKnownSize = size; - - // Calculate the total of all ratios - int32_t totalRatio = 0; - for (size_t c = 0; c < Columns.size(); c++) - { - auto& column = Columns[c]; - if (column.RatioWidth) - { - totalRatio += *column.RatioWidth; - } - } - - // Calculate column widths - int32_t widthRemaining = size.width; - for (size_t c = 0; c < Columns.size(); c++) - { - auto& column = Columns[c]; - if (c == Columns.size() - 1) - { - column.Width = widthRemaining; - } - else - { - column.Width = 0; - if (column.RatioWidth && *column.RatioWidth > 0) - { - column.Width = (size.width * *column.RatioWidth) / totalRatio; - } - if (column.MinWidth) - { - column.Width = std::max(column.Width, *column.MinWidth); - } - if (column.MaxWidth) - { - column.Width = std::min(column.Width, *column.MaxWidth); - } - } - widthRemaining -= column.Width; - } - } - - ScreenSize GetSize() - { - LastHighlightedCell = HighlightedCell; - HighlightedCell = std::nullopt; - ColumnHeaderPressedCurrentState = false; - LastIsMouseDown = IsMouseDown; - IsMouseDown = false; - - ScreenSize result; - result.width = 0; - result.height = static_cast(Items.size() * LIST_ROW_HEIGHT); - return result; - } - - void MouseOver(const ScreenCoordsXY& pos, bool isMouseDown) - { - auto hitResult = GetItemIndexAt(pos); - if (hitResult) - { - HighlightedCell = hitResult; - if (HighlightedCell != LastHighlightedCell) - { - if (hitResult->Row != HEADER_ROW && OnHighlight.context() != nullptr && OnHighlight.is_function()) - { - auto ctx = OnHighlight.context(); - duk_push_int(ctx, static_cast(HighlightedCell->Row)); - auto dukRow = DukValue::take_from_stack(ctx, -1); - duk_push_int(ctx, static_cast(HighlightedCell->Column)); - auto dukColumn = DukValue::take_from_stack(ctx, -1); - auto& scriptEngine = GetContext()->GetScriptEngine(); - scriptEngine.ExecutePluginCall(Owner, OnHighlight, { dukRow, dukColumn }, false); - } - } - } - - // Update the header currently held down - if (isMouseDown) - { - if (hitResult && hitResult->Row == HEADER_ROW) - { - ColumnHeaderPressedCurrentState = (hitResult->Column == ColumnHeaderPressed); - } - IsMouseDown = true; - } - else - { - if (LastIsMouseDown) - { - MouseUp(pos); - } - IsMouseDown = false; - } - } - - void MouseDown(const ScreenCoordsXY& pos) - { - auto hitResult = GetItemIndexAt(pos); - if (hitResult) - { - if (hitResult->Row != HEADER_ROW && OnClick.context() != nullptr && OnClick.is_function()) - { - if (CanSelect) - { - SelectedCell = hitResult; - } - - auto ctx = OnClick.context(); - duk_push_int(ctx, static_cast(hitResult->Row)); - auto dukRow = DukValue::take_from_stack(ctx, -1); - duk_push_int(ctx, static_cast(hitResult->Column)); - auto dukColumn = DukValue::take_from_stack(ctx, -1); - auto& scriptEngine = GetContext()->GetScriptEngine(); - scriptEngine.ExecutePluginCall(Owner, OnClick, { dukRow, dukColumn }, false); - } - } - if (hitResult && hitResult->Row == HEADER_ROW) - { - if (Columns[hitResult->Column].CanSort) - { - ColumnHeaderPressed = hitResult->Column; - ColumnHeaderPressedCurrentState = true; - } - } - IsMouseDown = true; - } - - void MouseUp(const ScreenCoordsXY& pos) - { - auto hitResult = GetItemIndexAt(pos); - if (hitResult && hitResult->Row == HEADER_ROW) - { - if (hitResult->Column == ColumnHeaderPressed) - { - SortItems(hitResult->Column); - } - } - - ColumnHeaderPressed = std::nullopt; - ColumnHeaderPressedCurrentState = false; - } - - void Paint(rct_window* w, rct_drawpixelinfo* dpi, const rct_scroll* scroll) const - { - auto paletteIndex = ColourMapA[w->colours[1]].mid_light; - gfx_fill_rect(dpi, dpi->x, dpi->y, dpi->x + dpi->width, dpi->y + dpi->height, paletteIndex); - - int32_t y = ShowColumnHeaders ? LIST_ROW_HEIGHT + 1 : 0; - for (size_t i = 0; i < Items.size(); i++) - { - if (y > dpi->y + dpi->height) - { - // Past the scroll view area - break; - } - - if (y + LIST_ROW_HEIGHT >= dpi->y) - { - const auto& itemIndex = SortedItems[i]; - const auto& item = Items[itemIndex]; - - // Background colour - auto isStriped = IsStriped && (i & 1); - auto isHighlighted = (HighlightedCell && itemIndex == HighlightedCell->Row); - auto isSelected = (SelectedCell && itemIndex == SelectedCell->Row); - if (isHighlighted) - { - gfx_filter_rect(dpi, dpi->x, y, dpi->x + dpi->width, y + (LIST_ROW_HEIGHT - 1), PALETTE_DARKEN_1); - } - else if (isSelected) - { - // gfx_fill_rect(dpi, dpi->x, y, dpi->x + dpi->width, y + LIST_ROW_HEIGHT - 1, - // ColourMapA[w->colours[1]].dark); - gfx_filter_rect(dpi, dpi->x, y, dpi->x + dpi->width, y + (LIST_ROW_HEIGHT - 1), PALETTE_DARKEN_2); - } - else if (isStriped) - { - gfx_fill_rect( - dpi, dpi->x, y, dpi->x + dpi->width, y + (LIST_ROW_HEIGHT - 1), - ColourMapA[w->colours[1]].lighter | 0x1000000); - } - - // Columns - if (Columns.size() == 0) - { - const auto& text = item.Cells[0]; - if (!text.empty()) - { - ScreenSize cellSize = { std::numeric_limits::max(), LIST_ROW_HEIGHT }; - PaintCell(dpi, { 0, y }, cellSize, text.c_str(), isHighlighted); - } - } - else - { - int32_t x = 0; - for (size_t j = 0; j < Columns.size(); j++) - { - const auto& column = Columns[j]; - if (item.Cells.size() > j) - { - const auto& text = item.Cells[j]; - if (!text.empty()) - { - ScreenSize cellSize = { column.Width, LIST_ROW_HEIGHT }; - PaintCell(dpi, { x, y }, cellSize, text.c_str(), isHighlighted); - } - } - x += column.Width; - } - } - } - - y += LIST_ROW_HEIGHT; - } - - if (ShowColumnHeaders) - { - y = scroll->v_top; - - auto bgColour = ColourMapA[w->colours[1]].mid_light; - gfx_fill_rect(dpi, dpi->x, y, dpi->x + dpi->width, y + 12, bgColour); - - int32_t x = 0; - for (size_t j = 0; j < Columns.size(); j++) - { - const auto& column = Columns[j]; - auto columnWidth = column.Width; - if (columnWidth != 0) - { - auto sortOrder = ColumnSortOrder::None; - if (CurrentSortColumn == j) - { - sortOrder = CurrentSortOrder; - } - - bool isPressed = ColumnHeaderPressed == j && ColumnHeaderPressedCurrentState; - PaintHeading(w, dpi, { x, y }, { column.Width, LIST_ROW_HEIGHT }, column.Header, sortOrder, isPressed); - x += columnWidth; - } - } - } - } - - private: - void PaintHeading( - rct_window* w, rct_drawpixelinfo* dpi, const ScreenCoordsXY& pos, const ScreenSize& size, const std::string& text, - ColumnSortOrder sortOrder, bool isPressed) const - { - auto boxFlags = 0; - if (isPressed) - { - boxFlags = INSET_RECT_FLAG_BORDER_INSET; - } - gfx_fill_rect_inset(dpi, pos.x, pos.y, pos.x + size.width - 1, pos.y + size.height - 1, w->colours[1], boxFlags); - if (!text.empty()) - { - PaintCell(dpi, pos, size, text.c_str(), false); - } - - if (sortOrder == ColumnSortOrder::Ascending) - { - auto ft = Formatter::Common(); - ft.Add(STR_UP); - gfx_draw_string_right(dpi, STR_BLACK_STRING, gCommonFormatArgs, COLOUR_BLACK, pos.x + size.width - 1, pos.y); - } - else if (sortOrder == ColumnSortOrder::Descending) - { - auto ft = Formatter::Common(); - ft.Add(STR_DOWN); - gfx_draw_string_right(dpi, STR_BLACK_STRING, gCommonFormatArgs, COLOUR_BLACK, pos.x + size.width - 1, pos.y); - } - } - - void PaintCell( - rct_drawpixelinfo* dpi, const ScreenCoordsXY& pos, const ScreenSize& size, const char* text, - bool isHighlighted) const - { - rct_string_id stringId = isHighlighted ? STR_WINDOW_COLOUR_2_STRINGID : STR_BLACK_STRING; - - auto ft = Formatter::Common(); - ft.Add(STR_STRING); - ft.Add(text); - gfx_draw_string_left_clipped(dpi, stringId, gCommonFormatArgs, COLOUR_BLACK, pos.x, pos.y, size.width); - } - - std::optional GetItemIndexAt(const ScreenCoordsXY& pos) - { - std::optional result; - if (pos.x >= 0) - { - // Check if we pressed the header - if (ShowColumnHeaders && pos.y >= 0 && pos.y < LIST_ROW_HEIGHT) - { - result = RowColumn(); - result->Row = HEADER_ROW; - } - else - { - // Check what row we pressed - int32_t firstY = ShowColumnHeaders ? LIST_ROW_HEIGHT + 1 : 0; - int32_t row = (pos.y - firstY) / LIST_ROW_HEIGHT; - if (row >= 0 && row < static_cast(Items.size())) - { - result = RowColumn(); - result->Row = static_cast(SortedItems[row]); - } - } - - // Check what column we pressed if there are any - if (result && Columns.size() > 0) - { - bool found = false; - int32_t x = 0; - for (size_t c = 0; c < Columns.size(); c++) - { - const auto& column = Columns[c]; - x += column.Width; - if (column.Width != 0) - { - if (pos.x < x) - { - result->Column = static_cast(c); - found = true; - break; - } - } - } - if (!found) - { - // Past all columns - return std::nullopt; - } - } - } - return result; - } - }; - class CustomWindowInfo { public: @@ -911,7 +336,7 @@ namespace OpenRCT2::Ui::Windows CustomWindowDesc Desc; std::vector Widgets; std::vector WidgetIndexMap; - std::vector ListViews; + std::vector ListViews; CustomWindowInfo(std::shared_ptr owner, const CustomWindowDesc& desc) : Owner(owner) @@ -1179,7 +604,7 @@ namespace OpenRCT2::Ui::Windows static void window_custom_scrollgetsize(rct_window* w, int32_t scrollIndex, int32_t* width, int32_t* height) { auto& info = GetInfo(w); - if (scrollIndex < info.ListViews.size()) + if (scrollIndex < static_cast(info.ListViews.size())) { auto size = info.ListViews[scrollIndex].GetSize(); *width = size.width; @@ -1190,7 +615,7 @@ namespace OpenRCT2::Ui::Windows static void window_custom_scrollmousedown(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords) { auto& info = GetInfo(w); - if (scrollIndex < info.ListViews.size()) + if (scrollIndex < static_cast(info.ListViews.size())) { info.ListViews[scrollIndex].MouseDown(screenCoords); } @@ -1199,7 +624,7 @@ namespace OpenRCT2::Ui::Windows static void window_custom_scrollmousedrag(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords) { auto& info = GetInfo(w); - if (scrollIndex < info.ListViews.size()) + if (scrollIndex < static_cast(info.ListViews.size())) { info.ListViews[scrollIndex].MouseOver(screenCoords, true); } @@ -1208,7 +633,7 @@ namespace OpenRCT2::Ui::Windows static void window_custom_scrollmouseover(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords) { auto& info = GetInfo(w); - if (scrollIndex < info.ListViews.size()) + if (scrollIndex < static_cast(info.ListViews.size())) { info.ListViews[scrollIndex].MouseOver(screenCoords, false); } @@ -1305,7 +730,7 @@ namespace OpenRCT2::Ui::Windows static void window_custom_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi, int32_t scrollIndex) { const auto& info = GetInfo(w); - if (scrollIndex < info.ListViews.size()) + if (scrollIndex < static_cast(info.ListViews.size())) { info.ListViews[scrollIndex].Paint(w, dpi, &w->scrolls[scrollIndex]); } @@ -1421,6 +846,10 @@ namespace OpenRCT2::Ui::Windows { widget.string = const_cast(desc.Items[desc.SelectedIndex].c_str()); } + else + { + widget.string = const_cast(""); + } widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; widgetList.push_back(widget); @@ -1556,7 +985,7 @@ namespace OpenRCT2::Ui::Windows if (widgetDesc.Type == "listview") { - CustomListViewInfo listView; + CustomListView listView; listView.Columns = widgetDesc.ListViewColumns; listView.SetItems(widgetDesc.ListViewItems); listView.ShowColumnHeaders = widgetDesc.ShowColumnHeaders; @@ -1673,6 +1102,20 @@ namespace OpenRCT2::Ui::Windows return std::nullopt; } + CustomListView* GetCustomListView(rct_window* w, rct_widgetindex widgetIndex) + { + if (w->custom_info != nullptr) + { + auto& customInfo = GetInfo(w); + auto scrollIndex = window_get_scroll_data_index(w, widgetIndex); + if (scrollIndex < static_cast(info.ListViews.size())) + { + return &customInfo.ListViews[scrollIndex]; + } + } + return nullptr; + } + } // namespace OpenRCT2::Ui::Windows #endif diff --git a/src/openrct2-ui/scripting/CustomWindow.h b/src/openrct2-ui/scripting/CustomWindow.h index 5877673780..3a1bd2ceb4 100644 --- a/src/openrct2-ui/scripting/CustomWindow.h +++ b/src/openrct2-ui/scripting/CustomWindow.h @@ -18,11 +18,14 @@ namespace OpenRCT2::Ui::Windows { + class CustomListView; + std::string GetWindowTitle(rct_window* w); void UpdateWindowTitle(rct_window* w, const std::string_view& value); void UpdateWidgetText(rct_window* w, rct_widgetindex widget, const std::string_view& string_view); rct_window* FindCustomWindowByClassification(const std::string_view& classification); std::optional FindWidgetIndexByName(rct_window* w, const std::string_view& name); + CustomListView* GetCustomListView(rct_window* w, rct_widgetindex widgetIndex); } // namespace OpenRCT2::Ui::Windows #endif diff --git a/src/openrct2-ui/scripting/ScWidget.hpp b/src/openrct2-ui/scripting/ScWidget.hpp index daeee14751..26987c71d0 100644 --- a/src/openrct2-ui/scripting/ScWidget.hpp +++ b/src/openrct2-ui/scripting/ScWidget.hpp @@ -13,6 +13,7 @@ # include "../interface/Widget.h" # include "../interface/Window.h" +# include "CustomListView.h" # include "CustomWindow.h" # include "ScViewport.hpp" @@ -351,10 +352,31 @@ namespace OpenRCT2::Scripting private: bool isStriped_get() const { + auto listView = GetListView(); + if (listView != nullptr) + { + return listView->IsStriped; + } + return false; } void isStriped_set(bool value) { + auto listView = GetListView(); + if (listView != nullptr) + { + listView->IsStriped = value; + } + } + + CustomListView* GetListView() const + { + auto w = GetWindow(); + if (w != nullptr) + { + return GetCustomListView(w, _widgetIndex); + } + return nullptr; } }; diff --git a/src/openrct2/scripting/Duktape.hpp b/src/openrct2/scripting/Duktape.hpp index 43d6ba3b53..02ec125821 100644 --- a/src/openrct2/scripting/Duktape.hpp +++ b/src/openrct2/scripting/Duktape.hpp @@ -190,6 +190,8 @@ namespace OpenRCT2::Scripting } } + std::string ProcessString(const DukValue& value); + template DukValue ToDuk(duk_context* ctx, const T& value) = delete; template T FromDuk(const DukValue& s) = delete; diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index fdb7f3dbec..8474d8b918 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -1136,6 +1136,13 @@ std::string OpenRCT2::Scripting::Stringify(const DukValue& val) return ExpressionStringifier::StringifyExpression(val); } +std::string OpenRCT2::Scripting::ProcessString(const DukValue& value) +{ + if (value.type() == DukValue::Type::STRING) + return language_convert_string(value.as_string()); + return {}; +} + bool OpenRCT2::Scripting::IsGameStateMutable() { // Allow single player to alter game state anywhere