Files
itgmania212121/src/archutils/Win32/GraphicsWindow.cpp
T
sukibaby e1bf59458f Check the ShowMouseCursor preference on Windows
Respect the user's setting in Preferences.ini. Hides the cursor if ShowMouseCursor is set to 0, shows the cursor if ShowMouseCursor is set to 1.
2025-03-03 23:28:13 -08:00

673 lines
19 KiB
C++

#include "global.h"
#include "GraphicsWindow.h"
#include "ProductInfo.h"
#include "RageLog.h"
#include "RageUtil.h"
#include "RageDisplay.h"
#include "DisplaySpec.h"
#include "arch/ArchHooks/ArchHooks.h"
#include "archutils/Win32/AppInstance.h"
#include "archutils/Win32/Crash.h"
#include "archutils/Win32/ErrorStrings.h"
#include "archutils/Win32/WindowIcon.h"
#include "archutils/Win32/GetFileInformation.h"
#include "CommandLineActions.h"
#include "DirectXHelpers.h"
#include "PrefsManager.h"
#include <optional>
#include <set>
static const RString g_sClassName = PRODUCT_ID;
static HWND g_hWndMain;
static HDC g_HDC;
static VideoModeParams g_CurrentParams;
static bool g_bResolutionChanged = false;
static bool g_bHasFocus = true;
static HICON g_hIcon = nullptr;
static bool m_bWideWindowClass;
static bool g_bD3D = false;
// If we're fullscreen, this is the mode we set.
static DEVMODE g_FullScreenDevMode;
static bool g_bRecreatingVideoMode = false;
static UINT g_iQueryCancelAutoPlayMessage = 0;
static RString GetNewWindow()
{
HWND h = GetForegroundWindow();
if( h == nullptr )
return "(NULL)";
DWORD iProcessID;
GetWindowThreadProcessId( h, &iProcessID );
RString sName;
GetProcessFileName( iProcessID, sName );
sName = Basename(sName);
return sName;
}
static std::optional<DEVMODE> GetDisplaySettings(const RString& displayId)
{
DEVMODE dm;
ZERO( dm );
dm.dmSize = sizeof(dm);
if (!EnumDisplaySettings(displayId, ENUM_CURRENT_SETTINGS, &dm))
{
LOG->Warn("%s", werr_ssprintf(GetLastError(), "EnumDisplaySettings failed").c_str());
return std::nullopt;
}
return dm;
}
static LRESULT CALLBACK GraphicsWindow_WndProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
CHECKPOINT_M( ssprintf("%p, %u, %08x, %08x", hWnd, msg, wParam, lParam) );
// Suppress autorun.
if( msg == g_iQueryCancelAutoPlayMessage )
return true;
switch( msg )
{
case WM_ACTIVATE:
{
const bool bInactive = (LOWORD(wParam) == WA_INACTIVE);
const bool bMinimized = (HIWORD(wParam) != 0);
const bool bHadFocus = g_bHasFocus;
g_bHasFocus = !bInactive && !bMinimized;
LOG->Trace( "WM_ACTIVATE (%i, %i): %s", bInactive, bMinimized, g_bHasFocus? "has focus":"doesn't have focus" );
if( !g_bHasFocus )
{
RString sName = GetNewWindow();
static std::set<RString> sLostFocusTo;
sLostFocusTo.insert( sName );
RString sStr;
for( std::set<RString>::const_iterator it = sLostFocusTo.begin(); it != sLostFocusTo.end(); ++it )
sStr += (sStr.size()?", ":"") + *it;
LOG->MapLog( "LOST_FOCUS", "Lost focus to: %s", sStr.c_str() );
}
if( !g_bD3D && !g_CurrentParams.windowed && !g_bRecreatingVideoMode )
{
/* In OpenGL (not D3D), it's our job to unset and reset the
* full-screen video mode when we focus changes, and to hide
* and show the window. Hiding is done in WM_KILLFOCUS,
* because that's where most other apps seem to do it. */
if( g_bHasFocus && !bHadFocus )
{
ChangeDisplaySettingsEx( g_CurrentParams.sDisplayId, &g_FullScreenDevMode, nullptr, CDS_FULLSCREEN, nullptr );
ShowWindow( g_hWndMain, SW_SHOWNORMAL );
SetWindowPos( g_hWndMain, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE );
}
else if( !g_bHasFocus && bHadFocus )
{
ChangeDisplaySettingsEx(g_CurrentParams.sDisplayId, nullptr, nullptr, 0, nullptr);
}
}
return 0;
}
case WM_KILLFOCUS:
if( !g_bD3D && !g_CurrentParams.windowed && !g_bRecreatingVideoMode )
ShowWindow( g_hWndMain, SW_SHOWMINNOACTIVE );
break;
/* Is there any reason we should care what size the user resizes
* the window to? (who? -aj)
* Short answer: yes. -aj */
// case WM_GETMINMAXINFO:
case WM_SETCURSOR:
if( !g_CurrentParams.windowed )
{
// We check if the preference is false because we don't want
// to show the cursor if the user has set it to false.
if (!PREFSMAN->m_bShowMouseCursor.Get())
{
SetCursor(nullptr);
}
return 1;
}
break;
case WM_SYSCOMMAND:
switch( wParam&0xFFF0 )
{
case SC_MONITORPOWER:
case SC_SCREENSAVE:
return 0;
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
BeginPaint( hWnd, &ps );
EndPaint( hWnd, &ps );
break;
}
case WM_KEYDOWN:
case WM_KEYUP:
case WM_SYSKEYDOWN:
case WM_SYSKEYUP:
case WM_MOUSEWHEEL: // might want to use this for GET_WHEEL_DELTA_WPARAM(wParam) -aj
// We handle all input ourself, via DirectInput.
return 0;
case WM_CLOSE:
LOG->Trace("WM_CLOSE: shutting down");
ArchHooks::SetUserQuit();
return 0;
case WM_WINDOWPOSCHANGED:
{
/* If we're fullscreen and don't have focus, our window is hidden,
* so GetClientRect isn't meaningful. */
if( !g_CurrentParams.windowed && !g_bHasFocus )
break;
RECT rect;
GetClientRect( hWnd, &rect );
int iWidth = rect.right - rect.left;
int iHeight = rect.bottom - rect.top;
if( g_CurrentParams.width != iWidth || g_CurrentParams.height != iHeight )
{
g_CurrentParams.width = iWidth;
g_CurrentParams.height = iHeight;
g_bResolutionChanged = true;
}
break;
}
case WM_COPYDATA:
{
PCOPYDATASTRUCT pMyCDS = (PCOPYDATASTRUCT) lParam;
RString sCommandLine( (char*)pMyCDS->lpData, pMyCDS->cbData );
CommandLineActions::CommandLineArgs args;
split( sCommandLine, "|", args.argv, false );
CommandLineActions::ToProcess.push_back( args );
break;
}
}
CHECKPOINT_M( ssprintf("%p, %u, %08x, %08x", hWnd, msg, wParam, lParam) );
if( m_bWideWindowClass )
{
return DefWindowProcW( hWnd, msg, wParam, lParam );
}
else
{
return DefWindowProcA( hWnd, msg, wParam, lParam );
}
}
static void AdjustVideoModeParams( VideoModeParams &p )
{
std::optional<DEVMODE> dmOpt = GetDisplaySettings(p.sDisplayId);
if (!dmOpt.has_value())
{
p.rate = 60;
return;
}
const DEVMODE& dm = *dmOpt;
if (!(dm.dmFields & DM_DISPLAYFREQUENCY) || dm.dmDisplayFrequency == 0 || dm.dmDisplayFrequency == 1)
{
p.rate = 60;
LOG->Warn( "EnumDisplaySettings doesn't know what the refresh rate is. %d %d %d", dm.dmPelsWidth, dm.dmPelsHeight, dm.dmBitsPerPel );
}
else
{
p.rate = dm.dmDisplayFrequency;
}
}
/* Set the display mode to the given size, bit depth and refresh.
* The refresh setting may be ignored. */
RString GraphicsWindow::SetScreenMode( const VideoModeParams &p )
{
if( p.windowed )
{
// We're going windowed. If we were previously fullscreen, reset.
ChangeDisplaySettingsEx( p.sDisplayId, nullptr, nullptr, 0, nullptr );
return RString();
}
std::optional<DEVMODE> dmOpt = GetDisplaySettings(p.sDisplayId);
if (!dmOpt.has_value())
{
return "Couldn't retrieve display settings";
}
DEVMODE DevMode = *dmOpt;
DevMode.dmPelsWidth = p.width;
DevMode.dmPelsHeight = p.height;
DevMode.dmBitsPerPel = p.bpp;
DevMode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;
if( p.rate != REFRESH_DEFAULT )
{
DevMode.dmDisplayFrequency = p.rate;
DevMode.dmFields |= DM_DISPLAYFREQUENCY;
}
ChangeDisplaySettingsEx(p.sDisplayId, nullptr, nullptr, 0, nullptr);
int ret = ChangeDisplaySettingsEx( p.sDisplayId, &DevMode, nullptr, CDS_FULLSCREEN, nullptr );
if( ret != DISP_CHANGE_SUCCESSFUL && (DevMode.dmFields & DM_DISPLAYFREQUENCY) )
{
DevMode.dmFields &= ~DM_DISPLAYFREQUENCY;
ret = ChangeDisplaySettingsEx( p.sDisplayId, &DevMode, nullptr, CDS_FULLSCREEN, nullptr );
}
if( ret != DISP_CHANGE_SUCCESSFUL )
{
LOG->Warn("ChangeDisplaySettingsEx failed with error code: %d", ret);
return "Couldn't set screen mode";
}
g_FullScreenDevMode = DevMode;
return RString();
}
static int GetWindowStyle( bool bWindowed , bool bWindowIsFullscreenBorderless)
{
if( bWindowed && !bWindowIsFullscreenBorderless )
return WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
else
return WS_POPUP | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
}
/* Set the final window size, set the window text and icon, and then unhide the
* window. */
void GraphicsWindow::CreateGraphicsWindow( const VideoModeParams &p, bool bForceRecreateWindow )
{
auto resetDeviceMode = [=](DEVMODE& mode)
{
ZeroMemory(&mode, sizeof(DEVMODE));
mode.dmSize = sizeof(DEVMODE);
mode.dmDriverExtra = 0;
};
auto deviceModeIsValid = [=](const DEVMODE& mode)
{
return (mode.dmFields & DM_PELSWIDTH) && (mode.dmFields & DM_PELSHEIGHT)
&& (mode.dmFields & DM_DISPLAYFREQUENCY)
&& (mode.dmBitsPerPel >= 32 || !(mode.dmFields & DM_BITSPERPEL));
};
g_CurrentParams = p;
// Adjust g_CurrentParams to reflect the actual display settings.
AdjustVideoModeParams( g_CurrentParams );
DEVMODE devmode;
resetDeviceMode(devmode);
//Top left corner of monitor
POINTL pos;
//Set default position to (0,0), which is the primary display device as a fallback
pos.x = 0;
pos.y = 0;
// Look for the preferred display's position.
if (EnumDisplaySettingsEx(p.sDisplayId, ENUM_CURRENT_SETTINGS, &devmode, 0) && deviceModeIsValid(devmode)
&& (devmode.dmFields & DM_POSITION))
{
pos = devmode.dmPosition;
resetDeviceMode(devmode);
}
if( g_hWndMain == nullptr || bForceRecreateWindow )
{
int iWindowStyle = GetWindowStyle( p.windowed , p.bWindowIsFullscreenBorderless );
AppInstance inst;
HWND hWnd = CreateWindow( g_sClassName, "app", iWindowStyle,
0, 0, 0, 0, nullptr, nullptr, inst, nullptr );
if( hWnd == nullptr )
RageException::Throw( "%s", werr_ssprintf( GetLastError(), "CreateWindow" ).c_str() );
/* If an old window exists, transfer focus to the new window before
* deleting it, or some other window may temporarily get focus, which
* can cause it to be resized. */
if( g_hWndMain != nullptr )
{
// While we change to the new window, don't do ChangeDisplaySettings in WM_ACTIVATE.
g_bRecreatingVideoMode = true;
SetForegroundWindow( hWnd );
g_bRecreatingVideoMode = false;
GraphicsWindow::DestroyGraphicsWindow();
}
g_hWndMain = hWnd;
CrashHandler::SetForegroundWindow( g_hWndMain );
g_HDC = GetDC( g_hWndMain );
}
// Update the window title.
do
{
if( m_bWideWindowClass )
{
if( SetWindowText( g_hWndMain, ConvertUTF8ToACP(p.sWindowTitle).c_str() ) )
break;
}
SetWindowTextA( g_hWndMain, ConvertUTF8ToACP(p.sWindowTitle) );
} while(0);
// Update the window icon.
if( g_hIcon != nullptr )
{
SetClassLongPtrA( g_hWndMain, GCLP_HICON, reinterpret_cast<LONG_PTR>(LoadIcon(nullptr,IDI_APPLICATION)) );
DestroyIcon( g_hIcon );
g_hIcon = nullptr;
}
g_hIcon = IconFromFile( p.sIconFile );
if( g_hIcon != nullptr )
SetClassLongPtrA( g_hWndMain, GCLP_HICON, reinterpret_cast<LONG_PTR>(g_hIcon) );
/* The window style may change as a result of switching to or from fullscreen;
* apply it. Don't change the WS_VISIBLE bit. */
int iWindowStyle = GetWindowStyle(p.windowed, p.bWindowIsFullscreenBorderless);
if( GetWindowLong( g_hWndMain, GWL_STYLE ) & WS_VISIBLE )
iWindowStyle |= WS_VISIBLE;
SetWindowLong( g_hWndMain, GWL_STYLE, iWindowStyle );
// Set rectangle for window based on the preferred display and resolution
RECT WindowRect;
SetRect( &WindowRect, pos.x, pos.y, pos.x + p.width, pos.y + p.height );
AdjustWindowRect( &WindowRect, iWindowStyle, FALSE );
//LOG->Warn( "w = %d, h = %d", p.width, p.height );
const int iWidth = WindowRect.right - WindowRect.left;
const int iHeight = WindowRect.bottom - WindowRect.top;
int x = pos.x, y = pos.y;
/* If windowed and not fullscreen borderless, center the window on the primary display.
* Otherwise the window will be centered on the last display option that was hovered before selecting Windowed in Options*/
if( p.windowed && !p.bWindowIsFullscreenBorderless)
{
if (EnumDisplaySettingsEx(NULL, ENUM_CURRENT_SETTINGS, &devmode, 0) && deviceModeIsValid(devmode)
&& (devmode.dmFields & DM_POSITION))
{
WindowRect.left = devmode.dmPosition.x;
WindowRect.top = devmode.dmPosition.y;
WindowRect.right = devmode.dmPosition.x + devmode.dmPelsWidth;
WindowRect.bottom = devmode.dmPosition.y + devmode.dmPelsHeight;
}
x = WindowRect.left + (WindowRect.right - WindowRect.left - iWidth) / 2;
y = WindowRect.top + (WindowRect.bottom - WindowRect.top - iHeight) / 2;
}
resetDeviceMode(devmode);
/* Move and resize the window. SWP_FRAMECHANGED causes the above
* SetWindowLong to take effect. */
if( !SetWindowPos( g_hWndMain, HWND_NOTOPMOST, x, y, iWidth, iHeight, SWP_FRAMECHANGED|SWP_SHOWWINDOW ) )
LOG->Warn( "%s", werr_ssprintf( GetLastError(), "SetWindowPos" ).c_str() );
SetForegroundWindow( g_hWndMain );
/* Pump messages quickly, to make sure the window is completely set up.
* If we don't do this, then starting up in a D3D fullscreen window may
* cause all other windows on the system to be resized. */
MSG msg;
while( PeekMessage( &msg, nullptr, 0, 0, PM_NOREMOVE ) )
{
GetMessage( &msg, nullptr, 0, 0 );
DispatchMessage( &msg );
}
}
/** @brief Shut down the window, but don't reset the video mode. */
void GraphicsWindow::DestroyGraphicsWindow()
{
if( g_HDC != nullptr )
{
ReleaseDC( g_hWndMain, g_HDC );
g_HDC = nullptr;
}
CHECKPOINT;
if( g_hWndMain != nullptr )
{
DestroyWindow( g_hWndMain );
g_hWndMain = nullptr;
CrashHandler::SetForegroundWindow( g_hWndMain );
}
CHECKPOINT;
if( g_hIcon != nullptr )
{
DestroyIcon( g_hIcon );
g_hIcon = nullptr;
}
CHECKPOINT;
MSG msg;
while( PeekMessage( &msg, nullptr, 0, 0, PM_NOREMOVE ) )
{
CHECKPOINT;
GetMessage( &msg, nullptr, 0, 0 );
CHECKPOINT;
DispatchMessage( &msg );
}
CHECKPOINT;
}
void GraphicsWindow::Initialize( bool bD3D )
{
// A few things need to be handled differently for D3D.
g_bD3D = bD3D;
AppInstance inst;
do
{
const std::wstring wsClassName = RStringToWstring( g_sClassName );
WNDCLASSW WindowClassW =
{
CS_OWNDC | CS_BYTEALIGNCLIENT,
GraphicsWindow_WndProc,
0, /* cbClsExtra */
0, /* cbWndExtra */
inst, /* hInstance */
nullptr, /* set icon later */
LoadCursor( nullptr, IDC_ARROW ), /* default cursor */
nullptr, /* hbrBackground */
nullptr, /* lpszMenuName */
wsClassName.c_str() /* lpszClassName */
};
m_bWideWindowClass = true;
if( RegisterClassW( &WindowClassW ) )
break;
WNDCLASS WindowClassA =
{
CS_OWNDC | CS_BYTEALIGNCLIENT,
GraphicsWindow_WndProc,
0, /* cbClsExtra */
0, /* cbWndExtra */
inst, /* hInstance */
nullptr, /* set icon later */
LoadCursor( nullptr, IDC_ARROW ), /* default cursor */
nullptr, /* hbrBackground */
nullptr, /* lpszMenuName */
g_sClassName /* lpszClassName */
};
m_bWideWindowClass = false;
if( !RegisterClassA( &WindowClassA ) )
RageException::Throw( "%s", werr_ssprintf( GetLastError(), "RegisterClass" ).c_str() );
} while(0);
g_iQueryCancelAutoPlayMessage = RegisterWindowMessage( "QueryCancelAutoPlay" );
}
void GraphicsWindow::Shutdown()
{
DestroyGraphicsWindow();
/* Return to the desktop resolution, if needed.
* It'd be nice to not do this: Windows will do it when we quit, and if
* we're shutting down OpenGL to try D3D, this will cause extra mode
* switches. However, we need to do this before displaying dialogs. */
ChangeDisplaySettingsEx( g_CurrentParams.sDisplayId, nullptr, nullptr, 0, nullptr );
AppInstance inst;
UnregisterClass( g_sClassName, inst );
}
HDC GraphicsWindow::GetHDC()
{
ASSERT( g_HDC != nullptr );
return g_HDC;
}
const VideoModeParams &GraphicsWindow::GetParams()
{
return g_CurrentParams;
}
void GraphicsWindow::Update()
{
MSG msg;
while( PeekMessage( &msg, nullptr, 0, 0, PM_NOREMOVE ) )
{
GetMessage( &msg, nullptr, 0, 0 );
DispatchMessage( &msg );
}
HOOKS->SetHasFocus( g_bHasFocus );
if( g_bResolutionChanged && DISPLAY != nullptr )
{
//LOG->Warn( "Changing resolution" );
/* Let DISPLAY know that our resolution has changed. (Note that ResolutionChanged()
* can come back here, so reset g_bResolutionChanged first.) */
g_bResolutionChanged = false;
DISPLAY->ResolutionChanged();
}
}
HWND GraphicsWindow::GetHwnd()
{
return g_hWndMain;
}
void GraphicsWindow::GetDisplaySpecs( DisplaySpecs &out )
{
auto resetDeviceMode = [=]( DEVMODE& mode ) {
ZeroMemory( &mode, sizeof( DEVMODE ) );
mode.dmSize = sizeof(DEVMODE);
mode.dmDriverExtra = 0;
};
auto deviceModeIsValid = [=]( const DEVMODE& mode ) {
return (mode.dmFields & DM_PELSWIDTH) && (mode.dmFields & DM_PELSHEIGHT)
&& (mode.dmFields & DM_DISPLAYFREQUENCY)
&& (mode.dmBitsPerPel >= 32 || !(mode.dmFields & DM_BITSPERPEL));
};
DEVMODE devmode;
resetDeviceMode(devmode);
DWORD deviceIter = 0;
DISPLAY_DEVICE dd;
ZeroMemory(&dd, sizeof(dd));
dd.cb = sizeof(dd);
// Loop through all displays and their display settings to create a DisplaySpec for each display.
while (EnumDisplayDevices(NULL, deviceIter++, &dd, 0))
{
int mode = 0;
std::set<DisplayMode> dispModes;
while (EnumDisplaySettingsEx(dd.DeviceName, mode++, &devmode, 0))
{
if (deviceModeIsValid(devmode))
{
DisplayMode m = { devmode.dmPelsWidth, devmode.dmPelsHeight, static_cast<double> (devmode.dmDisplayFrequency) };
dispModes.insert(m);
}
resetDeviceMode(devmode);
}
//Clean up the device name to store as the DisplaySpec ID
RString sDeviceName;
sDeviceName = dd.DeviceName;
TrimRight(sDeviceName);
//Make the DisplaySpec Name more friendly by removing the "\\.\" prefix on most Windows display devices
RString modeName;
modeName = sDeviceName;
TrimLeft(modeName, "\\");
TrimLeft(modeName, ".");
TrimLeft(modeName, "\\");
// Get the current display mode
// Set the ENUM_CURRENT_SETTINGS flag so that we only store DisplaySpecs for valid displays
if (EnumDisplaySettingsEx(dd.DeviceName, ENUM_CURRENT_SETTINGS, &devmode, 0) && deviceModeIsValid(devmode)
&& (devmode.dmFields & DM_POSITION))
{
DisplayMode m = { devmode.dmPelsWidth, devmode.dmPelsHeight, static_cast<double> (devmode.dmDisplayFrequency) };
RectI bounds = { devmode.dmPosition.x, devmode.dmPosition.y, static_cast<int> (m.width), static_cast<int> (m.height) };
out.insert(DisplaySpec(sDeviceName, modeName, dispModes, m, bounds));
}
else if (!dispModes.empty())
{
LOG->Warn("Could not retrieve valid current display mode");
out.insert(DisplaySpec(sDeviceName, modeName, *dispModes.begin()));
}
}
if (out.size() == 0)
LOG->Warn("Could not retrieve *any* DisplaySpec's!");
ZeroMemory(&dd, sizeof(dd));
dd.cb = sizeof(dd);
resetDeviceMode(devmode);
}
/*
* (c) 2004 Glenn Maynard
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, and/or sell copies of the Software, and to permit persons to
* whom the Software is furnished to do so, provided that the above
* copyright notice(s) and this permission notice appear in all copies of
* the Software and that both the above copyright notice(s) and this
* permission notice appear in supporting documentation.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
* THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/