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/OpenRCT2.xcodeproj/project.pbxproj b/OpenRCT2.xcodeproj/project.pbxproj index 7ee65cc2bd..10a5355d0b 100644 --- a/OpenRCT2.xcodeproj/project.pbxproj +++ b/OpenRCT2.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ 4C358E5221C445F700ADE6BC /* ReplayManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C358E5021C445F700ADE6BC /* ReplayManager.cpp */; }; 4C3B4236205914F7000C5BB7 /* InGameConsole.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C3B4234205914F7000C5BB7 /* InGameConsole.cpp */; }; 4C724B2221F0AD790012ADD0 /* BenchSpriteSort.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C724B2121F0AD790012ADD0 /* BenchSpriteSort.cpp */; }; + 4C81F7E124672C4D000E61BF /* CustomListView.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C81F7DF24672C4D000E61BF /* CustomListView.cpp */; }; 4C8A6FF323EB5326001A8255 /* Http.cURL.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C8A6FF223EB5326001A8255 /* Http.cURL.cpp */; }; 4C93F1AD1F8CD9F000A9330D /* Input.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C93F1AC1F8CD9F000A9330D /* Input.cpp */; }; 4C93F1AF1F8CD9F600A9330D /* KeyboardShortcut.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C93F1AE1F8CD9F600A9330D /* KeyboardShortcut.cpp */; }; @@ -838,6 +839,9 @@ 4C7B54792010DF4C00A52E21 /* Shared.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Shared.cpp; sourceTree = ""; }; 4C7B547A2010DF4C00A52E21 /* Windows.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Windows.cpp; sourceTree = ""; }; 4C7B547E2010DFF700A52E21 /* Crash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Crash.h; sourceTree = ""; }; + 4C81F7DF24672C4D000E61BF /* CustomListView.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CustomListView.cpp; path = scripting/CustomListView.cpp; sourceTree = ""; }; + 4C81F7E024672C4D000E61BF /* CustomListView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CustomListView.h; path = scripting/CustomListView.h; sourceTree = ""; }; + 4C81F7E224672C58000E61BF /* ScTileSelection.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ScTileSelection.hpp; path = scripting/ScTileSelection.hpp; sourceTree = ""; }; 4C8667801EEFDCDF0024AAB8 /* RideGroupManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = RideGroupManager.cpp; sourceTree = ""; }; 4C8667811EEFDCDF0024AAB8 /* RideGroupManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RideGroupManager.h; sourceTree = ""; }; 4C8A6FF123EB5325001A8255 /* Http.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Http.h; sourceTree = ""; }; @@ -1911,10 +1915,13 @@ 4C25594D244A326100CE7E45 /* scripting */ = { isa = PBXGroup; children = ( + 4C81F7DF24672C4D000E61BF /* CustomListView.cpp */, + 4C81F7E024672C4D000E61BF /* CustomListView.h */, 4C25594F244A328A00CE7E45 /* CustomMenu.cpp */, 4C255953244A328A00CE7E45 /* CustomMenu.h */, 4C255957244A328B00CE7E45 /* CustomWindow.cpp */, 4C25594E244A328A00CE7E45 /* CustomWindow.h */, + 4C81F7E224672C58000E61BF /* ScTileSelection.hpp */, 4C255950244A328A00CE7E45 /* ScUi.hpp */, 4C255955244A328A00CE7E45 /* ScViewport.hpp */, 4C255951244A328A00CE7E45 /* ScWidget.hpp */, @@ -4019,6 +4026,7 @@ C68878C020289B710084B384 /* ApplyPaletteShader.cpp in Sources */, C666EE791F37ACB10061AA04 /* ServerStart.cpp in Sources */, C61ADB231FBBCB8B0024F2EF /* GameBottomToolbar.cpp in Sources */, + 4C81F7E124672C4D000E61BF /* CustomListView.cpp in Sources */, 6341F4E22400AA0F0052902B /* Drawing.Sprite.BMP.cpp in Sources */, C666EE7E1F37ACB10061AA04 /* TitleOptions.cpp in Sources */, F76C887A1EC5324E00FA49E2 /* AudioMixer.cpp in Sources */, diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 92d97555e8..c5b50226ff 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -1072,7 +1072,7 @@ declare global { * Represents the type of a widget, e.g. button or label. */ type WidgetType = - "button" | "checkbox" | "dropdown" | "groupbox" | "label" | "spinner" | "viewport"; + "button" | "checkbox" | "dropdown" | "groupbox" | "label" | "listview" | "spinner" | "viewport"; interface Widget { type: WidgetType; @@ -1113,6 +1113,42 @@ declare global { onChange: (index: number) => void; } + type SortOrder = "none" | "ascending" | "descending"; + + type ScrollbarType = "none" | "horizontal" | "vertical" | "both"; + + interface ListViewColumn { + canSort?: boolean; + sortOrder?: SortOrder; + header?: string; + headerTooltip?: string; + width?: number; + ratioWidth?: number; + minWidth?: number; + maxWidth?: number; + } + + type ListViewItem = string[]; + + interface RowColumn { + row: number; + column: number; + } + + interface ListView extends Widget { + scrollbars?: ScrollbarType; + isStriped?: boolean; + showColumnHeaders?: boolean; + columns?: ListViewColumn[]; + items?: string[] | ListViewItem[]; + selectedCell?: RowColumn; + readonly highlightedCell?: RowColumn; + canSelect?: boolean; + + onHighlight: (item: number, column: number) => void; + onClick: (item: number, column: number) => void; + } + interface SpinnerWidget extends Widget { text: string; onDecrement: () => void; diff --git a/src/openrct2-ui/scripting/CustomListView.cpp b/src/openrct2-ui/scripting/CustomListView.cpp new file mode 100644 index 0000000000..397b6a1d44 --- /dev/null +++ b/src/openrct2-ui/scripting/CustomListView.cpp @@ -0,0 +1,683 @@ +/***************************************************************************** + * 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/Widget.h" +# include "../interface/Window.h" + +# include +# 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<> DukValue ToDuk(duk_context* ctx, const ColumnSortOrder& value) + { + switch (value) + { + case ColumnSortOrder::Ascending: + return ToDuk(ctx, "ascending"); + case ColumnSortOrder::Descending: + return ToDuk(ctx, "descending"); + default: + return ToDuk(ctx, "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<> DukValue ToDuk(duk_context* ctx, const ListViewColumn& value) + { + DukObject obj(ctx); + obj.Set("canSort", value.CanSort); + obj.Set("sortOrder", ToDuk(ctx, value.SortOrder)); + obj.Set("header", value.Header); + obj.Set("headerTooltip", value.HeaderTooltip); + obj.Set("minWidth", value.MinWidth); + obj.Set("maxWidth", value.MaxWidth); + obj.Set("ratioWidth", value.RatioWidth); + obj.Set("width", value.Width); + return obj.Take(); + } + + 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; + } + + template<> std::vector FromDuk(const DukValue& d) + { + std::vector result; + if (d.is_array()) + { + auto dukColumns = d.as_array(); + for (const auto& dukColumn : dukColumns) + { + result.push_back(FromDuk(dukColumn)); + } + } + return result; + } + + template<> std::vector FromDuk(const DukValue& d) + { + std::vector result; + if (d.is_array()) + { + auto dukItems = d.as_array(); + for (const auto& dukItem : dukItems) + { + result.push_back(FromDuk(dukItem)); + } + } + return result; + } + + template<> std::optional FromDuk(const DukValue& d) + { + if (d.type() == DukValue::Type::OBJECT) + { + auto dukRow = d["row"]; + auto dukColumn = d["column"]; + if (dukRow.type() == DukValue::Type::NUMBER && dukColumn.type() == DukValue::Type::NUMBER) + { + return RowColumn(dukRow.as_int(), dukColumn.as_int()); + } + } + return std::nullopt; + } + + template<> DukValue ToDuk(duk_context* ctx, const RowColumn& value) + { + DukObject obj(ctx); + obj.Set("row", value.Row); + obj.Set("column", value.Column); + return obj.Take(); + } + + template<> ScrollbarType FromDuk(const DukValue& d) + { + auto value = AsOrDefault(d, ""); + if (value == "horizontal") + return ScrollbarType::Horizontal; + if (value == "vertical") + return ScrollbarType::Vertical; + if (value == "both") + return ScrollbarType::Both; + return ScrollbarType::None; + } + + template<> DukValue ToDuk(duk_context* ctx, const ScrollbarType& value) + { + switch (value) + { + default: + case ScrollbarType::None: + return ToDuk(ctx, "none"); + case ScrollbarType::Horizontal: + return ToDuk(ctx, "horizontal"); + case ScrollbarType::Vertical: + return ToDuk(ctx, "vertical"); + case ScrollbarType::Both: + return ToDuk(ctx, "both"); + } + } + +} // namespace OpenRCT2::Scripting + +CustomListView::CustomListView(rct_window* parent, size_t scrollIndex) + : ParentWindow(parent) + , ScrollIndex(scrollIndex) +{ +} + +ScrollbarType CustomListView::GetScrollbars() const +{ + return Scrollbars; +} + +void CustomListView::SetScrollbars(ScrollbarType value, bool initialising) +{ + Scrollbars = value; + + if (!initialising) + { + size_t scrollIndex = 0; + for (auto widget = ParentWindow->widgets; widget->type != WWT_LAST; widget++) + { + if (widget->type == WWT_SCROLL) + { + if (scrollIndex == ScrollIndex) + { + if (value == ScrollbarType::Horizontal) + widget->content = SCROLL_HORIZONTAL; + else if (value == ScrollbarType::Vertical) + widget->content = SCROLL_VERTICAL; + else if (value == ScrollbarType::Both) + widget->content = SCROLL_BOTH; + else + widget->content = 0; + } + scrollIndex++; + } + } + window_init_scroll_widgets(ParentWindow); + } +} + +const std::vector& CustomListView::GetColumns() const +{ + return Columns; +} + +void CustomListView::SetColumns(const std::vector& columns, bool initialising) +{ + SelectedCell = std::nullopt; + Columns = columns; + LastKnownSize = {}; + SortItems(0, ColumnSortOrder::None); + if (!initialising) + window_init_scroll_widgets(ParentWindow); +} + +const std::vector& CustomListView::CustomListView::GetItems() const +{ + return Items; +} + +void CustomListView::SetItems(const std::vector& items, bool initialising) +{ + SelectedCell = std::nullopt; + Items = items; + SortItems(0, ColumnSortOrder::None); + if (!initialising) + window_update_scroll_widgets(ParentWindow); +} + +void CustomListView::SetItems(std::vector&& items, bool initialising) +{ + Items = items; + SortItems(0, ColumnSortOrder::None); + if (!initialising) + window_init_scroll_widgets(ParentWindow); +} + +bool CustomListView::SortItem(size_t indexA, size_t indexB, int32_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(int32_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(int32_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 + bool hasHorizontalScroll = Scrollbars == ScrollbarType::Horizontal || Scrollbars == ScrollbarType::Both; + int32_t widthRemaining = size.width; + for (size_t c = 0; c < Columns.size(); c++) + { + auto& column = Columns[c]; + auto isLastColumn = c == Columns.size() - 1; + if (!hasHorizontalScroll && isLastColumn) + { + column.Width = widthRemaining; + } + else + { + column.Width = 0; + if (column.RatioWidth && *column.RatioWidth > 0) + { + if (isLastColumn) + { + column.Width = widthRemaining; + } + else + { + 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 = std::max(0, widthRemaining - column.Width); + } + + window_init_scroll_widgets(ParentWindow); +} + +ScreenSize CustomListView::GetSize() +{ + LastHighlightedCell = HighlightedCell; + HighlightedCell = std::nullopt; + ColumnHeaderPressedCurrentState = false; + LastIsMouseDown = IsMouseDown; + IsMouseDown = false; + + ScreenSize result; + if (Scrollbars == ScrollbarType::Horizontal || Scrollbars == ScrollbarType::Both) + { + result.width = std::accumulate( + Columns.begin(), Columns.end(), 0, [](int32_t acc, const ListViewColumn& column) { return acc + column.Width; }); + + // Fixes an off-by-one error that causes the scrollbar thumb to not fill when the widget is wide enough + result.width--; + } + if (Scrollbars == ScrollbarType::Vertical || Scrollbars == ScrollbarType::Both) + { + 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 (isSelected) + { + gfx_filter_rect(dpi, dpi->x, y, dpi->x + dpi->width, y + (LIST_ROW_HEIGHT - 1), PALETTE_DARKEN_2); + } + else if (isHighlighted) + { + gfx_filter_rect(dpi, dpi->x, y, dpi->x + dpi->width, y + (LIST_ROW_HEIGHT - 1), PALETTE_DARKEN_1); + } + 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 (int32_t j = 0; j < static_cast(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..a6cef7d641 --- /dev/null +++ b/src/openrct2-ui/scripting/CustomListView.h @@ -0,0 +1,170 @@ +/***************************************************************************** + * 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; + + rct_window* ParentWindow{}; + size_t ScrollIndex{}; + std::vector Columns; + std::vector Items; + ScrollbarType Scrollbars = ScrollbarType::Vertical; + + public: + std::shared_ptr Owner; + std::vector SortedItems; + std::optional HighlightedCell; + std::optional LastHighlightedCell; + std::optional SelectedCell; + std::optional ColumnHeaderPressed; + bool ColumnHeaderPressedCurrentState{}; + bool ShowColumnHeaders{}; + bool IsStriped{}; + ScreenSize LastKnownSize; + ColumnSortOrder CurrentSortOrder{}; + int32_t CurrentSortColumn{}; + bool LastIsMouseDown{}; + bool IsMouseDown{}; + bool CanSelect{}; + + DukValue OnClick; + DukValue OnHighlight; + + CustomListView(rct_window* parent, size_t scrollIndex); + ScrollbarType GetScrollbars() const; + void SetScrollbars(ScrollbarType value, bool initialising = false); + const std::vector& GetColumns() const; + void SetColumns(const std::vector& columns, bool initialising = false); + const std::vector& GetItems() const; + void SetItems(const std::vector& items, bool initialising = false); + void SetItems(std::vector&& items, bool initialising = false); + bool SortItem(size_t indexA, size_t indexB, int32_t column); + void SortItems(int32_t column); + void SortItems(int32_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); + template<> std::vector FromDuk(const DukValue& d); + template<> std::vector FromDuk(const DukValue& d); + template<> std::optional FromDuk(const DukValue& d); + template<> DukValue ToDuk(duk_context* ctx, const RowColumn& value); + template<> DukValue ToDuk(duk_context* ctx, const ListViewColumn& value); + template<> ScrollbarType FromDuk(const DukValue& d); + template<> DukValue ToDuk(duk_context* ctx, const ScrollbarType& value); +} // namespace OpenRCT2::Scripting + +#endif diff --git a/src/openrct2-ui/scripting/CustomWindow.cpp b/src/openrct2-ui/scripting/CustomWindow.cpp index 9681fdbadc..26e9899fc5 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" @@ -54,8 +55,13 @@ namespace OpenRCT2::Ui::Windows static void window_custom_resize(rct_window* w); static void window_custom_dropdown(rct_window* w, rct_widgetindex widgetIndex, int32_t dropdownIndex); static void window_custom_update(rct_window* w); + static void window_custom_scrollgetsize(rct_window* w, int32_t scrollIndex, int32_t* width, int32_t* height); + static void window_custom_scrollmousedrag(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords); + static void window_custom_scrollmousedown(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords); + static void window_custom_scrollmouseover(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords); static void window_custom_invalidate(rct_window* w); static void window_custom_paint(rct_window* w, rct_drawpixelinfo* dpi); + static void window_custom_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi, int32_t scrollIndex); static void window_custom_update_viewport(rct_window* w); static rct_window_event_list window_custom_events = { window_custom_close, @@ -73,10 +79,10 @@ namespace OpenRCT2::Ui::Windows nullptr, nullptr, nullptr, - nullptr, - nullptr, - nullptr, - nullptr, + window_custom_scrollgetsize, + window_custom_scrollmousedown, + window_custom_scrollmousedrag, + window_custom_scrollmouseover, nullptr, nullptr, nullptr, @@ -85,7 +91,7 @@ namespace OpenRCT2::Ui::Windows nullptr, window_custom_invalidate, window_custom_paint, - nullptr }; + window_custom_scrollpaint }; struct CustomWidgetDesc { @@ -100,23 +106,23 @@ namespace OpenRCT2::Ui::Windows std::string Text; std::string Tooltip; std::vector Items; + std::vector ListViewItems; + std::vector ListViewColumns; + ScrollbarType Scrollbars{}; int32_t SelectedIndex{}; bool IsChecked{}; bool IsDisabled{}; bool HasBorder{}; + bool ShowColumnHeaders{}; + bool IsStriped{}; + bool CanSelect{}; // Event handlers DukValue OnClick; DukValue OnChange; DukValue OnIncrement; DukValue OnDecrement; - - static std::string ProcessString(const DukValue& value) - { - if (value.type() == DukValue::Type::STRING) - return language_convert_string(value.as_string()); - return {}; - } + DukValue OnHighlight; static CustomWidgetDesc FromDukValue(DukValue desc) { @@ -157,11 +163,6 @@ namespace OpenRCT2::Ui::Windows } else if (result.Type == "dropdown") { - auto dukItems = desc["items"].as_array(); - for (const auto& dukItem : dukItems) - { - result.Items.push_back(ProcessString(dukItem)); - } result.SelectedIndex = desc["selectedIndex"].as_int(); result.OnChange = desc["onChange"]; } @@ -169,6 +170,20 @@ namespace OpenRCT2::Ui::Windows { result.Text = ProcessString(desc["text"]); } + else if (result.Type == "listview") + { + result.ListViewColumns = FromDuk>(desc["columns"]); + result.ListViewItems = FromDuk>(desc["items"]); + result.ShowColumnHeaders = AsOrDefault(desc["showColumnHeaders"], false); + result.IsStriped = AsOrDefault(desc["isStriped"], false); + result.OnClick = desc["onClick"]; + result.OnHighlight = desc["onHighlight"]; + result.CanSelect = AsOrDefault(desc["canSelect"], false); + if (desc["scrollbars"].type() == DukValue::UNDEFINED) + result.Scrollbars = ScrollbarType::Vertical; + else + result.Scrollbars = FromDuk(desc["scrollbars"]); + } else if (result.Type == "spinner") { result.Text = ProcessString(desc["text"]); @@ -312,6 +327,7 @@ namespace OpenRCT2::Ui::Windows CustomWindowDesc Desc; std::vector Widgets; std::vector WidgetIndexMap; + std::vector ListViews; CustomWindowInfo(std::shared_ptr owner, const CustomWindowDesc& desc) : Owner(owner) @@ -401,8 +417,6 @@ namespace OpenRCT2::Ui::Windows window->max_height = desc.MaxHeight.value_or(std::numeric_limits::max()); } RefreshWidgets(window); - window_init_scroll_widgets(window); - window_custom_update_viewport(window); return window; } @@ -576,6 +590,44 @@ 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 < static_cast(info.ListViews.size())) + { + auto size = info.ListViews[scrollIndex].GetSize(); + *width = size.width; + *height = size.height; + } + } + + static void window_custom_scrollmousedown(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords) + { + auto& info = GetInfo(w); + if (scrollIndex < static_cast(info.ListViews.size())) + { + info.ListViews[scrollIndex].MouseDown(screenCoords); + } + } + + static void window_custom_scrollmousedrag(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords) + { + auto& info = GetInfo(w); + if (scrollIndex < static_cast(info.ListViews.size())) + { + info.ListViews[scrollIndex].MouseOver(screenCoords, true); + } + } + + static void window_custom_scrollmouseover(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords) + { + auto& info = GetInfo(w); + if (scrollIndex < static_cast(info.ListViews.size())) + { + info.ListViews[scrollIndex].MouseOver(screenCoords, false); + } + } + static void window_custom_set_pressed_tab(rct_window* w) { const auto& info = GetInfo(w); @@ -604,6 +656,28 @@ namespace OpenRCT2::Ui::Windows const auto& desc = GetInfo(w).Desc; set_format_arg(0, void*, desc.Title.c_str()); + + auto& info = GetInfo(w); + size_t scrollIndex = 0; + for (auto widget = w->widgets; widget->type != WWT_LAST; widget++) + { + if (widget->type == WWT_SCROLL) + { + auto& listView = info.ListViews[scrollIndex]; + auto width = widget->right - widget->left + 1 - 2; + auto height = widget->bottom - widget->top + 1 - 2; + if (listView.GetScrollbars() == ScrollbarType::Horizontal || listView.GetScrollbars() == ScrollbarType::Both) + { + height -= SCROLLBAR_WIDTH + 1; + } + if (listView.GetScrollbars() == ScrollbarType::Vertical || listView.GetScrollbars() == ScrollbarType::Both) + { + width -= SCROLLBAR_WIDTH + 1; + } + listView.Resize({ width, height }); + scrollIndex++; + } + } } static void window_custom_draw_tab_images(rct_window* w, rct_drawpixelinfo* dpi) @@ -642,6 +716,15 @@ 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 < static_cast(info.ListViews.size())) + { + info.ListViews[scrollIndex].Paint(w, dpi, &w->scrolls[scrollIndex]); + } + } + static std::optional GetViewportWidgetIndex(rct_window* w) { rct_widgetindex widgetIndex = 0; @@ -752,6 +835,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); @@ -782,6 +869,18 @@ namespace OpenRCT2::Ui::Windows widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; widgetList.push_back(widget); } + else if (desc.Type == "listview") + { + widget.type = WWT_SCROLL; + widget.content = 0; + if (desc.Scrollbars == ScrollbarType::Horizontal) + widget.content = SCROLL_HORIZONTAL; + else if (desc.Scrollbars == ScrollbarType::Vertical) + widget.content = SCROLL_VERTICAL; + else if (desc.Scrollbars == ScrollbarType::Both) + widget.content = SCROLL_BOTH; + widgetList.push_back(widget); + } else if (desc.Type == "spinner") { widget.type = WWT_SPINNER; @@ -827,6 +926,7 @@ namespace OpenRCT2::Ui::Windows widgets.clear(); info.WidgetIndexMap.clear(); + info.ListViews.clear(); // Add default widgets (window shim) widgets.insert(widgets.begin(), std::begin(CustomDefaultWidgets), std::end(CustomDefaultWidgets)); @@ -877,6 +977,20 @@ namespace OpenRCT2::Ui::Windows { info.WidgetIndexMap.push_back(widgetDescIndex); } + + if (widgetDesc.Type == "listview") + { + CustomListView listView(w, info.ListViews.size()); + listView.SetScrollbars(widgetDesc.Scrollbars, true); + listView.SetColumns(widgetDesc.ListViewColumns, true); + listView.SetItems(widgetDesc.ListViewItems, true); + listView.ShowColumnHeaders = widgetDesc.ShowColumnHeaders; + listView.IsStriped = widgetDesc.IsStriped; + listView.OnClick = widgetDesc.OnClick; + listView.OnHighlight = widgetDesc.OnHighlight; + listView.CanSelect = widgetDesc.CanSelect; + info.ListViews.push_back(std::move(listView)); + } } for (size_t i = firstCustomWidgetIndex; i < widgets.size(); i++) @@ -899,6 +1013,9 @@ namespace OpenRCT2::Ui::Windows widgets.push_back({ WIDGETS_END }); w->widgets = widgets.data(); + + window_init_scroll_widgets(w); + window_custom_update_viewport(w); } static void InvokeEventHandler(const std::shared_ptr& owner, const DukValue& dukHandler) @@ -984,6 +1101,47 @@ namespace OpenRCT2::Ui::Windows return std::nullopt; } + std::string GetWidgetName(rct_window* w, rct_widgetindex widgetIndex) + { + if (w->custom_info != nullptr) + { + const auto& customInfo = GetInfo(w); + auto customWidgetInfo = customInfo.GetCustomWidgetDesc(w, widgetIndex); + if (customWidgetInfo != nullptr) + { + return customWidgetInfo->Name; + } + } + return {}; + } + + void SetWidgetName(rct_window* w, rct_widgetindex widgetIndex, const std::string_view& name) + { + if (w->custom_info != nullptr) + { + auto& customInfo = GetInfo(w); + auto customWidgetInfo = customInfo.GetCustomWidgetDesc(w, widgetIndex); + if (customWidgetInfo != nullptr) + { + customWidgetInfo->Name = std::string(name); + } + } + } + + CustomListView* GetCustomListView(rct_window* w, rct_widgetindex widgetIndex) + { + if (w->custom_info != nullptr) + { + auto& info = GetInfo(w); + auto scrollIndex = window_get_scroll_data_index(w, widgetIndex); + if (scrollIndex < static_cast(info.ListViews.size())) + { + return &info.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..a08a89984e 100644 --- a/src/openrct2-ui/scripting/CustomWindow.h +++ b/src/openrct2-ui/scripting/CustomWindow.h @@ -18,11 +18,16 @@ 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); + std::string GetWidgetName(rct_window* w, rct_widgetindex widgetIndex); + void SetWidgetName(rct_window* w, rct_widgetindex widgetIndex, 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 ee112cfbed..e9162117b3 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" @@ -26,7 +27,7 @@ namespace OpenRCT2::Scripting { class ScWidget { - private: + protected: rct_windowclass _class{}; rct_windownumber _number{}; rct_widgetindex _widgetIndex{}; @@ -39,7 +40,28 @@ namespace OpenRCT2::Scripting { } + static DukValue ToDukValue(duk_context* ctx, rct_window* w, rct_widgetindex widgetIndex); + private: + std::string name_get() const + { + auto w = GetWindow(); + if (w != nullptr && IsCustomWindow()) + { + return OpenRCT2::Ui::Windows::GetWidgetName(w, _widgetIndex); + } + return {}; + } + + void name_set(const std::string& value) + { + auto w = GetWindow(); + if (w != nullptr && IsCustomWindow()) + { + OpenRCT2::Ui::Windows::SetWidgetName(w, _widgetIndex, value); + } + } + std::string type_get() const { auto widget = GetWidget(); @@ -190,24 +212,6 @@ namespace OpenRCT2::Scripting } } - bool isChecked_get() const - { - auto w = GetWindow(); - if (w != nullptr) - { - return widget_is_pressed(w, _widgetIndex); - } - return false; - } - void isChecked_set(bool value) - { - auto w = GetWindow(); - if (w != nullptr) - { - widget_set_checkbox_value(w, _widgetIndex, value ? 1 : 0); - } - } - uint32_t image_get() const { if (IsCustomWindow()) @@ -268,6 +272,7 @@ namespace OpenRCT2::Scripting static void Register(duk_context* ctx) { // Common + dukglue_register_property(ctx, &ScWidget::name_get, &ScWidget::name_set, "name"); dukglue_register_property(ctx, &ScWidget::type_get, nullptr, "type"); dukglue_register_property(ctx, &ScWidget::x_get, &ScWidget::x_set, "x"); dukglue_register_property(ctx, &ScWidget::y_get, &ScWidget::y_set, "y"); @@ -278,11 +283,10 @@ namespace OpenRCT2::Scripting // No so common dukglue_register_property(ctx, &ScWidget::image_get, &ScWidget::image_set, "image"); dukglue_register_property(ctx, &ScWidget::text_get, &ScWidget::text_set, "text"); - dukglue_register_property(ctx, &ScWidget::isChecked_get, &ScWidget::isChecked_set, "isChecked"); dukglue_register_property(ctx, &ScWidget::viewport_get, nullptr, "viewport"); } - private: + protected: rct_window* GetWindow() const { if (_class == WC_MAIN_WINDOW) @@ -316,6 +320,248 @@ namespace OpenRCT2::Scripting widget_invalidate_by_number(_class, _number, _widgetIndex); } }; + + class ScCheckBoxWidget : public ScWidget + { + public: + ScCheckBoxWidget(rct_windowclass c, rct_windownumber n, rct_widgetindex widgetIndex) + : ScWidget(c, n, widgetIndex) + { + } + + static void Register(duk_context* ctx) + { + dukglue_set_base_class(ctx); + dukglue_register_property(ctx, &ScCheckBoxWidget::isChecked_get, &ScCheckBoxWidget::isChecked_set, "isChecked"); + } + + private: + bool isChecked_get() const + { + auto w = GetWindow(); + if (w != nullptr) + { + return widget_is_pressed(w, _widgetIndex); + } + return false; + } + void isChecked_set(bool value) + { + auto w = GetWindow(); + if (w != nullptr) + { + widget_set_checkbox_value(w, _widgetIndex, value ? 1 : 0); + } + } + }; + + class ScListViewWidget : public ScWidget + { + public: + ScListViewWidget(rct_windowclass c, rct_windownumber n, rct_widgetindex widgetIndex) + : ScWidget(c, n, widgetIndex) + { + } + + static void Register(duk_context* ctx) + { + dukglue_set_base_class(ctx); + dukglue_register_property(ctx, &ScListViewWidget::canSelect_get, &ScListViewWidget::canSelect_set, "canSelect"); + dukglue_register_property(ctx, &ScListViewWidget::isStriped_get, &ScListViewWidget::isStriped_set, "isStriped"); + dukglue_register_property(ctx, &ScListViewWidget::scrollbars_get, &ScListViewWidget::scrollbars_set, "scrollbars"); + dukglue_register_property( + ctx, &ScListViewWidget::showColumnHeaders_get, &ScListViewWidget::showColumnHeaders_set, "showColumnHeaders"); + dukglue_register_property(ctx, &ScListViewWidget::highlightedCell_get, nullptr, "highlightedCell"); + dukglue_register_property( + ctx, &ScListViewWidget::selectedCell_get, &ScListViewWidget::selectedCell_set, "selectedCell"); + dukglue_register_property(ctx, &ScListViewWidget::columns_get, &ScListViewWidget::columns_set, "columns"); + dukglue_register_property(ctx, &ScListViewWidget::items_get, &ScListViewWidget::items_set, "items"); + } + + private: + bool canSelect_get() const + { + auto listView = GetListView(); + if (listView != nullptr) + { + return listView->CanSelect; + } + return false; + } + + void canSelect_set(bool value) + { + auto listView = GetListView(); + if (listView != nullptr) + { + listView->CanSelect = value; + } + } + + 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; + } + } + + DukValue scrollbars_get() const + { + auto ctx = GetContext()->GetScriptEngine().GetContext(); + auto scrollType = ScrollbarType::None; + auto listView = GetListView(); + if (listView != nullptr) + { + scrollType = listView->GetScrollbars(); + } + return ToDuk(ctx, scrollType); + } + + void scrollbars_set(const DukValue& value) + { + auto listView = GetListView(); + if (listView != nullptr) + { + listView->SetScrollbars(FromDuk(value)); + } + } + + bool showColumnHeaders_get() const + { + auto listView = GetListView(); + if (listView != nullptr) + { + return listView->ShowColumnHeaders; + } + return false; + } + + void showColumnHeaders_set(bool value) + { + auto listView = GetListView(); + if (listView != nullptr) + { + listView->ShowColumnHeaders = value; + } + } + + DukValue highlightedCell_get() + { + auto ctx = GetContext()->GetScriptEngine().GetContext(); + auto listView = GetListView(); + if (listView != nullptr) + { + return ToDuk(ctx, listView->LastHighlightedCell); + } + return ToDuk(ctx, nullptr); + } + + DukValue selectedCell_get() + { + auto ctx = GetContext()->GetScriptEngine().GetContext(); + auto listView = GetListView(); + if (listView != nullptr) + { + return ToDuk(ctx, listView->SelectedCell); + } + return ToDuk(ctx, nullptr); + } + + void selectedCell_set(const DukValue& value) + { + auto listView = GetListView(); + if (listView != nullptr) + { + listView->SelectedCell = FromDuk>(value); + } + } + + std::vector> items_get() + { + std::vector> result; + auto listView = GetListView(); + if (listView != nullptr) + { + for (const auto& item : listView->GetItems()) + { + result.push_back(item.Cells); + } + } + return result; + } + + void items_set(const DukValue& value) + { + auto listView = GetListView(); + if (listView != nullptr) + { + listView->SetItems(FromDuk>(value)); + } + } + + std::vector columns_get() + { + std::vector result; + auto listView = GetListView(); + if (listView != nullptr) + { + auto ctx = GetContext()->GetScriptEngine().GetContext(); + for (const auto& column : listView->GetColumns()) + { + result.push_back(ToDuk(ctx, column)); + } + } + return result; + } + + void columns_set(const DukValue& value) + { + auto listView = GetListView(); + if (listView != nullptr) + { + listView->SetColumns(FromDuk>(value)); + } + } + + CustomListView* GetListView() const + { + auto w = GetWindow(); + if (w != nullptr) + { + return GetCustomListView(w, _widgetIndex); + } + return nullptr; + } + }; + + inline DukValue ScWidget::ToDukValue(duk_context* ctx, rct_window* w, rct_widgetindex widgetIndex) + { + const auto& widget = w->widgets[widgetIndex]; + auto c = w->classification; + auto n = w->number; + switch (widget.type) + { + case WWT_CHECKBOX: + return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); + case WWT_SCROLL: + return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); + default: + return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); + } + } + } // namespace OpenRCT2::Scripting #endif diff --git a/src/openrct2-ui/scripting/ScWindow.hpp b/src/openrct2-ui/scripting/ScWindow.hpp index 1efab2166c..106b82281b 100644 --- a/src/openrct2-ui/scripting/ScWindow.hpp +++ b/src/openrct2-ui/scripting/ScWindow.hpp @@ -177,16 +177,18 @@ namespace OpenRCT2::Scripting return (flags & (WF_STICK_TO_BACK | WF_STICK_TO_FRONT)) != 0; } - std::vector> widgets_get() const + std::vector widgets_get() const { - std::vector> result; + auto ctx = GetContext()->GetScriptEngine().GetContext(); + + std::vector result; auto w = GetWindow(); if (w != nullptr) { rct_widgetindex widgetIndex = 0; for (auto widget = w->widgets; widget->type != WWT_LAST; widget++) { - result.push_back(std::make_shared(_class, _number, widgetIndex)); + result.push_back(ScWidget::ToDukValue(ctx, w, widgetIndex)); widgetIndex++; } } @@ -257,18 +259,19 @@ namespace OpenRCT2::Scripting } } - std::shared_ptr findWidget(std::string name) const + DukValue findWidget(std::string name) const { + auto ctx = GetContext()->GetScriptEngine().GetContext(); auto w = GetWindow(); if (w != nullptr) { auto widgetIndex = FindWidgetIndexByName(w, name); if (widgetIndex) { - return std::make_shared(_class, _number, *widgetIndex); + return ScWidget::ToDukValue(ctx, w, *widgetIndex); } } - return {}; + return GetObjectAsDukValue(ctx, nullptr); } void bringToFront() diff --git a/src/openrct2-ui/scripting/UiExtensions.cpp b/src/openrct2-ui/scripting/UiExtensions.cpp index 54a00df453..446412c8ad 100644 --- a/src/openrct2-ui/scripting/UiExtensions.cpp +++ b/src/openrct2-ui/scripting/UiExtensions.cpp @@ -32,6 +32,8 @@ void UiScriptExtensions::Extend(ScriptEngine& scriptEngine) ScUi::Register(ctx); ScViewport::Register(ctx); ScWidget::Register(ctx); + ScCheckBoxWidget::Register(ctx); + ScListViewWidget::Register(ctx); ScWindow::Register(ctx); InitialiseCustomMenuItems(scriptEngine); diff --git a/src/openrct2/scripting/Duktape.hpp b/src/openrct2/scripting/Duktape.hpp index 43d6ba3b53..c3b0e8bf0a 100644 --- a/src/openrct2/scripting/Duktape.hpp +++ b/src/openrct2/scripting/Duktape.hpp @@ -113,6 +113,20 @@ namespace OpenRCT2::Scripting duk_put_prop_string(_ctx, _idx, name); } + template void Set(const char* name, const std::optional& value) + { + if (value) + { + EnsureObjectPushed(); + duk_push_null(_ctx); + duk_put_prop_string(_ctx, _idx, name); + } + else + { + Set(name, *value); + } + } + DukValue Take() { EnsureObjectPushed(); @@ -190,6 +204,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; @@ -205,6 +221,17 @@ namespace OpenRCT2::Scripting return DukValue::take_from_stack(ctx); } + template inline DukValue ToDuk(duk_context* ctx, const char (&value)[TLen]) + { + duk_push_string(ctx, value); + return DukValue::take_from_stack(ctx); + } + + template DukValue ToDuk(duk_context* ctx, const std::optional& value) + { + return value ? ToDuk(ctx, *value) : ToDuk(ctx, nullptr); + } + } // namespace OpenRCT2::Scripting #endif diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 420ec44932..cecf644e19 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -1140,6 +1140,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 diff --git a/src/openrct2/world/Location.hpp b/src/openrct2/world/Location.hpp index 22aa3243e9..d2222e66d6 100644 --- a/src/openrct2/world/Location.hpp +++ b/src/openrct2/world/Location.hpp @@ -85,6 +85,29 @@ struct ScreenCoordsXY } }; +struct ScreenSize +{ + int32_t width{}; + int32_t height{}; + + ScreenSize() = default; + constexpr ScreenSize(int32_t _width, int32_t _height) + : width(_width) + , height(_height) + { + } + + bool operator==(const ScreenSize& other) const + { + return width == other.width && height == other.height; + } + + bool operator!=(const ScreenSize& other) const + { + return !(*this == other); + } +}; + /** * Tile coordinates use 1 x/y increment per tile and 1 z increment per step. * Regular ('big', 'sprite') coordinates use 32 x/y increments per tile and 8 z increments per step.