From 928bba97952f2bc1a4a018bc2bdc19299c46283f Mon Sep 17 00:00:00 2001 From: Ted John Date: Sun, 3 May 2020 16:29:15 +0100 Subject: [PATCH] Implement sorting --- distribution/openrct2.d.ts | 2 +- src/openrct2-ui/scripting/CustomWindow.cpp | 260 ++++++++++++++++----- 2 files changed, 197 insertions(+), 65 deletions(-) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 3096912c53..f21db9fb60 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -1099,7 +1099,7 @@ declare global { highlightedIndex?: number; onHighlight: (index: number) => void; - onSelect: (index: number) => void; + onClick: (index: number) => void; getCell(row: number, column: number): string; setCell(row: number, column: number, value: string): void; diff --git a/src/openrct2-ui/scripting/CustomWindow.cpp b/src/openrct2-ui/scripting/CustomWindow.cpp index 56f0692f4e..a1be644d16 100644 --- a/src/openrct2-ui/scripting/CustomWindow.cpp +++ b/src/openrct2-ui/scripting/CustomWindow.cpp @@ -22,6 +22,7 @@ # include # include # include +# include # include # include # include @@ -122,7 +123,7 @@ namespace OpenRCT2::Scripting result.MaxWidth = result.MinWidth; result.RatioWidth = std::nullopt; } - else + else if (!result.RatioWidth) { result.RatioWidth = 1; } @@ -174,6 +175,7 @@ namespace OpenRCT2::Ui::Windows 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); @@ -198,7 +200,7 @@ namespace OpenRCT2::Ui::Windows nullptr, window_custom_scrollgetsize, window_custom_scrollmousedown, - nullptr, + window_custom_scrollmousedrag, window_custom_scrollmouseover, nullptr, nullptr, @@ -237,6 +239,7 @@ namespace OpenRCT2::Ui::Windows DukValue OnChange; DukValue OnIncrement; DukValue OnDecrement; + DukValue OnHighlight; static CustomWidgetDesc FromDukValue(DukValue desc) { @@ -304,6 +307,8 @@ namespace OpenRCT2::Ui::Windows } result.ShowColumnHeaders = AsOrDefault(desc["showColumnHeaders"], false); result.IsStriped = AsOrDefault(desc["isStriped"], false); + result.OnClick = desc["onClick"]; + result.OnHighlight = desc["onHighlight"]; } else if (result.Type == "spinner") { @@ -441,38 +446,115 @@ 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: - struct HitTestResult - { - static constexpr size_t HEADER_ROW = std::numeric_limits::max(); - - size_t Row{}; - size_t Column{}; - - bool IsHeader() const - { - return Row == HEADER_ROW; - } - }; + static constexpr int32_t HEADER_ROW = -1; + std::vector Items; public: std::shared_ptr Owner; std::vector Columns; - std::vector Items; - std::optional HighlightedRow; - std::optional HighlightedColumn; - std::optional SelectedRow; - std::optional SelectedColumn; + std::vector SortedItems; + std::optional HighlightedCell; + std::optional LastHighlightedCell; std::optional ColumnHeaderPressed; + bool ColumnHeaderPressedCurrentState{}; bool ShowColumnHeaders{}; bool IsStriped{}; ScreenSize LastKnownSize; ScrollbarType Scrollbars = ScrollbarType::Vertical; + ColumnSortOrder CurrentSortOrder{}; + size_t CurrentSortColumn{}; + bool LastIsMouseDown{}; + bool IsMouseDown{}; + DukValue OnClick; DukValue OnHighlight; - DukValue OnSelect; + + 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) { @@ -503,7 +585,8 @@ namespace OpenRCT2::Ui::Windows } else { - if (column.RatioWidth) + column.Width = 0; + if (column.RatioWidth && *column.RatioWidth > 0) { column.Width = (size.width * *column.RatioWidth) / totalRatio; } @@ -520,36 +603,58 @@ namespace OpenRCT2::Ui::Windows } } - ScreenSize GetSize() const + 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) + void MouseOver(const ScreenCoordsXY& pos, bool isMouseDown) { auto hitResult = GetItemIndexAt(pos); if (hitResult) { - if (HighlightedRow != hitResult->Row || HighlightedColumn != hitResult->Column) + HighlightedCell = hitResult; + if (HighlightedCell != LastHighlightedCell) { - HighlightedRow = hitResult->Row; - HighlightedColumn = hitResult->Column; - if (!hitResult->IsHeader() && OnHighlight.context() != nullptr && OnHighlight.is_function()) + if (hitResult->Row != HEADER_ROW && OnHighlight.context() != nullptr && OnHighlight.is_function()) { auto ctx = OnHighlight.context(); - duk_push_int(ctx, static_cast(*HighlightedRow)); + duk_push_int(ctx, static_cast(HighlightedCell->Row)); auto dukRow = DukValue::take_from_stack(ctx, -1); - duk_push_int(ctx, static_cast(*HighlightedColumn)); + 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); } } } - ColumnHeaderPressed = std::nullopt; + + // 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) @@ -557,30 +662,41 @@ namespace OpenRCT2::Ui::Windows auto hitResult = GetItemIndexAt(pos); if (hitResult) { - if (SelectedRow != hitResult->Row || SelectedColumn != hitResult->Column) + if (hitResult->Row != HEADER_ROW && OnClick.context() != nullptr && OnClick.is_function()) { - SelectedRow = hitResult->Row; - SelectedColumn = hitResult->Column; - if (!hitResult->IsHeader() && OnSelect.context() != nullptr && OnSelect.is_function()) - { - auto ctx = OnSelect.context(); - duk_push_int(ctx, static_cast(*SelectedRow)); - auto dukRow = DukValue::take_from_stack(ctx, -1); - duk_push_int(ctx, static_cast(*SelectedColumn)); - auto dukColumn = DukValue::take_from_stack(ctx, -1); - auto& scriptEngine = GetContext()->GetScriptEngine(); - scriptEngine.ExecutePluginCall(Owner, OnSelect, { dukRow, dukColumn }, false); - } + 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->IsHeader()) + if (hitResult && hitResult->Row == HEADER_ROW) { - ColumnHeaderPressed = hitResult->Column; + if (Columns[hitResult->Column].CanSort) + { + ColumnHeaderPressed = hitResult->Column; + ColumnHeaderPressedCurrentState = true; + } } - else + IsMouseDown = true; + } + + void MouseUp(const ScreenCoordsXY& pos) + { + auto hitResult = GetItemIndexAt(pos); + if (hitResult && hitResult->Row == HEADER_ROW) { - ColumnHeaderPressed = std::nullopt; + 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 @@ -601,7 +717,7 @@ namespace OpenRCT2::Ui::Windows { // Background colour auto isStriped = IsStriped && (i & 1); - auto isHighlighted = i == HighlightedRow; + auto isHighlighted = (HighlightedCell && i == HighlightedCell->Row); if (isHighlighted) { gfx_filter_rect(dpi, dpi->x, y, dpi->x + dpi->width, y + (LIST_ROW_HEIGHT - 1), PALETTE_DARKEN_1); @@ -614,7 +730,8 @@ namespace OpenRCT2::Ui::Windows } // Columns - const auto& item = Items[i]; + const auto& itemIndex = SortedItems[i]; + const auto& item = Items[itemIndex]; if (Columns.size() == 0) { const auto& text = item.Cells[0]; @@ -661,10 +778,14 @@ namespace OpenRCT2::Ui::Windows auto columnWidth = column.Width; if (columnWidth != 0) { - bool isPressed = ColumnHeaderPressed == j; - PaintHeading( - w, dpi, { x, y }, { column.Width, LIST_ROW_HEIGHT }, column.Header, ColumnSortOrder::None, - isPressed); + 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; } } @@ -697,7 +818,7 @@ namespace OpenRCT2::Ui::Windows { auto ft = Formatter::Common(); ft.Add(STR_DOWN); - gfx_draw_string_right(dpi, STR_BLACK_STRING, gCommonFormatArgs, COLOUR_BLACK, pos.y + size.width - 1, pos.y); + gfx_draw_string_right(dpi, STR_BLACK_STRING, gCommonFormatArgs, COLOUR_BLACK, pos.x + size.width - 1, pos.y); } } @@ -713,16 +834,16 @@ namespace OpenRCT2::Ui::Windows gfx_draw_string_left_clipped(dpi, stringId, gCommonFormatArgs, COLOUR_BLACK, pos.x, pos.y, size.width); } - std::optional GetItemIndexAt(const ScreenCoordsXY& pos) + std::optional GetItemIndexAt(const ScreenCoordsXY& pos) { - std::optional result; + std::optional result; if (pos.x >= 0) { // Check if we pressed the header if (ShowColumnHeaders && pos.y >= 0 && pos.y < LIST_ROW_HEIGHT) { - result = HitTestResult(); - result->Row = HitTestResult::HEADER_ROW; + result = RowColumn(); + result->Row = HEADER_ROW; } else { @@ -731,8 +852,8 @@ namespace OpenRCT2::Ui::Windows int32_t row = (pos.y - firstY) / LIST_ROW_HEIGHT; if (row >= 0 && row < static_cast(Items.size())) { - result = HitTestResult(); - result->Row = static_cast(row); + result = RowColumn(); + result->Row = row; } } @@ -749,7 +870,7 @@ namespace OpenRCT2::Ui::Windows { if (pos.x < x) { - result->Column = c; + result->Column = static_cast(c); found = true; break; } @@ -1040,7 +1161,7 @@ namespace OpenRCT2::Ui::Windows static void window_custom_scrollgetsize(rct_window* w, int32_t scrollIndex, int32_t* width, int32_t* height) { - const auto& info = GetInfo(w); + auto& info = GetInfo(w); if (scrollIndex < info.ListViews.size()) { auto size = info.ListViews[scrollIndex].GetSize(); @@ -1058,12 +1179,21 @@ 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()) + { + 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 < info.ListViews.size()) { - info.ListViews[scrollIndex].MouseOver(screenCoords); + info.ListViews[scrollIndex].MouseOver(screenCoords, false); } } @@ -1411,9 +1541,11 @@ namespace OpenRCT2::Ui::Windows { CustomListViewInfo listView; listView.Columns = widgetDesc.ListViewColumns; - listView.Items = widgetDesc.ListViewItems; + listView.SetItems(widgetDesc.ListViewItems); listView.ShowColumnHeaders = widgetDesc.ShowColumnHeaders; listView.IsStriped = widgetDesc.IsStriped; + listView.OnClick = widgetDesc.OnClick; + listView.OnHighlight = widgetDesc.OnHighlight; info.ListViews.push_back(std::move(listView)); } }