diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 74f0c29d9f..cd4d2fbc96 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -18,6 +18,7 @@ - Feature: [#13613] Add single-rail roller coaster (Rocky Mountain Construction Raptor). - Feature: [#13614] Add terrain surfaces from RollerCoaster Tycoon 1. - Feature: [#13675] [Plugin] Add context.setInterval and context.setTimeout. +- Feature: [#13927] [Plugin] Add isVisible and text box widget. - Change: [#13346] [Plugin] Renamed FootpathScenery to FootpathAddition, fix typos. - Change: [#13857] Change Rotation Control Toggle to track element number 256 - Fix: [#4605, #11912] Water palettes are not updated properly when selected in Object Selection. diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index acbd7de6ca..a1c79735ca 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -1,5 +1,5 @@ /***************************************************************************** - * Copyright (c) 2014-2020 OpenRCT2 developers + * Copyright (c) 2014-2021 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 @@ -990,7 +990,19 @@ declare global { } type EntityType = - "car" | "duck" | "peep" | "steam_particle" | "money_effect" | "crashed_vehicle_particle" | "explosion_cloud" | "crash_splash" | "explosion_flare" | "jumping_fountain_water" | "balloon" | "jumping_fountain_snow"; + "balloon" | + "car" | + "crash_splash" | + "crashed_vehicle_particle" | + "duck" | + "explosion_cloud" | + "explosion_flare" | + "jumping_fountain_snow" | + "jumping_fountain_water" | + "litter" | + "money_effect" | + "peep" | + "steam_particle"; /** * Represents an object "entity" on the map that can typically moves and has a sub-tile coordinate. @@ -1733,7 +1745,7 @@ declare global { readonly windows: number; readonly mainViewport: Viewport; readonly tileSelection: TileSelection; - readonly tool: Tool; + readonly tool: Tool | null; getWindow(id: number): Window; getWindow(classification: string): Window; @@ -1754,6 +1766,19 @@ declare global { */ showTextInput(desc: TextInputDesc): void; + /** + * Shows the window for loading or saving a file and calls the given callback when a file + * is selected. + * @param desc The parameters for the file browse window. + */ + showFileBrowse(desc: FileBrowseDesc): void; + + /** + * Shows the scenario select window and calls the given callback when a scenario is + * selected. + */ + showScenarioSelect(desc: ScenarioSelectDesc): void; + /** * Begins a new tool session. The cursor will change to the style specified by the * given tool descriptor and cursor events will be provided. @@ -1794,6 +1819,59 @@ declare global { callback: (value: string) => void; } + /** + * Parameters for the file browse window. + */ + interface FileBrowseDesc { + /** + * Whether to browse a file for loading or saving. Saving will prompt the user + * before overwriting a file. + */ + type: 'load'; + + /** + * The type of file to browse for. + */ + fileType: 'game' | 'heightmap'; + + /** + * The pre-selected file to load by default if the user clicks OK. + */ + defaultPath?: string; + + /** + * The function to call when the user has selected a file. + */ + callback: (path: string) => void; + } + + /** + * Parameters for the scenario select window. + */ + interface ScenarioSelectDesc { + /** + * The function to call when the user has selected a scenario. + */ + callback: (scenario: ScenarioFile) => void; + } + + /** + * Represents an installed scenario's path and metadata. + */ + interface ScenarioFile { + id: number; + category: 'beginner' | 'challenging' | 'expert' | 'real' | 'other' | 'dlc' | 'build_your_own'; + sourceGame: 'rct1' | 'rct1_aa' | 'rct1_ll' | 'rct2' | 'rct2_ww' | 'rct2_tt' | 'real' | 'other'; + path: string; + internalName: string; + name: string; + details: string; + highscore: { + name: string; + companyValue: number; + } + } + interface TileSelection { range: MapRange; tiles: CoordsXY[]; @@ -1821,11 +1899,11 @@ declare global { id: string; cursor?: CursorType; - onStart: () => void; - onDown: (e: ToolEventArgs) => void; - onMove: (e: ToolEventArgs) => void; - onUp: (e: ToolEventArgs) => void; - onFinish: () => void; + onStart?: () => void; + onDown?: (e: ToolEventArgs) => void; + onMove?: (e: ToolEventArgs) => void; + onUp?: (e: ToolEventArgs) => void; + onFinish?: () => void; } type CursorType = @@ -1861,10 +1939,14 @@ declare global { * Represents the type of a widget, e.g. button or label. */ type WidgetType = - "button" | "checkbox" | "colourpicker" | "dropdown" | "groupbox" | "label" | "listview" | "spinner" | "viewport"; + "button" | "checkbox" | "colourpicker" | "dropdown" | "groupbox" | + "label" | "listview" | "spinner" | "textbox" | "viewport"; - interface Widget { - type: WidgetType; + type Widget = + ButtonWidget | CheckboxWidget | ColourPickerWidget | DropdownWidget | GroupBoxWidget | + LabelWidget | ListView | SpinnerWidget | TextBoxWidget | ViewportWidget; + + interface WidgetBase { x: number; y: number; width: number; @@ -1872,42 +1954,55 @@ declare global { name?: string; tooltip?: string; isDisabled?: boolean; + isVisible?: boolean; } - interface ButtonWidget extends Widget { + interface ButtonWidget extends WidgetBase { + type: 'button'; /** * Whether the button has a 3D border. * By default, text buttons have borders and image buttons do not but it can be overridden. */ border?: boolean; - image: number; - isPressed: boolean; - text: string; - onClick: () => void; + image?: number; + isPressed?: boolean; + text?: string; + onClick?: () => void; } - interface CheckboxWidget extends Widget { - text: string; - isChecked: boolean; - onChange: (isChecked: boolean) => void; + interface CheckboxWidget extends WidgetBase { + type: 'checkbox'; + text?: string; + isChecked?: boolean; + onChange?: (isChecked: boolean) => void; } - interface ColourPickerWidget extends Widget { - colour: number; - onChange: (colour: number) => void; + interface ColourPickerWidget extends WidgetBase { + type: 'colourpicker'; + colour?: number; + onChange?: (colour: number) => void; } - interface DropdownWidget extends Widget { - items: string[]; - selectedIndex: number; - onChange: (index: number) => void; + interface DropdownWidget extends WidgetBase { + type: 'dropdown'; + items?: string[]; + selectedIndex?: number; + onChange?: (index: number) => void; } - interface LabelWidget extends Widget { - text: string; - onChange: (index: number) => void; + interface GroupBoxWidget extends WidgetBase { + type: 'groupbox'; } + interface LabelWidget extends WidgetBase { + type: 'label'; + text?: string; + textAlign?: TextAlignment; + onChange?: (index: number) => void; + } + + type TextAlignment = "left" | "centred"; + type SortOrder = "none" | "ascending" | "descending"; type ScrollbarType = "none" | "horizontal" | "vertical" | "both"; @@ -1935,7 +2030,8 @@ declare global { column: number; } - interface ListView extends Widget { + interface ListView extends WidgetBase { + type: 'listview'; scrollbars?: ScrollbarType; isStriped?: boolean; showColumnHeaders?: boolean; @@ -1945,18 +2041,27 @@ declare global { readonly highlightedCell?: RowColumn; canSelect?: boolean; - onHighlight: (item: number, column: number) => void; - onClick: (item: number, column: number) => void; + onHighlight?: (item: number, column: number) => void; + onClick?: (item: number, column: number) => void; } - interface SpinnerWidget extends Widget { - text: string; - onDecrement: () => void; - onIncrement: () => void; + interface SpinnerWidget extends WidgetBase { + type: 'spinner'; + text?: string; + onDecrement?: () => void; + onIncrement?: () => void; } - interface ViewportWidget extends Widget { - viewport: Viewport + interface TextBoxWidget extends WidgetBase { + type: 'textbox'; + text?: string; + maxLength?: number; + onChange?: (text: string) => void; + } + + interface ViewportWidget extends WidgetBase { + type: 'viewport'; + viewport?: Viewport } interface Window { diff --git a/src/openrct2-ui/input/MouseInput.cpp b/src/openrct2-ui/input/MouseInput.cpp index cd2f66bbb9..24f5ea96fa 100644 --- a/src/openrct2-ui/input/MouseInput.cpp +++ b/src/openrct2-ui/input/MouseInput.cpp @@ -1442,7 +1442,7 @@ static void InputUpdateTooltip(rct_window* w, rct_widgetindex widgetIndex, const if (gTooltipCursor == screenCoords) { _tooltipNotShownTicks++; - if (_tooltipNotShownTicks > 50) + if (_tooltipNotShownTicks > 50 && WidgetIsVisible(w, widgetIndex)) { gTooltipTimeout = 0; window_tooltip_open(w, widgetIndex, screenCoords); @@ -1457,7 +1457,8 @@ static void InputUpdateTooltip(rct_window* w, rct_widgetindex widgetIndex, const reset_tooltip_not_shown(); if (w == nullptr || gTooltipWidget.window_classification != w->classification - || gTooltipWidget.window_number != w->number || gTooltipWidget.widget_index != widgetIndex) + || gTooltipWidget.window_number != w->number || gTooltipWidget.widget_index != widgetIndex + || !WidgetIsVisible(w, widgetIndex)) { window_tooltip_close(); } diff --git a/src/openrct2-ui/interface/Widget.cpp b/src/openrct2-ui/interface/Widget.cpp index 4c7edf6563..189cb44cac 100644 --- a/src/openrct2-ui/interface/Widget.cpp +++ b/src/openrct2-ui/interface/Widget.cpp @@ -357,8 +357,16 @@ static void WidgetTextCentred(rct_drawpixelinfo* dpi, rct_window* w, rct_widgeti stringId = STR_STRING; ft.Add(widget->string); } - DrawTextEllipsised( - dpi, { (topLeft.x + r + 1) / 2 - 1, topLeft.y }, widget->width() - 2, stringId, ft, colour, TextAlignment::CENTRE); + + ScreenCoordsXY coords = { (topLeft.x + r + 1) / 2 - 1, topLeft.y }; + if (widget->type == WindowWidgetType::LabelCentred) + { + gfx_draw_string_centred_wrapped(dpi, ft.Data(), coords, widget->width() - 2, stringId, colour); + } + else + { + DrawTextEllipsised(dpi, coords, widget->width() - 2, stringId, ft, colour, TextAlignment::CENTRE); + } } /** @@ -398,7 +406,16 @@ static void WidgetText(rct_drawpixelinfo* dpi, rct_window* w, rct_widgetindex wi stringId = STR_STRING; ft.Add(widget->string); } - DrawTextEllipsised(dpi, { l + 1, t }, r - l, stringId, ft, colour); + + ScreenCoordsXY coords = { l + 1, t }; + if (widget->type == WindowWidgetType::LabelCentred) + { + gfx_draw_string_centred_wrapped(dpi, ft.Data(), coords, r - l, stringId, colour); + } + else + { + DrawTextEllipsised(dpi, coords, r - l, stringId, ft, colour); + } } /** @@ -829,6 +846,8 @@ static void WidgetDrawImage(rct_drawpixelinfo* dpi, rct_window* w, rct_widgetind bool WidgetIsEnabled(rct_window* w, rct_widgetindex widgetIndex) { + if (!WidgetIsVisible(w, widgetIndex)) + return false; return (w->enabled_widgets & (1LL << widgetIndex)) != 0; } @@ -837,6 +856,11 @@ bool WidgetIsDisabled(rct_window* w, rct_widgetindex widgetIndex) return (w->disabled_widgets & (1LL << widgetIndex)) != 0; } +bool WidgetIsVisible(rct_window* w, rct_widgetindex widgetIndex) +{ + return w->widgets[widgetIndex].IsVisible(); +} + bool WidgetIsPressed(rct_window* w, rct_widgetindex widgetIndex) { if (w->pressed_widgets & (1LL << widgetIndex)) @@ -1026,6 +1050,18 @@ void WidgetSetDisabled(rct_window* w, rct_widgetindex widgetIndex, bool value) } } +void WidgetSetVisible(rct_window* w, rct_widgetindex widgetIndex, bool value) +{ + if (value) + { + w->widgets[widgetIndex].flags &= ~WIDGET_FLAGS::IS_HIDDEN; + } + else + { + w->widgets[widgetIndex].flags |= WIDGET_FLAGS::IS_HIDDEN; + } +} + void WidgetSetCheckboxValue(rct_window* w, rct_widgetindex widgetIndex, int32_t value) { if (value) diff --git a/src/openrct2-ui/interface/Window.cpp b/src/openrct2-ui/interface/Window.cpp index f954cda263..c1f9d09b60 100644 --- a/src/openrct2-ui/interface/Window.cpp +++ b/src/openrct2-ui/interface/Window.cpp @@ -628,11 +628,13 @@ void WindowDrawWidgets(rct_window* w, rct_drawpixelinfo* dpi) widgetIndex = 0; for (widget = w->widgets; widget->type != WindowWidgetType::Last; widget++) { - // Check if widget is outside the draw region - if (w->windowPos.x + widget->left < dpi->x + dpi->width && w->windowPos.x + widget->right >= dpi->x) - if (w->windowPos.y + widget->top < dpi->y + dpi->height && w->windowPos.y + widget->bottom >= dpi->y) - WidgetDraw(dpi, w, widgetIndex); - + if (widget->IsVisible()) + { + // Check if widget is outside the draw region + if (w->windowPos.x + widget->left < dpi->x + dpi->width && w->windowPos.x + widget->right >= dpi->x) + if (w->windowPos.y + widget->top < dpi->y + dpi->height && w->windowPos.y + widget->bottom >= dpi->y) + WidgetDraw(dpi, w, widgetIndex); + } widgetIndex++; } diff --git a/src/openrct2-ui/scripting/CustomListView.cpp b/src/openrct2-ui/scripting/CustomListView.cpp index 40605cb418..06bf610779 100644 --- a/src/openrct2-ui/scripting/CustomListView.cpp +++ b/src/openrct2-ui/scripting/CustomListView.cpp @@ -497,7 +497,7 @@ void CustomListView::MouseDown(const ScreenCoordsXY& pos) auto hitResult = GetItemIndexAt(pos); if (hitResult) { - if (hitResult->Row != HEADER_ROW && OnClick.context() != nullptr && OnClick.is_function()) + if (hitResult->Row != HEADER_ROW) { if (CanSelect) { @@ -506,12 +506,15 @@ void CustomListView::MouseDown(const ScreenCoordsXY& pos) } 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 (ctx != nullptr && OnClick.is_function()) + { + 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) @@ -597,11 +600,14 @@ void CustomListView::Paint(rct_window* w, rct_drawpixelinfo* dpi, const rct_scro // Columns if (Columns.size() == 0) { - const auto& text = item.Cells[0]; - if (!text.empty()) + if (item.Cells.size() != 0) { - ScreenSize cellSize = { std::numeric_limits::max(), LIST_ROW_HEIGHT }; - PaintCell(dpi, { 0, y }, cellSize, text.c_str(), isHighlighted); + 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 diff --git a/src/openrct2-ui/scripting/CustomWindow.cpp b/src/openrct2-ui/scripting/CustomWindow.cpp index 4933586301..7848ba63df 100644 --- a/src/openrct2-ui/scripting/CustomWindow.cpp +++ b/src/openrct2-ui/scripting/CustomWindow.cpp @@ -54,6 +54,7 @@ namespace OpenRCT2::Ui::Windows static void window_custom_mousedown(rct_window* w, rct_widgetindex widgetIndex, rct_widget* widget); 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_textinput(rct_window* w, rct_widgetindex widgetIndex, char* text); 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); @@ -70,6 +71,7 @@ namespace OpenRCT2::Ui::Windows events.resize = &window_custom_resize; events.mouse_down = &window_custom_mousedown; events.dropdown = &window_custom_dropdown; + events.text_input = &window_custom_textinput; events.update = &window_custom_update; events.get_scroll_size = &window_custom_scrollgetsize; events.scroll_mousedown = &window_custom_scrollmousedown; @@ -91,6 +93,7 @@ namespace OpenRCT2::Ui::Windows std::string Name; ImageId Image; std::string Text; + TextAlignment TextAlign; colour_t Colour; std::string Tooltip; std::vector Items; @@ -98,9 +101,11 @@ namespace OpenRCT2::Ui::Windows std::vector ListViewColumns; ScrollbarType Scrollbars{}; int32_t SelectedIndex{}; + int32_t MaxLength{}; std::optional SelectedCell; bool IsChecked{}; bool IsDisabled{}; + bool IsVisible{}; bool IsPressed{}; bool HasBorder{}; bool ShowColumnHeaders{}; @@ -123,6 +128,7 @@ namespace OpenRCT2::Ui::Windows result.Width = desc["width"].as_int(); result.Height = desc["height"].as_int(); result.IsDisabled = AsOrDefault(desc["isDisabled"], false); + result.IsVisible = AsOrDefault(desc["isVisible"], true); result.Name = AsOrDefault(desc["name"], ""); result.Tooltip = AsOrDefault(desc["tooltip"], ""); if (result.Type == "button") @@ -159,18 +165,29 @@ namespace OpenRCT2::Ui::Windows } else if (result.Type == "dropdown") { - auto dukItems = desc["items"].as_array(); - for (const auto& dukItem : dukItems) + if (desc["items"].is_array()) { - result.Items.push_back(ProcessString(dukItem)); + auto dukItems = desc["items"].as_array(); + for (const auto& dukItem : dukItems) + { + result.Items.push_back(ProcessString(dukItem)); + } } - result.SelectedIndex = desc["selectedIndex"].as_int(); + result.SelectedIndex = AsOrDefault(desc["selectedIndex"], 0); result.OnChange = desc["onChange"]; } - else if (result.Type == "groupbox" || result.Type == "label") + else if (result.Type == "groupbox") { result.Text = ProcessString(desc["text"]); } + else if (result.Type == "label") + { + result.Text = ProcessString(desc["text"]); + if (ProcessString(desc["textAlign"]) == "centred") + { + result.TextAlign = TextAlignment::CENTRE; + } + } else if (result.Type == "listview") { result.ListViewColumns = FromDuk>(desc["columns"]); @@ -192,6 +209,12 @@ namespace OpenRCT2::Ui::Windows result.OnIncrement = desc["onIncrement"]; result.OnDecrement = desc["onDecrement"]; } + else if (result.Type == "textbox") + { + result.Text = ProcessString(desc["text"]); + result.MaxLength = AsOrDefault(desc["maxLength"], 32); + result.OnChange = desc["onChange"]; + } result.HasBorder = AsOrDefault(desc["border"], result.HasBorder); return result; } @@ -307,7 +330,11 @@ namespace OpenRCT2::Ui::Windows colour_t c = COLOUR_BLACK; if (w.type() == DukValue::Type::NUMBER) { - c = static_cast(std::clamp(w.as_int(), COLOUR_BLACK, COLOUR_COUNT - 1)); + c = std::clamp(BASE_COLOUR(w.as_int()), COLOUR_BLACK, COLOUR_COUNT - 1); + if (w.as_int() & COLOUR_FLAG_TRANSLUCENT) + { + c = TRANSLUCENT(c); + } } return c; }); @@ -388,7 +415,7 @@ namespace OpenRCT2::Ui::Windows { auto desc = CustomWindowDesc::FromDukValue(dukDesc); - uint16_t windowFlags = WF_RESIZABLE; + uint16_t windowFlags = WF_RESIZABLE | WF_TRANSPARENT; rct_window* window{}; if (desc.X && desc.Y) @@ -552,6 +579,11 @@ namespace OpenRCT2::Ui::Windows InvokeEventHandler(info.Owner, widgetDesc->OnIncrement); } } + else if (widgetDesc->Type == "textbox") + { + auto* text = const_cast(widgetDesc->Text.c_str()); + window_start_textbox(w, widgetIndex, STR_STRING, text, widgetDesc->MaxLength + 1); + } } } @@ -575,6 +607,28 @@ namespace OpenRCT2::Ui::Windows } } + static void window_custom_textinput(rct_window* w, rct_widgetindex widgetIndex, char* text) + { + if (text == nullptr) + return; + + auto& info = GetInfo(w); + auto widgetDesc = info.GetCustomWidgetDesc(w, widgetIndex); + if (widgetDesc != nullptr) + { + if (widgetDesc->Type == "textbox") + { + UpdateWidgetText(w, widgetIndex, text); + + std::vector args; + auto ctx = widgetDesc->OnChange.context(); + duk_push_string(ctx, text); + args.push_back(DukValue::take_from_stack(ctx)); + InvokeEventHandler(info.Owner, widgetDesc->OnChange, args); + } + } + } + static void window_custom_update(rct_window* w) { const auto& info = GetInfo(w); @@ -668,14 +722,28 @@ namespace OpenRCT2::Ui::Windows w->widgets[WIDX_CLOSE].right = w->width - 3; w->widgets[WIDX_CONTENT_PANEL].right = w->width - 1; w->widgets[WIDX_CONTENT_PANEL].bottom = w->height - 1; + w->widgets[WIDX_CLOSE].text = (w->colours[0] & COLOUR_FLAG_TRANSLUCENT) ? STR_CLOSE_X_WHITE : STR_CLOSE_X; + + // Having the content panel visible for transparent windows makes the borders darker than they should be + // For now just hide it if there are no tabs and the window is not resizable + auto& info = GetInfo(w); + auto canResize = (w->flags & WF_RESIZABLE) != 0 && (w->min_width != w->max_width || w->min_height != w->max_height); + auto numTabs = info.Desc.Tabs.size(); + if (canResize || numTabs != 0) + { + w->widgets[WIDX_CONTENT_PANEL].flags &= ~WIDGET_FLAGS::IS_HIDDEN; + } + else + { + w->widgets[WIDX_CONTENT_PANEL].flags |= WIDGET_FLAGS::IS_HIDDEN; + } window_custom_set_pressed_tab(w); - const auto& desc = GetInfo(w).Desc; + const auto& desc = info.Desc; auto ft = Formatter::Common(); ft.Add(desc.Title.c_str()); - auto& info = GetInfo(w); size_t scrollIndex = 0; for (auto widget = w->widgets; widget->type != WindowWidgetType::Last; widget++) { @@ -723,25 +791,6 @@ namespace OpenRCT2::Ui::Windows } } - static void window_custom_paint(rct_window* w, rct_drawpixelinfo* dpi) - { - WindowDrawWidgets(w, dpi); - window_custom_draw_tab_images(w, dpi); - if (w->viewport != nullptr) - { - window_draw_viewport(dpi, w); - } - } - - 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; @@ -756,6 +805,29 @@ namespace OpenRCT2::Ui::Windows return std::nullopt; } + static void window_custom_paint(rct_window* w, rct_drawpixelinfo* dpi) + { + WindowDrawWidgets(w, dpi); + window_custom_draw_tab_images(w, dpi); + if (w->viewport != nullptr) + { + auto widgetIndex = GetViewportWidgetIndex(w); + if (WidgetIsVisible(w, widgetIndex.value_or(false))) + { + window_draw_viewport(dpi, w); + } + } + } + + 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 void window_custom_update_viewport(rct_window* w) { auto viewportWidgetIndex = GetViewportWidgetIndex(w); @@ -833,6 +905,8 @@ namespace OpenRCT2::Ui::Windows widget.flags |= WIDGET_FLAGS::IS_ENABLED; if (desc.IsDisabled) widget.flags |= WIDGET_FLAGS::IS_DISABLED; + if (!desc.IsVisible) + widget.flags |= WIDGET_FLAGS::IS_HIDDEN; if (desc.Type == "button") { @@ -911,6 +985,10 @@ namespace OpenRCT2::Ui::Windows widget.type = WindowWidgetType::Label; widget.string = const_cast(desc.Text.c_str()); widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; + if (desc.TextAlign == TextAlignment::CENTRE) + { + widget.type = WindowWidgetType::LabelCentred; + } widgetList.push_back(widget); } else if (desc.Type == "listview") @@ -953,6 +1031,13 @@ namespace OpenRCT2::Ui::Windows widget.text = STR_NUMERIC_UP; widgetList.push_back(widget); } + else if (desc.Type == "textbox") + { + widget.type = WindowWidgetType::TextBox; + widget.string = const_cast(desc.Text.c_str()); + widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; + widgetList.push_back(widget); + } else if (desc.Type == "viewport") { widget.type = WindowWidgetType::Viewport; @@ -1319,6 +1404,33 @@ namespace OpenRCT2::Ui::Windows return nullptr; } + int32_t GetWidgetMaxLength(rct_window* w, rct_widgetindex widgetIndex) + { + if (w->custom_info != nullptr) + { + auto& customInfo = GetInfo(w); + auto customWidgetInfo = customInfo.GetCustomWidgetDesc(w, widgetIndex); + if (customWidgetInfo != nullptr) + { + return customWidgetInfo->MaxLength; + } + } + return 0; + } + + void SetWidgetMaxLength(rct_window* w, rct_widgetindex widgetIndex, int32_t value) + { + if (w->custom_info != nullptr) + { + auto& customInfo = GetInfo(w); + auto customWidgetInfo = customInfo.GetCustomWidgetDesc(w, widgetIndex); + if (customWidgetInfo != nullptr) + { + customWidgetInfo->MaxLength = value; + } + } + } + } // namespace OpenRCT2::Ui::Windows #endif diff --git a/src/openrct2-ui/scripting/CustomWindow.h b/src/openrct2-ui/scripting/CustomWindow.h index 84890e36f3..c30d6249c7 100644 --- a/src/openrct2-ui/scripting/CustomWindow.h +++ b/src/openrct2-ui/scripting/CustomWindow.h @@ -34,6 +34,8 @@ namespace OpenRCT2::Ui::Windows std::string GetWidgetName(rct_window* w, rct_widgetindex widgetIndex); void SetWidgetName(rct_window* w, rct_widgetindex widgetIndex, std::string_view name); CustomListView* GetCustomListView(rct_window* w, rct_widgetindex widgetIndex); + int32_t GetWidgetMaxLength(rct_window* w, rct_widgetindex widgetIndex); + void SetWidgetMaxLength(rct_window* w, rct_widgetindex widgetIndex, int32_t value); } // namespace OpenRCT2::Ui::Windows #endif diff --git a/src/openrct2-ui/scripting/ScUi.hpp b/src/openrct2-ui/scripting/ScUi.hpp index ee22d43845..2b42fa270f 100644 --- a/src/openrct2-ui/scripting/ScUi.hpp +++ b/src/openrct2-ui/scripting/ScUi.hpp @@ -193,7 +193,7 @@ namespace OpenRCT2::Scripting auto plugin = _scriptEngine.GetExecInfo().GetCurrentPlugin(); auto title = desc["title"].as_string(); auto description = desc["description"].as_string(); - auto initialValue = AsOrDefault(desc["maxLength"], ""); + auto initialValue = AsOrDefault(desc["initialValue"], ""); auto maxLength = AsOrDefault(desc["maxLength"], std::numeric_limits::max()); auto callback = desc["callback"]; window_text_input_open( diff --git a/src/openrct2-ui/scripting/ScWidget.hpp b/src/openrct2-ui/scripting/ScWidget.hpp index c06ff93695..2973d7c7b2 100644 --- a/src/openrct2-ui/scripting/ScWidget.hpp +++ b/src/openrct2-ui/scripting/ScWidget.hpp @@ -318,6 +318,39 @@ namespace OpenRCT2::Scripting } } + bool isVisible_get() const + { + auto w = GetWindow(); + if (w != nullptr) + { + return WidgetIsVisible(w, _widgetIndex); + } + return false; + } + void isVisible_set(bool value) + { + auto w = GetWindow(); + if (w != nullptr) + { + WidgetSetVisible(w, _widgetIndex, value); + + auto widget = GetWidget(); + if (widget != nullptr) + { + if (widget->type == WindowWidgetType::DropdownMenu) + { + WidgetSetVisible(w, _widgetIndex + 1, value); + } + else if (widget->type == WindowWidgetType::Spinner) + { + WidgetSetVisible(w, _widgetIndex + 1, value); + WidgetSetVisible(w, _widgetIndex + 2, value); + } + } + } + } + + protected: std::string text_get() const { if (IsCustomWindow()) @@ -330,6 +363,7 @@ namespace OpenRCT2::Scripting } return ""; } + void text_set(std::string value) { auto w = GetWindow(); @@ -339,20 +373,6 @@ namespace OpenRCT2::Scripting } } - std::shared_ptr viewport_get() const - { - auto w = GetWindow(); - if (w != nullptr && IsCustomWindow()) - { - auto widget = GetWidget(); - if (widget != nullptr && widget->type == WindowWidgetType::Viewport) - { - return std::make_shared(w->classification, w->number); - } - } - return {}; - } - public: static void Register(duk_context* ctx) { @@ -364,10 +384,7 @@ namespace OpenRCT2::Scripting dukglue_register_property(ctx, &ScWidget::width_get, &ScWidget::width_set, "width"); dukglue_register_property(ctx, &ScWidget::height_get, &ScWidget::height_set, "height"); dukglue_register_property(ctx, &ScWidget::isDisabled_get, &ScWidget::isDisabled_set, "isDisabled"); - - // No so common - dukglue_register_property(ctx, &ScWidget::text_get, &ScWidget::text_set, "text"); - dukglue_register_property(ctx, &ScWidget::viewport_get, nullptr, "viewport"); + dukglue_register_property(ctx, &ScWidget::isVisible_get, &ScWidget::isVisible_set, "isVisible"); } protected: @@ -608,6 +625,63 @@ namespace OpenRCT2::Scripting } }; + class ScGroupBoxWidget : public ScWidget + { + public: + ScGroupBoxWidget(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, &ScGroupBoxWidget::text_get, &ScGroupBoxWidget::text_set, "text"); + } + }; + + class ScLabelWidget : public ScWidget + { + public: + ScLabelWidget(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, &ScLabelWidget::text_get, &ScLabelWidget::text_set, "text"); + dukglue_register_property(ctx, &ScLabelWidget::textAlign_get, &ScLabelWidget::textAlign_set, "textAlign"); + } + + private: + std::string textAlign_get() const + { + auto* widget = GetWidget(); + if (widget != nullptr) + { + if (widget->type == WindowWidgetType::LabelCentred) + { + return "centred"; + } + } + return "left"; + } + + void textAlign_set(const std::string& value) + { + auto* widget = GetWidget(); + if (widget != nullptr) + { + if (value == "centred") + widget->type = WindowWidgetType::LabelCentred; + else + widget->type = WindowWidgetType::Label; + } + } + }; + class ScListViewWidget : public ScWidget { public: @@ -799,6 +873,86 @@ namespace OpenRCT2::Scripting } }; + class ScSpinnerWidget : public ScWidget + { + public: + ScSpinnerWidget(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, &ScSpinnerWidget::text_get, &ScSpinnerWidget::text_set, "text"); + } + }; + + class ScTextBoxWidget : public ScWidget + { + public: + ScTextBoxWidget(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, &ScTextBoxWidget::maxLength_get, &ScTextBoxWidget::maxLength_set, "maxLength"); + } + + private: + int32_t maxLength_get() const + { + auto w = GetWindow(); + if (w != nullptr && IsCustomWindow()) + { + return OpenRCT2::Ui::Windows::GetWidgetMaxLength(w, _widgetIndex); + } + return 0; + } + + void maxLength_set(int32_t value) + { + auto w = GetWindow(); + if (w != nullptr && IsCustomWindow()) + { + OpenRCT2::Ui::Windows::SetWidgetMaxLength(w, _widgetIndex, value); + } + } + }; + + class ScViewportWidget : public ScWidget + { + public: + ScViewportWidget(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, &ScViewportWidget::viewport_get, nullptr, "viewport"); + } + + private: + std::shared_ptr viewport_get() const + { + auto w = GetWindow(); + if (w != nullptr && IsCustomWindow()) + { + auto widget = GetWidget(); + if (widget != nullptr && widget->type == WindowWidgetType::Viewport) + { + return std::make_shared(w->classification, w->number); + } + } + return {}; + } + }; + inline DukValue ScWidget::ToDukValue(duk_context* ctx, rct_window* w, rct_widgetindex widgetIndex) { const auto& widget = w->widgets[widgetIndex]; @@ -816,8 +970,19 @@ namespace OpenRCT2::Scripting return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); case WindowWidgetType::DropdownMenu: return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); + case WindowWidgetType::Groupbox: + return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); + case WindowWidgetType::Label: + case WindowWidgetType::LabelCentred: + return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); case WindowWidgetType::Scroll: return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); + case WindowWidgetType::Spinner: + return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); + case WindowWidgetType::TextBox: + return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); + case WindowWidgetType::Viewport: + return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); default: return GetObjectAsDukValue(ctx, std::make_shared(c, n, widgetIndex)); } diff --git a/src/openrct2-ui/scripting/ScWindow.hpp b/src/openrct2-ui/scripting/ScWindow.hpp index 8601841a96..6771121207 100644 --- a/src/openrct2-ui/scripting/ScWindow.hpp +++ b/src/openrct2-ui/scripting/ScWindow.hpp @@ -189,8 +189,12 @@ namespace OpenRCT2::Scripting } bool isSticky_get() const { - auto flags = GetWindow()->flags; - return (flags & (WF_STICK_TO_BACK | WF_STICK_TO_FRONT)) != 0; + auto w = GetWindow(); + if (w != nullptr) + { + return (w->flags & (WF_STICK_TO_BACK | WF_STICK_TO_FRONT)) != 0; + } + return false; } std::vector widgets_get() const @@ -232,8 +236,16 @@ namespace OpenRCT2::Scripting { for (size_t i = 0; i < std::size(w->colours); i++) { - w->colours[i] = i < colours.size() ? std::clamp(colours[i], COLOUR_BLACK, COLOUR_COUNT - 1) - : COLOUR_BLACK; + int32_t c = COLOUR_BLACK; + if (i < colours.size()) + { + c = std::clamp(BASE_COLOUR(colours[i]), COLOUR_BLACK, COLOUR_COUNT - 1); + if (colours[i] & COLOUR_FLAG_TRANSLUCENT) + { + c = TRANSLUCENT(c); + } + } + w->colours[i] = c; } } } diff --git a/src/openrct2-ui/scripting/UiExtensions.cpp b/src/openrct2-ui/scripting/UiExtensions.cpp index 3cf4404550..0361b58f1d 100644 --- a/src/openrct2-ui/scripting/UiExtensions.cpp +++ b/src/openrct2-ui/scripting/UiExtensions.cpp @@ -31,12 +31,18 @@ void UiScriptExtensions::Extend(ScriptEngine& scriptEngine) ScTool::Register(ctx); ScUi::Register(ctx); ScViewport::Register(ctx); + ScWidget::Register(ctx); ScButtonWidget::Register(ctx); ScColourPickerWidget::Register(ctx); ScCheckBoxWidget::Register(ctx); ScDropdownWidget::Register(ctx); + ScGroupBoxWidget::Register(ctx); + ScLabelWidget::Register(ctx); ScListViewWidget::Register(ctx); + ScSpinnerWidget::Register(ctx); + ScTextBoxWidget::Register(ctx); + ScViewportWidget::Register(ctx); ScWindow::Register(ctx); InitialiseCustomMenuItems(scriptEngine); diff --git a/src/openrct2/interface/Widget.h b/src/openrct2/interface/Widget.h index 7bb87829f9..565c203242 100644 --- a/src/openrct2/interface/Widget.h +++ b/src/openrct2/interface/Widget.h @@ -137,6 +137,7 @@ void WidgetDraw(rct_drawpixelinfo* dpi, rct_window* w, rct_widgetindex widgetInd bool WidgetIsEnabled(rct_window* w, rct_widgetindex widgetIndex); bool WidgetIsDisabled(rct_window* w, rct_widgetindex widgetIndex); +bool WidgetIsVisible(rct_window* w, rct_widgetindex widgetIndex); bool WidgetIsPressed(rct_window* w, rct_widgetindex widgetIndex); bool WidgetIsHighlighted(rct_window* w, rct_widgetindex widgetIndex); bool WidgetIsActiveTool(rct_window* w, rct_widgetindex widgetIndex); @@ -146,6 +147,7 @@ void WidgetScrollGetPart( void WidgetSetEnabled(rct_window* w, rct_widgetindex widgetIndex, bool enabled); void WidgetSetDisabled(rct_window* w, rct_widgetindex widgetIndex, bool value); +void WidgetSetVisible(rct_window* w, rct_widgetindex widgetIndex, bool value); void WidgetSetCheckboxValue(rct_window* w, rct_widgetindex widgetIndex, int32_t value); #endif diff --git a/src/openrct2/interface/Window.cpp b/src/openrct2/interface/Window.cpp index 572eef55c4..424e8af1ec 100644 --- a/src/openrct2/interface/Window.cpp +++ b/src/openrct2/interface/Window.cpp @@ -436,7 +436,7 @@ rct_widgetindex window_find_widget_from_point(rct_window* w, const ScreenCoordsX { break; } - else if (widget->type != WindowWidgetType::Empty) + else if (widget->type != WindowWidgetType::Empty && widget->IsVisible()) { if (screenCoords.x >= w->windowPos.x + widget->left && screenCoords.x <= w->windowPos.x + widget->right && screenCoords.y >= w->windowPos.y + widget->top && screenCoords.y <= w->windowPos.y + widget->bottom) @@ -2004,12 +2004,18 @@ void window_cancel_textbox() if (gUsingWidgetTextBox) { rct_window* w = window_find_by_number(gCurrentTextBox.window.classification, gCurrentTextBox.window.number); - window_event_textinput_call(w, gCurrentTextBox.widget_index, nullptr); + if (w != nullptr) + { + window_event_textinput_call(w, gCurrentTextBox.widget_index, nullptr); + } gCurrentTextBox.window.classification = WC_NULL; gCurrentTextBox.window.number = 0; context_stop_text_input(); gUsingWidgetTextBox = false; - widget_invalidate(w, gCurrentTextBox.widget_index); + if (w != nullptr) + { + widget_invalidate(w, gCurrentTextBox.widget_index); + } gCurrentTextBox.widget_index = static_cast(WindowWidgetType::Last); } } diff --git a/src/openrct2/interface/Window.h b/src/openrct2/interface/Window.h index 137b3e111a..1993e2d178 100644 --- a/src/openrct2/interface/Window.h +++ b/src/openrct2/interface/Window.h @@ -76,6 +76,7 @@ namespace WIDGET_FLAGS const WidgetFlags IS_PRESSED = 1 << 2; const WidgetFlags IS_DISABLED = 1 << 3; const WidgetFlags TOOLTIP_IS_STRING = 1 << 4; + const WidgetFlags IS_HIDDEN = 1 << 5; } // namespace WIDGET_FLAGS enum class WindowWidgetType : uint8_t; @@ -132,6 +133,11 @@ struct rct_widget else return top - 1; } + + bool IsVisible() const + { + return !(flags & WIDGET_FLAGS::IS_HIDDEN); + } }; /** diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 8512b41f79..a0a30963f1 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -44,7 +44,7 @@ using namespace OpenRCT2; using namespace OpenRCT2::Scripting; -static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 18; +static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 19; struct ExpressionStringifier final { @@ -718,8 +718,15 @@ DukValue ScriptEngine::ExecutePluginCall( void ScriptEngine::LogPluginInfo(const std::shared_ptr& plugin, std::string_view message) { - const auto& pluginName = plugin->GetMetadata().Name; - _console.WriteLine("[" + pluginName + "] " + std::string(message)); + if (plugin == nullptr) + { + _console.WriteLine(std::string(message)); + } + else + { + const auto& pluginName = plugin->GetMetadata().Name; + _console.WriteLine("[" + pluginName + "] " + std::string(message)); + } } void ScriptEngine::AddNetworkPlugin(std::string_view code)