/***************************************************************************** * Copyright (c) 2014-2025 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. *****************************************************************************/ #include "WindowManager.h" #include "interface/FileBrowser.h" #include "interface/Theme.h" #include "interface/Window.h" #include "ride/VehicleSounds.h" #include "windows/Windows.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace OpenRCT2; using namespace OpenRCT2::Ui; using namespace OpenRCT2::Ui::Windows; namespace OpenRCT2 { class Formatter; } namespace WindowCloseFlags { static constexpr uint32_t None = 0; static constexpr uint32_t CloseSingle = (1 << 0); } // namespace WindowCloseFlags class WindowManager final : public IWindowManager { public: void Init() override { ThemeManagerInitialise(); WindowNewRideInitVars(); } WindowBase* OpenWindow(WindowClass wc) override { switch (wc) { case WindowClass::about: return AboutOpen(); case WindowClass::bottomToolbar: return GameBottomToolbarOpen(); case WindowClass::changelog: return openView(WindowView::changelog); case WindowClass::cheats: return CheatsOpen(); case WindowClass::clearScenery: return ClearSceneryOpen(); case WindowClass::customCurrencyConfig: return CustomCurrencyOpen(); case WindowClass::debugPaint: return DebugPaintOpen(); case WindowClass::editorInventionList: return EditorInventionsListOpen(); case WindowClass::editorObjectSelection: return EditorObjectSelectionOpen(); case WindowClass::editorScenarioOptions: return EditorScenarioOptionsOpen(); case WindowClass::finances: return FinancesOpen(); case WindowClass::footpath: return FootpathOpen(); case WindowClass::guestList: return GuestListOpen(); case WindowClass::land: return LandOpen(); case WindowClass::landRights: return LandRightsOpen(); case WindowClass::mainWindow: return MainOpen(); case WindowClass::map: return MapOpen(); case WindowClass::mapgen: return MapgenOpen(); case WindowClass::multiplayer: return MultiplayerOpen(); case WindowClass::constructRide: return NewRideOpen(); case WindowClass::parkInformation: return ParkEntranceOpen(); case WindowClass::recentNews: return NewsOpen(); case WindowClass::rideConstruction: return RideConstructionOpen(); case WindowClass::research: return ResearchOpen(); case WindowClass::rideList: return RideListOpen(); case WindowClass::options: return OptionsOpen(); case WindowClass::savePrompt: return SavePromptOpen(); case WindowClass::scenery: return SceneryOpen(); case WindowClass::sceneryScatter: return SceneryScatterOpen(); #ifndef DISABLE_NETWORK case WindowClass::serverList: return ServerListOpen(); case WindowClass::serverStart: return ServerStartOpen(); #endif case WindowClass::keyboardShortcutList: return ShortcutKeysOpen(); case WindowClass::staffList: return StaffListOpen(); case WindowClass::themes: return ThemesOpen(); case WindowClass::tileInspector: return TileInspectorOpen(); case WindowClass::titleExit: return TitleExitOpen(); case WindowClass::titleLogo: return TitleLogoOpen(); case WindowClass::titleMenu: return TitleMenuOpen(); case WindowClass::titleOptions: return TitleOptionsOpen(); case WindowClass::titleVersion: return TitleVersionOpen(); case WindowClass::topToolbar: return TopToolbarOpen(); case WindowClass::viewClipping: return ViewClippingOpen(); case WindowClass::viewport: return ViewportOpen(); case WindowClass::water: return WaterOpen(); case WindowClass::transparency: return TransparencyOpen(); case WindowClass::assetPacks: return AssetPacksOpen(); case WindowClass::editorParkEntrance: return EditorParkEntranceOpen(); default: Console::Error::WriteLine("Unhandled window class (%d)", wc); return nullptr; } } WindowBase* openView(WindowView view) override { switch (view) { case WindowView::parkAwards: return ParkAwardsOpen(); case WindowView::parkRating: return ParkRatingOpen(); case WindowView::parkObjective: return ParkObjectiveOpen(); case WindowView::parkGuests: return ParkGuestsOpen(); case WindowView::financesResearch: return FinancesResearchOpen(); case WindowView::rideResearch: if (Config::Get().interface.ToolbarShowResearch) { return this->OpenWindow(WindowClass::research); } return NewRideOpenResearch(); case WindowView::mazeConstruction: return MazeConstructionOpen(); case WindowView::networkPassword: return NetworkStatusOpenPassword(); case WindowView::editorBottomToolbar: return EditorBottomToolbarOpen(); case WindowView::changelog: return ChangelogOpen(WindowView::changelog); case WindowView::newVersionInfo: return ChangelogOpen(WindowView::newVersionInfo); case WindowView::contributors: return ChangelogOpen(WindowView::contributors); case WindowView::financeMarketing: return FinancesMarketingOpen(); default: return nullptr; } } WindowBase* openDetails(WindowDetail type, int32_t id) override { switch (type) { case WindowDetail::banner: return BannerOpen(id); case WindowDetail::newCampaign: return NewCampaignOpen(id); case WindowDetail::demolishRide: return RideDemolishPromptOpen(*GetRide(RideId::FromUnderlying(id))); case WindowDetail::refurbishRide: return RideRefurbishPromptOpen(*GetRide(RideId::FromUnderlying(id))); case WindowDetail::sign: return SignOpen(id); case WindowDetail::signSmall: return SignSmallOpen(id); case WindowDetail::player: return PlayerOpen(id); default: return nullptr; } } WindowBase* ShowError(StringId title, StringId message, const Formatter& args, bool autoClose /* = false */) override { return ErrorOpen(title, message, args, autoClose); } WindowBase* ShowError(std::string_view title, std::string_view message, bool autoClose /* = false */) override { return ErrorOpen(title, message, autoClose); } WindowBase* OpenIntent(Intent* intent) override { switch (intent->GetWindowClass()) { case WindowClass::peep: return GuestOpen(static_cast(intent->GetPointerExtra(INTENT_EXTRA_PEEP))); case WindowClass::firePrompt: return StaffFirePromptOpen(static_cast(intent->GetPointerExtra(INTENT_EXTRA_PEEP))); case WindowClass::installTrack: return InstallTrackOpen(intent->GetStringExtra(INTENT_EXTRA_PATH).c_str()); case WindowClass::guestList: return GuestListOpenWithFilter( static_cast(intent->GetSIntExtra(INTENT_EXTRA_GUEST_LIST_FILTER)), intent->GetSIntExtra(INTENT_EXTRA_RIDE_ID)); case WindowClass::loadsave: { auto action = intent->GetEnumExtra(INTENT_EXTRA_LOADSAVE_ACTION); auto type = intent->GetEnumExtra(INTENT_EXTRA_LOADSAVE_TYPE); std::string defaultPath = intent->GetStringExtra(INTENT_EXTRA_PATH); LoadSaveCallback callback = reinterpret_cast( intent->GetCloseCallbackExtra(INTENT_EXTRA_CALLBACK)); TrackDesign* trackDesign = static_cast(intent->GetPointerExtra(INTENT_EXTRA_TRACK_DESIGN)); auto* w = FileBrowser::OpenPreferred( action, type, defaultPath, [callback](ModalResult result, std::string_view path) { if (callback != nullptr) { callback(result, std::string(path).c_str()); } }, trackDesign); return w; } case WindowClass::manageTrackDesign: return TrackManageOpen(static_cast(intent->GetPointerExtra(INTENT_EXTRA_TRACK_DESIGN))); case WindowClass::networkStatus: { std::string message = intent->GetStringExtra(INTENT_EXTRA_MESSAGE); CloseCallback callback = intent->GetCloseCallbackExtra(INTENT_EXTRA_CALLBACK); return NetworkStatusOpen(message, callback); } case WindowClass::objectLoadError: { std::string path = intent->GetStringExtra(INTENT_EXTRA_PATH); auto objects = static_cast(intent->GetPointerExtra(INTENT_EXTRA_LIST)); size_t count = intent->GetUIntExtra(INTENT_EXTRA_LIST_COUNT); ObjectLoadErrorOpen(const_cast(path.c_str()), count, objects); return nullptr; } case WindowClass::ride: { const auto rideId = RideId::FromUnderlying(intent->GetSIntExtra(INTENT_EXTRA_RIDE_ID)); auto ride = GetRide(rideId); return ride == nullptr ? nullptr : RideMainOpen(*ride); } case WindowClass::trackDesignPlace: return TrackPlaceOpen(static_cast(intent->GetPointerExtra(INTENT_EXTRA_TRACK_DESIGN))); case WindowClass::trackDesignList: { RideSelection rideItem; rideItem.Type = intent->GetUIntExtra(INTENT_EXTRA_RIDE_TYPE); rideItem.EntryIndex = intent->GetUIntExtra(INTENT_EXTRA_RIDE_ENTRY_INDEX); return TrackListOpen(rideItem); } case WindowClass::scenarioSelect: return ScenarioselectOpen( reinterpret_cast(intent->GetCloseCallbackExtra(INTENT_EXTRA_CALLBACK))); case WindowClass::null: // Intent does not hold a window class break; default: Guard::Assert(false, "OpenIntent was called for an unhandled window class."); break; } switch (intent->GetAction()) { case INTENT_ACTION_NEW_RIDE_OF_TYPE: { // Open ride list window auto w = NewRideOpen(); // Switch to right tab and scroll to ride location RideSelection rideItem; rideItem.Type = intent->GetUIntExtra(INTENT_EXTRA_RIDE_TYPE); rideItem.EntryIndex = intent->GetUIntExtra(INTENT_EXTRA_RIDE_ENTRY_INDEX); WindowNewRideFocus(rideItem); return w; } case INTENT_ACTION_NEW_SCENERY: { // Check if window is already open auto* window = BringToFrontByClass(WindowClass::scenery); if (window == nullptr) ToggleSceneryWindow(); // Switch to new scenery tab WindowScenerySetSelectedTab(intent->GetUIntExtra(INTENT_EXTRA_SCENERY_GROUP_ENTRY_INDEX)); return window; } case INTENT_ACTION_PROGRESS_OPEN: { std::string message = intent->GetStringExtra(INTENT_EXTRA_MESSAGE); CloseCallback callback = intent->GetCloseCallbackExtra(INTENT_EXTRA_CALLBACK); return ProgressWindowOpen(message, callback); } case INTENT_ACTION_PROGRESS_SET: { uint32_t currentProgress = intent->GetUIntExtra(INTENT_EXTRA_PROGRESS_OFFSET); uint32_t totalCount = intent->GetUIntExtra(INTENT_EXTRA_PROGRESS_TOTAL); StringId format = intent->GetUIntExtra(INTENT_EXTRA_STRING_ID); ProgressWindowSet(currentProgress, totalCount, format); return nullptr; } case INTENT_ACTION_PROGRESS_CLOSE: { ProgressWindowClose(); return nullptr; } case INTENT_ACTION_NULL: // Intent does not hold an intent action break; default: Guard::Assert(false, "OpenIntent was called for an unhandled intent action."); break; } switch (intent->GetWindowDetail()) { case WindowDetail::vehicle: return RideOpenVehicle(static_cast(intent->GetPointerExtra(INTENT_EXTRA_VEHICLE))); case WindowDetail::track: return RideOpenTrack(static_cast(intent->GetPointerExtra(INTENT_EXTRA_TILE_ELEMENT))); case WindowDetail::null: // Intent does not hold an window detail break; default: Guard::Assert(false, "OpenIntent was called for an unhandled window detail."); break; } Console::Error::WriteLine("Unhandled window class for intent (%d)", intent->GetWindowClass()); return nullptr; } void BroadcastIntent(const Intent& intent) override { switch (intent.GetAction()) { case INTENT_ACTION_MAP: WindowMapReset(); break; case INTENT_ACTION_REFRESH_NEW_RIDES: WindowNewRideInitVars(); break; case INTENT_ACTION_REFRESH_CAMPAIGN_RIDE_LIST: { WindowCampaignRefreshRides(); break; } case INTENT_ACTION_REFRESH_RIDE_LIST: { auto window = FindByClass(WindowClass::rideList); if (window != nullptr) { WindowRideListRefreshList(window); } break; } case INTENT_ACTION_UPDATE_MAZE_CONSTRUCTION: WindowMazeConstructionUpdatePressedWidgets(); break; case INTENT_ACTION_RIDE_CONSTRUCTION_FOCUS: { auto rideIndex = intent.GetUIntExtra(INTENT_EXTRA_RIDE_ID); auto w = FindByClass(WindowClass::rideConstruction); if (w == nullptr || w->number != static_cast(rideIndex)) { CloseConstructionWindows(); _currentRideIndex = RideId::FromUnderlying(rideIndex); OpenWindow(WindowClass::rideConstruction); } else { RideConstructionInvalidateCurrentTrack(); _currentRideIndex = RideId::FromUnderlying(rideIndex); } break; } case INTENT_ACTION_RIDE_CONSTRUCTION_UPDATE_PIECES: WindowRideConstructionUpdateEnabledTrackPieces(); break; case INTENT_ACTION_RIDE_CONSTRUCTION_UPDATE_ACTIVE_ELEMENTS: WindowRideConstructionUpdateActiveElementsImpl(); break; case INTENT_ACTION_INIT_SCENERY: WindowSceneryInit(); break; case INTENT_ACTION_SET_DEFAULT_SCENERY_CONFIG: WindowScenerySetDefaultPlacementConfiguration(); break; case INTENT_ACTION_REFRESH_SCENERY: WindowSceneryResetSelectedSceneryItems(); break; case INTENT_ACTION_INVALIDATE_TICKER_NEWS: WindowGameBottomToolbarInvalidateNewsItem(); break; case INTENT_ACTION_REFRESH_GUEST_LIST: WindowGuestListRefreshList(); break; case INTENT_ACTION_REFRESH_STAFF_LIST: { WindowStaffListRefresh(); break; } case INTENT_ACTION_CLEAR_TILE_INSPECTOR_CLIPBOARD: WindowTileInspectorClearClipboard(); break; case INTENT_ACTION_INVALIDATE_VEHICLE_WINDOW: { auto vehicle = static_cast(intent.GetPointerExtra(INTENT_EXTRA_VEHICLE)); if (vehicle != nullptr) { WindowRideInvalidateVehicle(*vehicle); } break; } case INTENT_ACTION_RIDE_PAINT_RESET_VEHICLE: { auto rideIndex = intent.GetUIntExtra(INTENT_EXTRA_RIDE_ID); WindowRidePaintResetVehicle(RideId::FromUnderlying(rideIndex)); break; } case INTENT_ACTION_UPDATE_CLIMATE: gToolbarDirtyFlags |= BTM_TB_DIRTY_FLAG_CLIMATE; InvalidateByClass(WindowClass::guestList); break; case INTENT_ACTION_UPDATE_GUEST_COUNT: gToolbarDirtyFlags |= BTM_TB_DIRTY_FLAG_PEEP_COUNT; InvalidateByClass(WindowClass::guestList); InvalidateByClass(WindowClass::parkInformation); WindowGuestListRefreshList(); break; case INTENT_ACTION_UPDATE_PARK_RATING: gToolbarDirtyFlags |= BTM_TB_DIRTY_FLAG_PARK_RATING; InvalidateByClass(WindowClass::parkInformation); break; case INTENT_ACTION_UPDATE_DATE: gToolbarDirtyFlags |= BTM_TB_DIRTY_FLAG_DATE; break; case INTENT_ACTION_UPDATE_CASH: InvalidateByClass(WindowClass::finances); gToolbarDirtyFlags |= BTM_TB_DIRTY_FLAG_MONEY; break; case INTENT_ACTION_UPDATE_BANNER: { WindowNumber bannerIndex = static_cast(intent.GetUIntExtra(INTENT_EXTRA_BANNER_INDEX)); WindowBase* w = FindByNumber(WindowClass::banner, bannerIndex); if (w != nullptr) { w->invalidate(); } break; } case INTENT_ACTION_UPDATE_RESEARCH: InvalidateByClass(WindowClass::finances); InvalidateByClass(WindowClass::research); break; case INTENT_ACTION_UPDATE_VEHICLE_SOUNDS: OpenRCT2::Audio::UpdateVehicleSounds(); break; case INTENT_ACTION_SET_MAP_TOOLTIP: { auto ft = static_cast(intent.GetPointerExtra(INTENT_EXTRA_FORMATTER)); if (ft != nullptr) { SetMapTooltip(*ft); } break; } case INTENT_ACTION_TILE_MODIFY: { InvalidateByClass(WindowClass::tileInspector); break; } case INTENT_ACTION_REMOVE_PROVISIONAL_ELEMENTS: ProvisionalElementsRemove(); break; case INTENT_ACTION_RESTORE_PROVISIONAL_ELEMENTS: ProvisionalElementsRestore(); break; case INTENT_ACTION_REMOVE_PROVISIONAL_FOOTPATH: FootpathRemoveProvisional(); break; case INTENT_ACTION_REMOVE_PROVISIONAL_TRACK_PIECE: RideRemoveProvisionalTrackPiece(); break; default: break; } } void ForceClose(WindowClass windowClass) override { switch (windowClass) { case WindowClass::editorObjectSelection: EditorObjectSelectionClose(); break; case WindowClass::networkStatus: WindowNetworkStatusClose(); break; case WindowClass::progressWindow: ProgressWindowClose(); break; default: break; } } void UpdateMapTooltip() override { WindowMapTooltipUpdateVisibility(); } void HandleInput() override { GameHandleInput(); } void HandleKeyboard(bool isTitle) override { auto& inputManager = GetInputManager(); inputManager.Process(); } std::string GetKeyboardShortcutString(std::string_view shortcutId) override { auto& shortcutManager = GetShortcutManager(); auto* shortcut = shortcutManager.GetShortcut(shortcutId); return shortcut != nullptr ? shortcut->GetDisplayString() : std::string(); } void SetMainView(const ScreenCoordsXY& viewPos, ZoomLevel zoom, int32_t rotation) override { auto mainWindow = WindowGetMain(); if (mainWindow != nullptr) { auto viewport = WindowGetViewport(mainWindow); mainWindow->viewportTargetSprite = EntityId::GetNull(); mainWindow->savedViewPos = viewPos; viewport->zoom = zoom; viewport->rotation = rotation; mainWindow->savedViewPos.x -= viewport->ViewWidth() / 2; mainWindow->savedViewPos.y -= viewport->ViewHeight() / 2; mainWindow->invalidate(); } } void UpdateMouseWheel() override { WindowAllWheelInput(); } WindowBase* GetOwner(const Viewport* viewport) override { for (auto& w : gWindowList) { if (w->viewport == viewport) { return w.get(); } } return nullptr; } static bool WindowFitsBetweenOthers(const ScreenCoordsXY& loc, const ScreenSize windowSize) { for (auto& w : gWindowList) { if (w->flags.has(WindowFlag::dead)) continue; if (w->flags.has(WindowFlag::stickToBack)) continue; if (loc.x + windowSize.width <= w->windowPos.x) continue; if (loc.x >= w->windowPos.x + w->width) continue; if (loc.y + windowSize.height <= w->windowPos.y) continue; if (loc.y >= w->windowPos.y + w->height) continue; return false; } return true; } static bool WindowFitsWithinSpace(const ScreenCoordsXY& loc, ScreenSize windowSize) { if (loc.x < 0) return false; if (loc.y <= kTopToolbarHeight && gLegacyScene != LegacyScene::titleSequence) return false; if (loc.x + windowSize.width > ContextGetWidth()) return false; if (loc.y + windowSize.height > ContextGetHeight()) return false; return WindowFitsBetweenOthers(loc, windowSize); } static bool WindowFitsOnScreen(const ScreenCoordsXY& loc, const ScreenSize windowSize) { uint16_t screenWidth = ContextGetWidth(); uint16_t screenHeight = ContextGetHeight(); int32_t unk; unk = -(windowSize.width / 4); if (loc.x < unk) return false; unk = screenWidth + (unk * 2); if (loc.x > unk) return false; if (loc.y <= kTopToolbarHeight && gLegacyScene != LegacyScene::titleSequence) return false; unk = screenHeight - (windowSize.height / 4); if (loc.y > unk) return false; return WindowFitsBetweenOthers(loc, windowSize); } static ScreenCoordsXY ClampWindowToScreen( const ScreenCoordsXY& pos, const ScreenSize screenSize, const ScreenSize windowSize) { auto screenPos = pos; if (windowSize.width > screenSize.width || screenPos.x < 0) screenPos.x = 0; else if (screenPos.x + windowSize.width > screenSize.width) screenPos.x = screenSize.width - windowSize.width; auto toolbarAllowance = gLegacyScene == LegacyScene::titleSequence ? 0 : (kTopToolbarHeight + 1); if (windowSize.height - toolbarAllowance > screenSize.height || screenPos.y < toolbarAllowance) screenPos.y = toolbarAllowance; else if (screenPos.y + windowSize.height - toolbarAllowance > screenSize.height) screenPos.y = screenSize.height + toolbarAllowance - windowSize.height; return screenPos; } static ScreenCoordsXY GetAutoPositionForNewWindow(const ScreenSize windowSize) { auto& uiContext = GetContext()->GetUiContext(); ScreenSize screenSize = { uiContext.GetWidth(), uiContext.GetHeight() }; // Place window in an empty corner of the screen const ScreenCoordsXY cornerPositions[] = { { 0, 30 }, // topLeft { screenSize.width - windowSize.width, 30 }, // topRight { 0, screenSize.height - 34 - windowSize.height }, // bottomLeft { screenSize.width - windowSize.width, screenSize.height - 34 - windowSize.height }, // bottomRight }; for (const auto& cornerPos : cornerPositions) { if (WindowFitsWithinSpace(cornerPos, windowSize)) { return ClampWindowToScreen(cornerPos, screenSize, windowSize); } } // Place window next to another for (auto& w : gWindowList) { if (w->flags.has(WindowFlag::dead)) continue; if (w->flags.has(WindowFlag::stickToBack)) continue; const ScreenCoordsXY offsets[] = { { w->width + 2, 0 }, { -w->width - 2, 0 }, { 0, w->height + 2 }, { 0, -w->height - 2 }, { w->width + 2, -w->height - 2 }, { -w->width - 2, -w->height - 2 }, { w->width + 2, w->height + 2 }, { -w->width - 2, w->height + 2 }, }; for (const auto& offset : offsets) { auto screenPos = w->windowPos + offset; if (WindowFitsWithinSpace(screenPos, windowSize)) { return ClampWindowToScreen(screenPos, screenSize, windowSize); } } } // Overlap for (auto& w : gWindowList) { if (w->flags.has(WindowFlag::dead)) continue; if (w->flags.has(WindowFlag::stickToBack)) continue; const ScreenCoordsXY offsets[] = { { w->width + 2, 0 }, { -w->width - 2, 0 }, { 0, w->height + 2 }, { 0, -w->height - 2 }, }; for (const auto& offset : offsets) { auto screenPos = w->windowPos + offset; if (WindowFitsOnScreen(screenPos, windowSize)) { return ClampWindowToScreen(screenPos, screenSize, windowSize); } } } // Cascade auto screenPos = ScreenCoordsXY{ 0, 30 }; for (auto& w : gWindowList) { if (screenPos == w->windowPos) { screenPos.x += 5; screenPos.y += 5; } } return ClampWindowToScreen(screenPos, screenSize, windowSize); } static ScreenCoordsXY GetCentrePositionForNewWindow(ScreenSize size) { auto& uiContext = GetContext()->GetUiContext(); auto screenWidth = uiContext.GetWidth(); auto screenHeight = uiContext.GetHeight(); return ScreenCoordsXY{ (screenWidth - size.width) / 2, std::max(kTopToolbarHeight + 1, (screenHeight - size.height) / 2) }; } WindowBase* Create( std::unique_ptr&& wp, WindowClass cls, ScreenCoordsXY pos, ScreenSize windowSize, WindowFlags flags) override { windowSize.height += wp->getTitleBarDiffTarget(); if (flags.has(WindowFlag::autoPosition)) { if (flags.has(WindowFlag::centreScreen)) { pos = GetCentrePositionForNewWindow(windowSize); } else { pos = GetAutoPositionForNewWindow(windowSize); } } windowSize.height -= wp->getTitleBarDiffTarget(); // Check if there are any window slots left // include kWindowLimitReserved for items such as the main viewport and toolbars to not appear to be counted. if (gWindowList.size() >= static_cast(Config::Get().general.WindowLimit + kWindowLimitReserved)) { // Close least recently used window for (auto& w : gWindowList) { if (w->flags.has(WindowFlag::dead)) continue; if (!(w->flags.hasAny(WindowFlag::stickToBack, WindowFlag::stickToFront, WindowFlag::noAutoClose))) { Close(*w.get()); break; } } } // Find right position to insert new window auto itDestPos = gWindowList.end(); if (flags.has(WindowFlag::stickToBack)) { for (auto it = gWindowList.begin(); it != gWindowList.end(); it++) { if ((*it)->flags.has(WindowFlag::dead)) continue; if (!((*it)->flags.has(WindowFlag::stickToBack))) { itDestPos = it; } } } else if (!(flags.has(WindowFlag::stickToFront))) { for (auto it = gWindowList.rbegin(); it != gWindowList.rend(); it++) { if ((*it)->flags.has(WindowFlag::dead)) continue; if (!((*it)->flags.has(WindowFlag::stickToFront))) { itDestPos = it.base(); break; } } } // Setup window wp->classification = cls; wp->flags = flags; // Play sounds and flash the window if (!(flags.hasAny(WindowFlag::stickToBack, WindowFlag::stickToFront))) { wp->flash(); OpenRCT2::Audio::Play(OpenRCT2::Audio::SoundId::WindowOpen, 0, pos.x + (windowSize.width / 2)); } wp->windowPos = pos; wp->width = windowSize.width; wp->height = windowSize.height; wp->minWidth = windowSize.width; wp->maxWidth = windowSize.width; wp->minHeight = windowSize.height; wp->maxHeight = windowSize.height; wp->focus = std::nullopt; ColourSchemeUpdate(wp.get()); wp->invalidate(); wp->onOpen(); auto itNew = gWindowList.insert(itDestPos, std::move(wp)); return itNew->get(); } /** * Closes the specified window. * rct2: 0x006ECD4C */ void Close(WindowBase& w) override { if (!w.canClose()) { // Something's preventing this window from closing -- bail out early return; } w.onClose(); // Remove viewport w.removeViewport(); // Invalidate the window (area) w.invalidate(); w.flags |= WindowFlag::dead; } void CloseSurplus(int32_t cap, WindowClass avoid_classification) override { // find the amount of windows that are currently open auto count = static_cast(gWindowList.size()); // difference between amount open and cap = amount to close auto diff = count - kWindowLimitReserved - cap; for (auto i = 0; i < diff; i++) { // iterates through the list until it finds the newest window, or a window that can be closed WindowBase* foundW{}; for (auto& w : gWindowList) { if (w->flags.has(WindowFlag::dead)) continue; if (!(w->flags.hasAny(WindowFlag::stickToBack, WindowFlag::stickToFront, WindowFlag::noAutoClose))) { foundW = w.get(); break; } } // skip window if window matches specified WindowClass (as user may be modifying via options) if (avoid_classification != WindowClass::null && foundW != nullptr && foundW->classification == avoid_classification) { continue; } if (foundW != nullptr) Close(*foundW); } } template void CloseByCondition(TPred pred, uint32_t flags = WindowCloseFlags::None) { // Collect windows to close first to avoid iterator invalidation // when Close() might trigger window creation via OnClose() std::vector windowsToClose; for (auto it = gWindowList.rbegin(); it != gWindowList.rend(); ++it) { auto& wnd = *(*it); if (wnd.flags.has(WindowFlag::dead)) continue; if (pred(&wnd)) { windowsToClose.push_back(&wnd); if (flags & WindowCloseFlags::CloseSingle) { break; } } } // Now close the collected windows for (auto* wnd : windowsToClose) { if (!(wnd->flags.has(WindowFlag::dead))) { Close(*wnd); } } } /** * Closes all windows with the specified window class. * rct2: 0x006ECCF4 */ void CloseByClass(WindowClass cls) override { CloseByCondition([&](WindowBase* w) -> bool { return w->classification == cls; }); } /** * Closes all windows with specified window class and number. * rct2: 0x006ECCF4 */ void CloseByNumber(WindowClass cls, WindowNumber number) override { CloseByCondition([cls, number](WindowBase* w) -> bool { return w->classification == cls && w->number == number; }); } // TODO: Refactor this to use variant once the new window class is done. void CloseByNumber(WindowClass cls, EntityId number) override { CloseByNumber(cls, static_cast(number.ToUnderlying())); } /** * Closes the top-most window * * rct2: 0x006E403C */ void CloseTop() override { CloseByClass(WindowClass::dropdown); if (gLegacyScene == LegacyScene::scenarioEditor) { if (getGameState().editorStep != EditorStep::LandscapeEditor) return; } auto pred = [](WindowBase* w) -> bool { return !(w->flags.hasAny(WindowFlag::stickToBack, WindowFlag::stickToFront)); }; CloseByCondition(pred, WindowCloseFlags::CloseSingle); } /** * Closes all open windows * * rct2: 0x006EE927 */ void CloseAll() override { CloseByClass(WindowClass::dropdown); CloseByCondition( [](WindowBase* w) -> bool { return !(w->flags.hasAny(WindowFlag::stickToBack, WindowFlag::stickToFront)); }); } void CloseAllExceptClass(WindowClass cls) override { CloseByClass(WindowClass::dropdown); CloseByCondition([cls](WindowBase* w) -> bool { return w->classification != cls && !(w->flags.hasAny(WindowFlag::stickToBack, WindowFlag::stickToFront)); }); } /** * Closes all windows, save for those having any of the passed flags. */ void CloseAllExceptFlags(WindowFlags flags) override { CloseByCondition([flags](WindowBase* w) -> bool { return !(w->flags.hasAny(flags)); }); } /** * Closes all windows except the specified window number and class. */ void CloseAllExceptNumberAndClass(WindowNumber number, WindowClass cls) override { CloseByClass(WindowClass::dropdown); CloseByCondition([cls, number](WindowBase* w) -> bool { return ( !(w->number == number && w->classification == cls) && !(w->flags.hasAny(WindowFlag::stickToBack, WindowFlag::stickToFront))); }); } /** * * rct2: 0x006CBCC3 */ void CloseConstructionWindows() override { CloseByClass(WindowClass::rideConstruction); CloseByClass(WindowClass::footpath); CloseByClass(WindowClass::trackDesignList); CloseByClass(WindowClass::trackDesignPlace); } /** * Finds the first window with the specified window class. * rct2: 0x006EA8A0 * @returns the window or nullptr if no window was found. */ WindowBase* FindByClass(WindowClass cls) override { for (auto& w : gWindowList) { if (w->flags.has(WindowFlag::dead)) continue; if (w->classification == cls) { return w.get(); } } return nullptr; } /** * Finds the first window with the specified window class and number. * rct2: 0x006EA8A0 * @returns the window or nullptr if no window was found. */ WindowBase* FindByNumber(WindowClass cls, WindowNumber number) override { for (auto& w : gWindowList) { if (w->flags.has(WindowFlag::dead)) continue; if (w->classification == cls && w->number == number) { return w.get(); } } return nullptr; } // TODO: Use variant for this once the window framework is done. WindowBase* FindByNumber(WindowClass cls, EntityId id) override { return FindByNumber(cls, static_cast(id.ToUnderlying())); } /** * * rct2: 0x006EA845 */ WindowBase* FindFromPoint(const ScreenCoordsXY& screenCoords) override { for (auto it = gWindowList.rbegin(); it != gWindowList.rend(); it++) { auto& w = *it; if (w->flags.has(WindowFlag::dead)) continue; if (screenCoords.x < w->windowPos.x || screenCoords.x >= w->windowPos.x + w->width || screenCoords.y < w->windowPos.y || screenCoords.y >= w->windowPos.y + w->height) continue; if (w->flags.has(WindowFlag::noBackground)) { auto widgetIndex = FindWidgetFromPoint(*w.get(), screenCoords); if (widgetIndex == kWidgetIndexNull) continue; } return w.get(); } return nullptr; } /** * * rct2: 0x006EA594 * returns widget_index if found, -1 otherwise */ WidgetIndex FindWidgetFromPoint(WindowBase& w, const ScreenCoordsXY& screenCoords) override { // Invalidate the window w.onPrepareDraw(); // Find the widget at point x, y WidgetIndex widget_index = kWidgetIndexNull; for (auto i = 0u; i < w.widgets.size(); i++) { const auto& widget = w.widgets[i]; if (widget.type != WidgetType::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) { widget_index = i; } } } // Return next widget if a dropdown if (widget_index != kWidgetIndexNull) { const auto& widget = w.widgets[widget_index]; if (widget.type == WidgetType::dropdownMenu) widget_index++; } // Return the widget index return widget_index; } /** * Invalidates the specified window. * rct2: 0x006EB13A */ template static void InvalidateByCondition(TPred pred) { WindowVisitEach([pred](WindowBase* w) { if (pred(w)) { w->invalidate(); } }); } /** * Invalidates all windows with the specified window class. * rct2: 0x006EC3AC */ void InvalidateByClass(WindowClass cls) override { InvalidateByCondition([cls](WindowBase* w) -> bool { return w->classification == cls; }); } /** * Invalidates all windows with the specified window class and number. * rct2: 0x006EC3AC */ void InvalidateByNumber(WindowClass cls, WindowNumber number) override { InvalidateByCondition([cls, number](WindowBase* w) -> bool { return w->classification == cls && w->number == number; }); } // TODO: Use variant for this once the window framework is done. void InvalidateByNumber(WindowClass cls, EntityId id) override { InvalidateByNumber(cls, static_cast(id.ToUnderlying())); } /** * Invalidates all windows. */ void InvalidateAll() override { WindowVisitEach([](WindowBase* w) { w->invalidate(); }); } /** * Invalidates the specified widget of a window. * rct2: 0x006EC402 */ void InvalidateWidget(WindowBase& w, WidgetIndex widgetIndex) override { if (w.widgets.empty()) { // This might be called before the window is fully created. return; } if (static_cast(widgetIndex) >= w.widgets.size()) { return; } const auto& widget = w.widgets[widgetIndex]; if (widget.left == -2) return; GfxSetDirtyBlocks( { { w.windowPos + ScreenCoordsXY{ widget.left, widget.top } }, { w.windowPos + ScreenCoordsXY{ widget.right + 1, widget.bottom + 1 } } }); } /** * Invalidates the specified widget of all windows that match the specified window class. */ void InvalidateWidgetByClass(WindowClass cls, WidgetIndex widgetIndex) override { WindowVisitEach([this, cls, widgetIndex](WindowBase* w) { if (w->classification == cls) { InvalidateWidget(*w, widgetIndex); } }); } /** * Invalidates the specified widget of all windows that match the specified window class and number. * rct2: 0x006EC3AC */ void InvalidateWidgetByNumber(WindowClass cls, WindowNumber number, WidgetIndex widgetIndex) override { WindowVisitEach([this, cls, number, widgetIndex](WindowBase* w) { if (w->classification == cls && w->number == number) { InvalidateWidget(*w, widgetIndex); } }); } /** * * rct2: 0x006ECDA4 */ WindowBase* BringToFront(WindowBase& w) override { if (!(w.flags.hasAny(WindowFlag::stickToBack, WindowFlag::stickToFront))) { auto itSourcePos = WindowGetIterator(&w); if (itSourcePos != gWindowList.end()) { // Insert in front of the first non-stick-to-front window auto itDestPos = gWindowList.begin(); for (auto it = gWindowList.rbegin(); it != gWindowList.rend(); it++) { auto& w2 = *it; if (w2->flags.has(WindowFlag::dead)) { continue; } if (!(w2->flags.has(WindowFlag::stickToFront))) { // base() returns the next element in the list, so we need to decrement it. itDestPos = std::prev(it.base()); break; } } if (itSourcePos != itDestPos) { std::iter_swap(itSourcePos, itDestPos); } w.invalidate(); if (w.windowPos.x + w.width < 20) { int32_t i = 20 - w.windowPos.x; w.windowPos.x += i; if (w.viewport != nullptr) w.viewport->pos.x += i; w.invalidate(); } } } return &w; } WindowBase* BringToFrontByClass(WindowClass cls) override { WindowBase* w = FindByClass(cls); if (w != nullptr) { w->flash(); w->invalidate(); w = BringToFront(*w); } return w; } /** * * rct2: 0x006ED78A */ WindowBase* BringToFrontByNumber(WindowClass cls, WindowNumber number) override { WindowBase* w = FindByNumber(cls, number); if (w != nullptr) { w->flash(); w->invalidate(); w = BringToFront(*w); } return w; } }; std::unique_ptr OpenRCT2::Ui::CreateWindowManager() { return std::make_unique(); }