diff --git a/src/engine/localevent.cpp b/src/engine/localevent.cpp index eb13d2e01b5..0be2f9752c6 100644 --- a/src/engine/localevent.cpp +++ b/src/engine/localevent.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -737,6 +738,7 @@ bool LocalEvent::HandleEvents( const bool sleepAfterEventProcessing, const bool _mouseCursorRenderArea = {}; fheroes2::Display & display = fheroes2::Display::instance(); + fheroes2::BaseRenderEngine & engine = fheroes2::engine(); if ( fheroes2::RenderProcessor::instance().isCyclingUpdateRequired() ) { // To maintain color cycling animation we need to render the whole frame with an updated palette. @@ -852,6 +854,22 @@ bool LocalEvent::HandleEvents( const bool sleepAfterEventProcessing, const bool // As of now we have no logic for this so we at least log this event. DEBUG_LOG( DBG_ENGINE, DBG_WARN, "OS indicates low memory. Release some resources." ) break; + case SDL_DISPLAYEVENT: + if ( event.display.event == 3 ) { + DEBUG_LOG( DBG_ENGINE, DBG_INFO, "The display with id %d was disconnected " << event.display.display ); + engine.setDisplayIndex( engine.getCurrentDisplayIndex() ); + + fheroes2::Image temp; + assert( display.singleLayer() ); + temp._disableTransformLayer(); + fheroes2::Copy( display, temp ); + + const fheroes2::ResolutionInfo currentResolution{ display.width(), display.height(), display.screenSize().width, display.screenSize().height }; + + display.setResolution( currentResolution ); + fheroes2::Copy( temp, display ); + } + break; default: // If this assertion blows up then we included an event type but we didn't add logic for it. assert( eventTypeStatus.count( event.type ) == 0 ); @@ -1526,7 +1544,7 @@ void LocalEvent::setEventProcessingStates() setEventProcessingState( SDL_APP_DIDENTERFOREGROUND, false ); // SDL_LOCALECHANGED is supported from SDL 2.0.14 // TODO: we don't process this event. Add the logic. - setEventProcessingState( SDL_DISPLAYEVENT, false ); + setEventProcessingState( SDL_DISPLAYEVENT, true ); setEventProcessingState( SDL_WINDOWEVENT, true ); // TODO: verify why disabled processing of this event. setEventProcessingState( SDL_SYSWMEVENT, false ); diff --git a/src/engine/screen.cpp b/src/engine/screen.cpp index 1db13f69c68..cb837d79d7a 100644 --- a/src/engine/screen.cpp +++ b/src/engine/screen.cpp @@ -851,15 +851,16 @@ namespace std::vector getAvailableResolutions() const override { - static const std::vector filteredResolutions = []() { + int currentIndex = static_cast( getCurrentDisplayIndex() ); + std::vector filteredResolutions = [=]() { std::set resolutionSet; const int displayCount = SDL_GetNumVideoDisplays(); if ( displayCount > 0 ) { - const int displayModeCount = SDL_GetNumDisplayModes( 0 ); + const int displayModeCount = SDL_GetNumDisplayModes( currentIndex ); for ( int i = 0; i < displayModeCount; ++i ) { SDL_DisplayMode videoMode; - const int returnCode = SDL_GetDisplayMode( 0, i, &videoMode ); + const int returnCode = SDL_GetDisplayMode( currentIndex, i, &videoMode ); if ( returnCode < 0 ) { ERROR_LOG( "Failed to get display mode. The error value: " << returnCode << ", description: " << SDL_GetError() ) } @@ -938,6 +939,51 @@ namespace } } + uint8_t getCurrentDisplayIndex() const override + { + if ( uint8_t maxDis = getMaximumDisplays(); _displayIndex >= maxDis ) { + return maxDis - 1; + } + return _displayIndex; + } + + void setDisplayIndex( const uint8_t display ) override + { + if ( display == _displayIndex ) + return; + + SDL_GetWindowPosition( _window, &_prevWindowPos.x, &_prevWindowPos.y ); + + if ( display < getMaximumDisplays() ) { + _displayIndex = display; + } + else { + _displayIndex = 0; + } + + clear(); + } + + uint8_t getMaximumDisplays() const override + { + if ( !isAllocated() ) + // If engine is not allocated returning highest uint8_t number + return 255; + + const int displayCount = SDL_GetNumVideoDisplays(); + if ( displayCount > 0 ) { + return static_cast( displayCount ); + } + ERROR_LOG( "Failed to Get Number of Displays, description: " << SDL_GetError() ); + // There should be one display at least. + return 1; + } + + const char * getDisplayName( const uint8_t display ) override + { + return SDL_GetDisplayName( display ); + } + protected: RenderEngine() : _window( nullptr ) @@ -945,6 +991,7 @@ namespace , _renderer( nullptr ) , _texture( nullptr ) , _driverIndex( -1 ) + , _displayIndex( 0 ) , _prevWindowPos( SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED ) , _isVSyncEnabled( false ) { @@ -1045,9 +1092,7 @@ namespace const std::vector resolutions = getAvailableResolutions(); assert( !resolutions.empty() ); - if ( !resolutions.empty() ) { - resolutionInfo = GetNearestResolution( resolutionInfo, resolutions ); - } + resolutionInfo = GetNearestResolution( resolutionInfo, resolutions ); #if defined( ANDROID ) // Same as ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE @@ -1057,6 +1102,7 @@ namespace #endif uint32_t flags = SDL_WINDOW_SHOWN; + if ( isFullScreen ) { #if defined( _WIN32 ) if ( fheroes2::cursor().isSoftwareEmulation() ) { @@ -1071,8 +1117,8 @@ namespace } flags |= SDL_WINDOW_RESIZABLE; - - _window = SDL_CreateWindow( _previousWindowTitle.data(), _prevWindowPos.x, _prevWindowPos.y, resolutionInfo.screenWidth, resolutionInfo.screenHeight, flags ); + _window = SDL_CreateWindow( _previousWindowTitle.data(), SDL_WINDOWPOS_CENTERED_DISPLAY( getCurrentDisplayIndex() ), + SDL_WINDOWPOS_CENTERED_DISPLAY( getCurrentDisplayIndex() ), resolutionInfo.screenWidth, resolutionInfo.screenHeight, flags ); if ( _window == nullptr ) { ERROR_LOG( "Failed to create an application window of " << resolutionInfo.screenWidth << " x " << resolutionInfo.screenHeight << " size. The error: " << SDL_GetError() ) @@ -1155,12 +1201,19 @@ namespace return ( _window != nullptr ) && ( ( SDL_GetWindowFlags( _window ) & SDL_WINDOW_MOUSE_FOCUS ) == SDL_WINDOW_MOUSE_FOCUS ); } + bool isAllocated() const override + { + // We should check for at least one of the variables destroyed in Clear() + return _window != nullptr; + } + private: SDL_Window * _window; SDL_Surface * _surface; SDL_Renderer * _renderer; SDL_Texture * _texture; int _driverIndex; + uint8_t _displayIndex; std::string _previousWindowTitle; fheroes2::Point _prevWindowPos; @@ -1333,17 +1386,19 @@ namespace fheroes2 void Display::setResolution( ResolutionInfo info ) { if ( width() > 0 && height() > 0 && info.gameWidth == width() && info.gameHeight == height() && info.screenWidth == _screenSize.width - && info.screenHeight == _screenSize.height ) // nothing to resize + && info.screenHeight == _screenSize.height && _engine->isAllocated() ) { + // Nothing to resize return; + } const bool isFullScreen = _engine->isFullScreen(); - // deallocate engine resources + // Deallocate engine resources _engine->clear(); _prevRoi = {}; - // allocate engine resources + // Allocate engine resources if ( !_engine->allocate( info, isFullScreen ) ) { clear(); } diff --git a/src/engine/screen.h b/src/engine/screen.h index bb2d2672b82..dcf8c1d8c59 100644 --- a/src/engine/screen.h +++ b/src/engine/screen.h @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2020 - 2023 * + * Copyright (C) 2020 - 2024 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -139,6 +139,26 @@ namespace fheroes2 return _nearestScaling; } + virtual uint8_t getCurrentDisplayIndex() const + { + return 0; + } + + virtual void setDisplayIndex( const uint8_t ) + { + // Do nothing. + } + + virtual uint8_t getMaximumDisplays() const + { + return 1; + } + + virtual const char * getDisplayName( const uint8_t ) + { + return ""; + } + protected: BaseRenderEngine() : _isFullScreen( false ) @@ -162,6 +182,11 @@ namespace fheroes2 return false; } + virtual bool isAllocated() const + { + return true; + } + virtual bool isMouseCursorActive() const { return false; diff --git a/src/fheroes2/dialog/dialog_graphics_settings.cpp b/src/fheroes2/dialog/dialog_graphics_settings.cpp index f98a3ccbfd2..108c5431e6a 100644 --- a/src/fheroes2/dialog/dialog_graphics_settings.cpp +++ b/src/fheroes2/dialog/dialog_graphics_settings.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2022 - 2023 * + * Copyright (C) 2022 - 2024 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -49,17 +49,21 @@ namespace Mode, VSync, SystemInfo, + SwitchDisplay, Exit }; - const fheroes2::Size offsetBetweenOptions{ 118, 110 }; - const fheroes2::Point optionOffset{ 69, 47 }; + const fheroes2::Size offsetBetweenOptions{ 92, 110 }; + const fheroes2::Point optionOffset{ 36, 47 }; const int32_t optionWindowSize{ 65 }; + const int32_t centerAlignmentOffset{ offsetBetweenOptions.width / 2 }; const fheroes2::Rect resolutionRoi{ optionOffset.x, optionOffset.y, optionWindowSize, optionWindowSize }; const fheroes2::Rect modeRoi{ optionOffset.x + offsetBetweenOptions.width, optionOffset.y, optionWindowSize, optionWindowSize }; - const fheroes2::Rect vSyncRoi{ optionOffset.x, optionOffset.y + offsetBetweenOptions.height, optionWindowSize, optionWindowSize }; - const fheroes2::Rect systemInfoRoi{ optionOffset.x + offsetBetweenOptions.width, optionOffset.y + offsetBetweenOptions.height, optionWindowSize, optionWindowSize }; + const fheroes2::Rect vSyncRoi{ optionOffset.x + centerAlignmentOffset, optionOffset.y + offsetBetweenOptions.height, optionWindowSize, optionWindowSize }; + const fheroes2::Rect systemInfoRoi{ optionOffset.x + offsetBetweenOptions.width + centerAlignmentOffset, optionOffset.y + offsetBetweenOptions.height, + optionWindowSize, optionWindowSize }; + const fheroes2::Rect switchDisplayRoi{ optionOffset.x + offsetBetweenOptions.width * 2, optionOffset.y, optionWindowSize, optionWindowSize }; void drawResolution( const fheroes2::Rect & optionRoi ) { @@ -120,15 +124,25 @@ namespace fheroes2::drawOption( optionRoi, image, _( "System Info" ), isSystemInfoDisplayed ? _( "on" ) : _( "off" ), fheroes2::UiOptionTextWidth::TWO_ELEMENTS_ROW ); } + void drawDisplay( const fheroes2::Rect & optionRoi ) + { + const uint8_t displayIndex = fheroes2::engine().getCurrentDisplayIndex(); + fheroes2::drawOption( optionRoi, fheroes2::AGG::GetICN( ICN::GAME_OPTION_ICON, 1 ), _( "Display Index" ), + std::to_string( displayIndex + 1 ) + ": " + fheroes2::engine().getDisplayName( displayIndex ), + fheroes2::UiOptionTextWidth::TWO_ELEMENTS_ROW ); + } + SelectedWindow showConfigurationWindow() { fheroes2::Display & display = fheroes2::Display::instance(); const Settings & conf = Settings::Get(); const bool isEvilInterface = conf.isEvilInterfaceEnabled(); - const fheroes2::Sprite & dialog = fheroes2::AGG::GetICN( ( isEvilInterface ? ICN::ESPANBKG_EVIL : ICN::ESPANBKG ), 0 ); + const fheroes2::Sprite & dialog = fheroes2::AGG::GetICN( ( isEvilInterface ? ICN::CSPANBKE : ICN::CSPANBKG ), 0 ); const fheroes2::Sprite & dialogShadow = fheroes2::AGG::GetICN( ( isEvilInterface ? ICN::CSPANBKE : ICN::CSPANBKG ), 1 ); + const fheroes2::Sprite & secondRowBackground = fheroes2::AGG::GetICN( Settings::Get().isEvilInterfaceEnabled() ? ICN::SURDRBKE : ICN::SURDRBKG, 0 ); + const fheroes2::Point dialogOffset( ( display.width() - dialog.width() ) / 2, ( display.height() - dialog.height() ) / 2 ); const fheroes2::Point shadowOffset( dialogOffset.x - BORDERWIDTH, dialogOffset.y ); @@ -139,18 +153,25 @@ namespace fheroes2::Blit( dialogShadow, display, windowRoi.x - BORDERWIDTH, windowRoi.y + BORDERWIDTH ); fheroes2::Blit( dialog, display, windowRoi.x, windowRoi.y ); + // The image is taken from another big ICN and cropped, with its background at an adequate size. + // The offsets are statically fixed based on the original image to fill the empty spaces on the second row alongside it's shadow. + fheroes2::Blit( secondRowBackground, 50, 30, display, windowRoi.x + BORDERWIDTH + 10, windowRoi.y + optionOffset.y - 10 + offsetBetweenOptions.height, + dialog.width() - BORDERWIDTH * 2 - 20, optionWindowSize + 20 ); + fheroes2::ImageRestorer emptyDialogRestorer( display, windowRoi.x, windowRoi.y, windowRoi.width, windowRoi.height ); const fheroes2::Rect windowResolutionRoi( resolutionRoi + windowRoi.getPosition() ); const fheroes2::Rect windowModeRoi( modeRoi + windowRoi.getPosition() ); const fheroes2::Rect windowVSyncRoi( vSyncRoi + windowRoi.getPosition() ); const fheroes2::Rect windowSystemInfoRoi( systemInfoRoi + windowRoi.getPosition() ); + const fheroes2::Rect windowSwitchDisplayRoi( switchDisplayRoi + windowRoi.getPosition() ); - const auto drawOptions = [&windowResolutionRoi, &windowModeRoi, &windowVSyncRoi, &windowSystemInfoRoi]() { + const auto drawOptions = [&windowResolutionRoi, &windowModeRoi, &windowVSyncRoi, &windowSwitchDisplayRoi, &windowSystemInfoRoi]() { drawResolution( windowResolutionRoi ); drawMode( windowModeRoi ); drawVSync( windowVSyncRoi ); drawSystemInfo( windowSystemInfoRoi ); + drawDisplay( windowSwitchDisplayRoi ); }; drawOptions(); @@ -187,7 +208,9 @@ namespace if ( le.MouseClickLeft( windowSystemInfoRoi ) ) { return SelectedWindow::SystemInfo; } - + if ( le.MouseClickLeft( windowSwitchDisplayRoi ) ) { + return SelectedWindow::SwitchDisplay; + } if ( le.MousePressRight( windowResolutionRoi ) ) { fheroes2::showStandardTextMessage( _( "Select Game Resolution" ), _( "Change the resolution of the game." ), 0 ); } @@ -197,7 +220,10 @@ namespace else if ( le.MousePressRight( windowVSyncRoi ) ) { fheroes2::showStandardTextMessage( _( "V-Sync" ), _( "The V-Sync option can be enabled to resolve flickering issues on some monitors." ), 0 ); } - if ( le.MousePressRight( windowSystemInfoRoi ) ) { + else if ( le.MousePressRight( windowSwitchDisplayRoi ) ) { + fheroes2::showStandardTextMessage( _( "Display Selection" ), _( "Toggle Between available Displays" ), 0 ); + } + else if ( le.MousePressRight( windowSystemInfoRoi ) ) { fheroes2::showStandardTextMessage( _( "System Info" ), _( "Show extra information such as FPS and current time." ), 0 ); } else if ( le.MousePressRight( okayButton.area() ) ) { @@ -253,6 +279,19 @@ namespace fheroes2 conf.Save( Settings::configFileName ); windowType = SelectedWindow::Configuration; break; + case SelectedWindow::SwitchDisplay: { + fheroes2::BaseRenderEngine & engine = fheroes2::engine(); + engine.setDisplayIndex( ( engine.getCurrentDisplayIndex() + 1 ) % engine.getMaximumDisplays() ); + + fheroes2::Display & display = fheroes2::Display::instance(); + const fheroes2::ResolutionInfo currentResolution{ display.width(), display.height(), display.screenSize().width, display.screenSize().height }; + display.setResolution( currentResolution ); + updateUI(); + + conf.Save( Settings::configFileName ); + windowType = SelectedWindow::Configuration; + break; + } default: return; } diff --git a/src/fheroes2/system/settings.cpp b/src/fheroes2/system/settings.cpp index 31a06fc7e62..2f30cb8d549 100644 --- a/src/fheroes2/system/settings.cpp +++ b/src/fheroes2/system/settings.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2019 - 2023 * + * Copyright (C) 2019 - 2024 * * * * Free Heroes2 Engine: http://sourceforge.net/projects/fheroes2 * * Copyright (C) 2009 by Andrey Afletdinov * @@ -328,6 +328,10 @@ bool Settings::Read( const std::string & filePath ) _optGlobal.SetModes( GLOBAL_ENABLE_EDITOR ); } + if ( config.Exists( "display" ) ) { + fheroes2::engine().setDisplayIndex( static_cast( std::max( config.IntParams( "display" ), 0 ) ) ); + } + return true; } @@ -373,6 +377,9 @@ std::string Settings::String() const os << std::endl << "# video mode: in-game width x in-game height : on-screen width x on-screen height" << std::endl; os << "videomode = " << display.width() << "x" << display.height() << ":" << display.screenSize().width << "x" << display.screenSize().height << std::endl; + os << std::endl << "# Display Index" << std::endl; + os << "display = " << static_cast( fheroes2::engine().getCurrentDisplayIndex() ) << std::endl; + os << std::endl << "# music: original, expansion, external" << std::endl; os << "music = " << musicType << std::endl;