Implement list view

This commit is contained in:
Ted John 2020-05-02 20:11:07 +01:00
parent 887a86afe9
commit 096de3ccc8
3 changed files with 612 additions and 17 deletions

View File

@ -1031,7 +1031,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;
@ -1072,6 +1072,39 @@ declare global {
onChange: (index: number) => void;
}
type SortOrder = "none" | "ascending" | "descending";
type ScrollType = "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 ListView extends Widget {
scroll?: ScrollType;
isStriped?: boolean;
showColumnHeaders?: boolean;
columns?: ListViewColumn[];
items?: string[] | ListViewItem[];
selectedIndex?: number;
highlightedIndex?: number;
onHighlight: (index: number) => void;
onSelect: (index: number) => void;
getCell(row: number, column: number): string;
setCell(row: number, column: number, value: string): void;
}
interface SpinnerWidget extends Widget {
text: string;
onDecrement: () => void;

View File

@ -30,6 +30,125 @@
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<int32_t> RatioWidth{};
std::optional<int32_t> MinWidth{};
std::optional<int32_t> MaxWidth{};
int32_t Width{};
};
struct ListViewItem
{
std::vector<std::string> Cells;
ListViewItem() = default;
explicit ListViewItem(const std::string_view& text)
{
Cells.emplace_back(text);
}
explicit ListViewItem(std::vector<std::string>&& 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<int32_t> 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<ColumnSortOrder>(d["sortOrder"]);
result.Header = AsOrDefault(d["header"], "");
result.HeaderTooltip = AsOrDefault(d["headerTooltip"], "");
result.MinWidth = FromDuk<std::optional<int32_t>>(d["minWidth"]);
result.MaxWidth = FromDuk<std::optional<int32_t>>(d["maxWidth"]);
result.RatioWidth = FromDuk<std::optional<int32_t>>(d["ratioWidth"]);
if (d["width"].type() == DukValue::Type::NUMBER)
{
result.MinWidth = d["width"].as_int();
result.MaxWidth = result.MinWidth;
result.RatioWidth = std::nullopt;
}
else
{
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<std::string> 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
@ -54,8 +173,12 @@ 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_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 +196,10 @@ namespace OpenRCT2::Ui::Windows
nullptr,
nullptr,
nullptr,
window_custom_scrollgetsize,
window_custom_scrollmousedown,
nullptr,
nullptr,
nullptr,
nullptr,
window_custom_scrollmouseover,
nullptr,
nullptr,
nullptr,
@ -85,7 +208,7 @@ namespace OpenRCT2::Ui::Windows
nullptr,
window_custom_invalidate,
window_custom_paint,
nullptr };
window_custom_scrollpaint };
struct CustomWidgetDesc
{
@ -100,10 +223,14 @@ namespace OpenRCT2::Ui::Windows
std::string Text;
std::string Tooltip;
std::vector<std::string> Items;
std::vector<ListViewItem> ListViewItems;
std::vector<ListViewColumn> ListViewColumns;
int32_t SelectedIndex{};
bool IsChecked{};
bool IsDisabled{};
bool HasBorder{};
bool ShowColumnHeaders{};
bool IsStriped{};
// Event handlers
DukValue OnClick;
@ -111,13 +238,6 @@ namespace OpenRCT2::Ui::Windows
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 {};
}
static CustomWidgetDesc FromDukValue(DukValue desc)
{
CustomWidgetDesc result;
@ -157,11 +277,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 +284,27 @@ namespace OpenRCT2::Ui::Windows
{
result.Text = ProcessString(desc["text"]);
}
else if (result.Type == "listview")
{
if (desc["columns"].is_array())
{
auto dukColumns = desc["columns"].as_array();
for (const auto& dukColumn : dukColumns)
{
result.ListViewColumns.push_back(FromDuk<ListViewColumn>(dukColumn));
}
}
if (desc["items"].is_array())
{
auto dukItems = desc["items"].as_array();
for (const auto& dukItem : dukItems)
{
result.ListViewItems.push_back(FromDuk<ListViewItem>(dukItem));
}
}
result.ShowColumnHeaders = AsOrDefault(desc["showColumnHeaders"], false);
result.IsStriped = AsOrDefault(desc["isStriped"], false);
}
else if (result.Type == "spinner")
{
result.Text = ProcessString(desc["text"]);
@ -305,6 +441,331 @@ namespace OpenRCT2::Ui::Windows
}
};
class CustomListViewInfo
{
private:
struct HitTestResult
{
static constexpr size_t HEADER_ROW = std::numeric_limits<size_t>::max();
size_t Row{};
size_t Column{};
bool IsHeader() const
{
return Row == HEADER_ROW;
}
};
public:
std::shared_ptr<Plugin> Owner;
std::vector<ListViewColumn> Columns;
std::vector<ListViewItem> Items;
std::optional<size_t> HighlightedRow;
std::optional<size_t> HighlightedColumn;
std::optional<size_t> SelectedRow;
std::optional<size_t> SelectedColumn;
std::optional<size_t> ColumnHeaderPressed;
bool ShowColumnHeaders{};
bool IsStriped{};
ScreenSize LastKnownSize;
ScrollbarType Scrollbars = ScrollbarType::Vertical;
DukValue OnHighlight;
DukValue OnSelect;
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
{
if (column.RatioWidth)
{
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() const
{
ScreenSize result;
result.width = 0;
result.height = static_cast<int32_t>(Items.size() * LIST_ROW_HEIGHT);
return result;
}
void MouseOver(const ScreenCoordsXY& pos)
{
auto hitResult = GetItemIndexAt(pos);
if (hitResult)
{
if (HighlightedRow != hitResult->Row || HighlightedColumn != hitResult->Column)
{
HighlightedRow = hitResult->Row;
HighlightedColumn = hitResult->Column;
if (!hitResult->IsHeader() && OnHighlight.context() != nullptr && OnHighlight.is_function())
{
auto ctx = OnHighlight.context();
duk_push_int(ctx, static_cast<int32_t>(*HighlightedRow));
auto dukRow = DukValue::take_from_stack(ctx, -1);
duk_push_int(ctx, static_cast<int32_t>(*HighlightedColumn));
auto dukColumn = DukValue::take_from_stack(ctx, -1);
auto& scriptEngine = GetContext()->GetScriptEngine();
scriptEngine.ExecutePluginCall(Owner, OnHighlight, { dukRow, dukColumn }, false);
}
}
}
ColumnHeaderPressed = std::nullopt;
}
void MouseDown(const ScreenCoordsXY& pos)
{
auto hitResult = GetItemIndexAt(pos);
if (hitResult)
{
if (SelectedRow != hitResult->Row || SelectedColumn != hitResult->Column)
{
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<int32_t>(*SelectedRow));
auto dukRow = DukValue::take_from_stack(ctx, -1);
duk_push_int(ctx, static_cast<int32_t>(*SelectedColumn));
auto dukColumn = DukValue::take_from_stack(ctx, -1);
auto& scriptEngine = GetContext()->GetScriptEngine();
scriptEngine.ExecutePluginCall(Owner, OnSelect, { dukRow, dukColumn }, false);
}
}
}
if (hitResult && hitResult->IsHeader())
{
ColumnHeaderPressed = hitResult->Column;
}
else
{
ColumnHeaderPressed = std::nullopt;
}
}
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)
{
// Background colour
auto isStriped = IsStriped && (i & 1);
auto isHighlighted = i == HighlightedRow;
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
const auto& item = Items[i];
if (Columns.size() == 0)
{
const auto& text = item.Cells[0];
if (!text.empty())
{
ScreenSize cellSize = { std::numeric_limits<int32_t>::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)
{
bool isPressed = ColumnHeaderPressed == j;
PaintHeading(
w, dpi, { x, y }, { column.Width, LIST_ROW_HEIGHT }, column.Header, ColumnSortOrder::None,
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<rct_string_id>(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<rct_string_id>(STR_DOWN);
gfx_draw_string_right(dpi, STR_BLACK_STRING, gCommonFormatArgs, COLOUR_BLACK, pos.y + 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<rct_string_id>(STR_STRING);
ft.Add<const char*>(text);
gfx_draw_string_left_clipped(dpi, stringId, gCommonFormatArgs, COLOUR_BLACK, pos.x, pos.y, size.width);
}
std::optional<HitTestResult> GetItemIndexAt(const ScreenCoordsXY& pos)
{
std::optional<HitTestResult> 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;
}
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<int32_t>(Items.size()))
{
result = HitTestResult();
result->Row = static_cast<size_t>(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 = c;
found = true;
break;
}
}
}
if (!found)
{
// Past all columns
return std::nullopt;
}
}
}
return result;
}
};
class CustomWindowInfo
{
public:
@ -312,6 +773,7 @@ namespace OpenRCT2::Ui::Windows
CustomWindowDesc Desc;
std::vector<rct_widget> Widgets;
std::vector<size_t> WidgetIndexMap;
std::vector<CustomListViewInfo> ListViews;
CustomWindowInfo(std::shared_ptr<Plugin> owner, const CustomWindowDesc& desc)
: Owner(owner)
@ -576,6 +1038,35 @@ 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);
if (scrollIndex < 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 < info.ListViews.size())
{
info.ListViews[scrollIndex].MouseDown(screenCoords);
}
}
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);
}
}
static void window_custom_set_pressed_tab(rct_window* w)
{
const auto& info = GetInfo(w);
@ -604,6 +1095,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.Scrollbars == ScrollbarType::Horizontal || listView.Scrollbars == ScrollbarType::Both)
{
height -= SCROLLBAR_WIDTH + 1;
}
if (listView.Scrollbars == ScrollbarType::Vertical || listView.Scrollbars == 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 +1155,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 < info.ListViews.size())
{
info.ListViews[scrollIndex].Paint(w, dpi, &w->scrolls[scrollIndex]);
}
}
static std::optional<rct_widgetindex> GetViewportWidgetIndex(rct_window* w)
{
rct_widgetindex widgetIndex = 0;
@ -782,6 +1304,12 @@ 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.text = SCROLL_VERTICAL;
widgetList.push_back(widget);
}
else if (desc.Type == "spinner")
{
widget.type = WWT_SPINNER;
@ -827,6 +1355,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 +1406,16 @@ namespace OpenRCT2::Ui::Windows
{
info.WidgetIndexMap.push_back(widgetDescIndex);
}
if (widgetDesc.Type == "listview")
{
CustomListViewInfo listView;
listView.Columns = widgetDesc.ListViewColumns;
listView.Items = widgetDesc.ListViewItems;
listView.ShowColumnHeaders = widgetDesc.ShowColumnHeaders;
listView.IsStriped = widgetDesc.IsStriped;
info.ListViews.push_back(std::move(listView));
}
}
for (size_t i = firstCustomWidgetIndex; i < widgets.size(); i++)

View File

@ -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.