/***************************************************************************** * 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 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace OpenRCT2; // clang-format off enum { WIDX_BACKGROUND, WIDX_TITLE, WIDX_CLOSE, WIDX_CONTENT_PANEL, WIDX_SCROLL, WIDX_OPEN_URL, }; static constexpr const int32_t WW = 500; static constexpr const int32_t WH = 400; static constexpr const rct_string_id WINDOW_TITLE = STR_CHANGELOG_TITLE; constexpr int32_t MIN_WW = 300; constexpr int32_t MIN_WH = 250; static rct_widget _windowChangelogWidgets[] = { WINDOW_SHIM(WINDOW_TITLE, WW, WH), MakeWidget({0, 14}, {500, 382}, WindowWidgetType::Resize, WindowColour::Secondary ), // content panel MakeWidget({3, 16}, {495, 366}, WindowWidgetType::Scroll, WindowColour::Secondary, SCROLL_BOTH ), // scroll area MakeWidget({3, 473}, {300, 14}, WindowWidgetType::Placeholder, WindowColour::Secondary, STR_NEW_RELEASE_DOWNLOAD_PAGE), // changelog button WIDGETS_END, }; // clang-format on class ChangelogWindow final : public Window { const NewVersionInfo* _newVersionInfo; std::vector _changelogLines; int32_t _changelogLongestLineWidth = 0; int _personality = 0; public: /** * @brief Retrieves the changelog contents. */ const std::string GetChangelogText() { auto path = GetChangelogPath(); auto fs = std::ifstream(fs::u8path(path), std::ios::in); if (!fs.is_open()) { throw std::runtime_error("Unable to open " + path); } return std::string((std::istreambuf_iterator(fs)), std::istreambuf_iterator()); } /** * @brief Set the Changelog Window's Personality, should be called just after creation. Returns true on success * * @param personality */ bool SetPersonality(int personality) { switch (personality) { case WV_NEW_VERSION_INFO: if (!GetContext()->HasNewVersionInfo()) { return false; } _personality = WV_NEW_VERSION_INFO; NewVersionProcessInfo(); widgets[WIDX_OPEN_URL].type = WindowWidgetType::Button; return true; case WV_CHANGELOG: if (!ReadChangelogFile()) { return false; } _personality = WV_CHANGELOG; return true; default: log_error("Invalid personality for changelog window: %d", personality); return false; } } void OnOpen() override { widgets = _windowChangelogWidgets; WindowInitScrollWidgets(this); min_width = MIN_WW; min_height = MIN_WH; max_width = MIN_WW; max_height = MIN_WH; } void OnResize() override { int32_t screenWidth = context_get_width(); int32_t screenHeight = context_get_height(); max_width = (screenWidth * 4) / 5; max_height = (screenHeight * 4) / 5; min_width = MIN_WW; min_height = MIN_WH; auto download_button_width = widgets[WIDX_OPEN_URL].width(); widgets[WIDX_OPEN_URL].left = (width - download_button_width) / 2; widgets[WIDX_OPEN_URL].right = widgets[WIDX_OPEN_URL].left + download_button_width; if (width < min_width) { Invalidate(); width = min_width; } if (height < min_height) { Invalidate(); height = min_height; } } void OnPrepareDraw() override { widgets[WIDX_BACKGROUND].right = width - 1; widgets[WIDX_BACKGROUND].bottom = height - 1; widgets[WIDX_TITLE].right = width - 2; widgets[WIDX_CLOSE].left = width - 13; widgets[WIDX_CLOSE].right = width - 3; widgets[WIDX_CONTENT_PANEL].right = width - 1; widgets[WIDX_CONTENT_PANEL].bottom = height - 1; widgets[WIDX_SCROLL].right = width - 3; widgets[WIDX_SCROLL].bottom = height - 22; widgets[WIDX_OPEN_URL].bottom = height - 5; widgets[WIDX_OPEN_URL].top = height - 19; } void OnMouseUp(rct_widgetindex widgetIndex) override { switch (widgetIndex) { case WIDX_CLOSE: Close(); break; case WIDX_OPEN_URL: if (_newVersionInfo != nullptr) { GetContext()->GetUiContext()->OpenURL(_newVersionInfo->url); } else { log_error("Cannot open URL: NewVersionInfo for ChangelogWindow is undefined!"); } break; } } void OnScrollDraw(int32_t scrollIndex, rct_drawpixelinfo& dpi) override { const int32_t lineHeight = font_get_line_height(FontSpriteBase::MEDIUM); ScreenCoordsXY screenCoords(3, 3 - lineHeight); for (const auto& line : _changelogLines) { screenCoords.y += lineHeight; if (screenCoords.y + lineHeight < dpi.y || screenCoords.y >= dpi.y + dpi.height) continue; gfx_draw_string(&dpi, screenCoords, line.c_str(), { colours[0] }); } } ScreenSize OnScrollGetSize(int32_t scrollIndex) override { return ScreenSize( _changelogLongestLineWidth + 4, static_cast(_changelogLines.size()) * font_get_line_height(FontSpriteBase::MEDIUM)); } // TODO: This probably should be a utility function defined elsewhere for reusability /** * @brief Reimplementation of Window's GetCentrePositionForNewWindow for ChangelogWindow. * * @return ScreenCoordsXY */ static ScreenCoordsXY GetCentrePositionForNewWindow(int32_t width, int32_t height) { auto uiContext = GetContext()->GetUiContext(); auto screenWidth = uiContext->GetWidth(); auto screenHeight = uiContext->GetHeight(); return ScreenCoordsXY{ (screenWidth - width) / 2, std::max(TOP_TOOLBAR_HEIGHT + 1, (screenHeight - height) / 2) }; } private: /** * @brief Converts NewVersionInfo into changelog lines * */ void NewVersionProcessInfo() { _newVersionInfo = GetContext()->GetNewVersionInfo(); if (_newVersionInfo != nullptr) { char version_info[256]; const char* version_info_ptr = _newVersionInfo->name.c_str(); format_string(version_info, 256, STR_NEW_RELEASE_VERSION_INFO, &version_info_ptr); _changelogLines.emplace_back(version_info); _changelogLines.emplace_back(""); ProcessChangelogText(_newVersionInfo->changelog); } else { log_error("ChangelogWindow: Could not process NewVersionInfo, result was undefined"); } } /** * @brief Get the absolute path for the changelog file * * @return std::string */ std::string GetChangelogPath() { auto env = GetContext()->GetPlatformEnvironment(); return env->GetFilePath(PATHID::CHANGELOG); } /** * @brief Attempts to read the changelog file, returns true on success * */ bool ReadChangelogFile() { std::string _changelogText; try { _changelogText = GetChangelogText(); } catch (const std::bad_alloc&) { log_error("Unable to allocate memory for changelog.txt"); return false; } catch (const std::exception&) { log_error("Unable to read changelog.txt"); return false; } ProcessChangelogText(_changelogText); return true; } /** * @brief Ingests a string of text and splits it into lines for the changelog and updates the longest line width for * scrolling purposes * * @param text */ void ProcessChangelogText(const std::string& text) { std::string::size_type pos = 0; std::string::size_type prev = 0; while ((pos = text.find("\n", prev)) != std::string::npos) { _changelogLines.push_back(text.substr(prev, pos - prev)); prev = pos + 1; } // To get the last substring (or only, if delimiter is not found) _changelogLines.push_back(text.substr(prev)); _changelogLongestLineWidth = 0; for (const auto& line : _changelogLines) { int32_t linewidth = gfx_get_string_width(line.c_str(), FontSpriteBase::MEDIUM); _changelogLongestLineWidth = std::max(linewidth, _changelogLongestLineWidth); } } }; rct_window* WindowChangelogOpen(int personality) { auto* window = window_bring_to_front_by_class(WC_CHANGELOG); if (window == nullptr) { // Create a new centred window int32_t screenWidth = context_get_width(); int32_t screenHeight = context_get_height(); int32_t width = (screenWidth * 4) / 5; int32_t height = (screenHeight * 4) / 5; auto pos = ChangelogWindow::GetCentrePositionForNewWindow(width, height); auto* newWindow = WindowCreate(WC_CHANGELOG, pos, width, height, WF_RESIZABLE); newWindow->SetPersonality(personality); return newWindow; } return window; }