Windows platform enhancements (#84)

* Enabling portable paths for Windows (custom user preferences dir, drag'n'drop paths)

* Updated the windows messages loop for DXGI window API

* Fixed international keyboard layouts text input in DXGI
This commit is contained in:
Radek Krzyśków 2024-06-30 06:46:14 +02:00 committed by GitHub
parent 4967c911cc
commit e2c15afc68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 275 additions and 115 deletions

View File

@ -52,7 +52,7 @@ bool fs_init(const char *writepath) {
#endif #endif
// we shall not progress any further if the path is inaccessible // we shall not progress any further if the path is inaccessible
if ('\0' == fs_writepath[0]) { if (('\0' == fs_writepath[0]) || !fs_sys_dir_exists(fs_writepath)) {
sys_fatal("Could not access the User Preferences directory."); sys_fatal("Could not access the User Preferences directory.");
} }

View File

@ -24,15 +24,13 @@
#include "gfx_dxgi.h" #include "gfx_dxgi.h"
extern "C" { extern "C" {
#include "pc/mods/mod_import.h" #include "pc/mods/mod_import.h"
#ifdef DISCORD_SDK #include "pc/rom_checker.h"
#include "pc/discord/discord.h" #include "pc/network/version.h"
#endif #include "pc/configfile.h"
#include "pc/network/version.h"
} }
#include "../configfile.h" #include "pc/pc_main.h"
#include "../pc_main.h"
#include "gfx_window_manager_api.h" #include "gfx_window_manager_api.h"
#include "gfx_rendering_api.h" #include "gfx_rendering_api.h"
@ -91,7 +89,6 @@ static struct {
bool sync_interval_means_frames_to_wait; bool sync_interval_means_frames_to_wait;
UINT length_in_vsync_frames; UINT length_in_vsync_frames;
void (*run_one_game_iter)(void);
bool (*on_key_down)(int scancode); bool (*on_key_down)(int scancode);
bool (*on_key_up)(int scancode); bool (*on_key_up)(int scancode);
void (*on_all_keys_up)(void); void (*on_all_keys_up)(void);
@ -244,100 +241,122 @@ static void gfx_dxgi_on_resize(void) {
} }
} }
static void onkeydown(WPARAM w_param, LPARAM l_param) { static void gfx_dxgi_on_key_down(WPARAM w_param, LPARAM l_param) {
int key = ((l_param >> 16) & 0x1ff); int key = ((l_param >> 16) & 0x1ff);
if (inTextInput) { if (dxgi.on_key_down != nullptr) { dxgi.on_key_down(key); }
const int keyboardScanCode = (l_param >> 16) & 0x00ff;
const int virtualKey = w_param;
BYTE keyboardState[256];
GetKeyboardState(keyboardState);
WORD ascii = 0;
const int len = ToAscii(virtualKey, keyboardScanCode, keyboardState, &ascii, 0);
if (len > 0) {
dxgi.on_text_input((char*)&ascii);
}
}
if (dxgi.on_key_down != nullptr) {
dxgi.on_key_down(key);
}
} }
static void onkeyup(WPARAM w_param, LPARAM l_param) { static void gfx_dxgi_on_key_up(WPARAM w_param, LPARAM l_param) {
int key = ((l_param >> 16) & 0x1ff); int key = ((l_param >> 16) & 0x1ff);
if (dxgi.on_key_up != nullptr) { if (dxgi.on_key_up != nullptr) { dxgi.on_key_up(key); }
dxgi.on_key_up(key); }
static void gfx_dxgi_on_text_input(wchar_t code_unit) {
if (inTextInput && (!IS_HIGH_SURROGATE(code_unit)) && (!IS_LOW_SURROGATE(code_unit))) {
char utf8_buffer[3 + 1];
if (code_unit >= 0x0800) { // 3-byte encoding
utf8_buffer[0] = 0xe0 | ((code_unit >> 12) & 0x0f);
utf8_buffer[1] = 0x80 | ((code_unit >> 6) & 0x3f);
utf8_buffer[2] = 0x80 | (code_unit & 0x3f);
utf8_buffer[3] = '\0';
} else if (code_unit >= 0x0080) { // 2-byte encoding
utf8_buffer[0] = 0xc0 | ((code_unit >> 6) & 0x1f);
utf8_buffer[1] = 0x80 | (code_unit & 0x3f);
utf8_buffer[2] = '\0';
} else { // 1-byte encoding
if (code_unit < ' ') { return; } // skipping control chars
utf8_buffer[0] = (char)code_unit;
utf8_buffer[1] = '\0';
}
dxgi.on_text_input(utf8_buffer);
} }
} }
static LRESULT CALLBACK gfx_dxgi_wnd_proc(HWND h_wnd, UINT message, WPARAM w_param, LPARAM l_param) { static LRESULT CALLBACK gfx_dxgi_wnd_proc(HWND h_wnd, UINT message, WPARAM w_param, LPARAM l_param) {
WCHAR wcsFileName[MAX_PATH];
char szFileName[MAX_PATH];
switch (message) { switch (message) {
case WM_SIZE: case WM_SIZE: {
gfx_dxgi_on_resize(); gfx_dxgi_on_resize();
break; return 0;
case WM_DESTROY: }
case WM_CLOSE: {
DestroyWindow(h_wnd);
return 0;
}
case WM_DESTROY: {
game_exit(); game_exit();
break; PostQuitMessage(0);
case WM_PAINT: return 0;
if (dxgi.showing_error) { }
return DefWindowProcW(h_wnd, message, w_param, l_param); case WM_ACTIVATEAPP: {
} else {
if (dxgi.run_one_game_iter != nullptr) {
dxgi.run_one_game_iter();
}
}
break;
case WM_ACTIVATEAPP:
if (dxgi.on_all_keys_up != nullptr) { if (dxgi.on_all_keys_up != nullptr) {
dxgi.on_all_keys_up(); dxgi.on_all_keys_up();
return 0;
} }
break; break;
case WM_KEYDOWN: }
onkeydown(w_param, l_param); case WM_KEYDOWN: {
break; gfx_dxgi_on_key_down(w_param, l_param);
case WM_KEYUP: return 0;
onkeyup(w_param, l_param); }
break; case WM_KEYUP: {
case WM_SYSKEYDOWN: gfx_dxgi_on_key_up(w_param, l_param);
return 0;
}
case WM_CHAR: {
// some keyboard input translated to a single UTF-16LE code unit
gfx_dxgi_on_text_input((wchar_t)w_param);
return 0;
}
case WM_SYSKEYDOWN: {
if ((w_param == VK_RETURN) && ((l_param & 1 << 30) == 0)) { if ((w_param == VK_RETURN) && ((l_param & 1 << 30) == 0)) {
toggle_borderless_window_full_screen(!dxgi.is_full_screen); toggle_borderless_window_full_screen(!dxgi.is_full_screen);
break; return 0;
} else {
return DefWindowProcW(h_wnd, message, w_param, l_param);
}
case WM_DROPFILES: {
HDROP hDrop = (HDROP)w_param;
UINT nFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
for (UINT i = 0; i < nFiles; i++)
{
char szFileName[MAX_PATH] = { 0 };
DragQueryFile(hDrop, i, szFileName, MAX_PATH);
mod_import_file(szFileName);
}
DragFinish(hDrop);
} }
break; break;
default: }
return DefWindowProcW(h_wnd, message, w_param, l_param); case WM_LBUTTONDOWN: {
if (!gRomIsValid) {
OPENFILENAMEW ofn;
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = h_wnd;
ofn.lpstrFilter = L"N64 ROM files (*.z64)\0*.z64\0";
ofn.lpstrFile = wcsFileName;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrTitle = L"Select the \"Super Mario 64 (U) [!]\" ROM file..";
ofn.Flags = (OFN_DONTADDTORECENT | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST);
wcsFileName[0] = L'\0';
if (GetOpenFileNameW(&ofn) && sys_windows_short_path_from_wcs(szFileName, MAX_PATH, wcsFileName)) {
rom_on_drop_file(szFileName);
}
return 0;
}
break;
}
case WM_DROPFILES: {
HDROP hDrop = (HDROP)w_param;
UINT nFiles = DragQueryFileW(hDrop, 0xFFFFFFFF, NULL, 0);
for (UINT i = 0; i < nFiles; i++) {
if (0 != DragQueryFileW(hDrop, i, wcsFileName, MAX_PATH)) {
if (sys_windows_short_path_from_wcs(szFileName, MAX_PATH, wcsFileName)) {
if (!gRomIsValid) {
rom_on_drop_file(szFileName);
} else if (gGameInited) {
mod_import_file(szFileName);
}
}
}
}
DragFinish(hDrop);
return 0;
}
} }
if (configWindow.reset) { return DefWindowProcW(h_wnd, message, w_param, l_param);
dxgi.last_maximized_state = false;
configWindow.reset = false;
configWindow.x = WAPI_WIN_CENTERPOS;
configWindow.y = WAPI_WIN_CENTERPOS;
configWindow.w = DESIRED_SCREEN_WIDTH;
configWindow.h = DESIRED_SCREEN_HEIGHT;
configWindow.fullscreen = false;
configWindow.settings_changed = true;
}
if (configWindow.settings_changed) {
configWindow.settings_changed = false;
update_screen_settings();
}
return 0;
} }
static void gfx_dxgi_init(const char *window_title) { static void gfx_dxgi_init(const char *window_title) {
@ -406,16 +425,7 @@ static void gfx_dxgi_set_keyboard_callbacks(bool (*on_key_down)(int scancode), b
} }
static void gfx_dxgi_main_loop(void (*run_one_game_iter)(void)) { static void gfx_dxgi_main_loop(void (*run_one_game_iter)(void)) {
dxgi.run_one_game_iter = run_one_game_iter; run_one_game_iter();
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
#ifdef DISCORD_SDK
discord_update();
#endif
}
} }
static void gfx_dxgi_get_dimensions(uint32_t *width, uint32_t *height) { static void gfx_dxgi_get_dimensions(uint32_t *width, uint32_t *height) {
@ -424,11 +434,28 @@ static void gfx_dxgi_get_dimensions(uint32_t *width, uint32_t *height) {
} }
static void gfx_dxgi_handle_events(void) { static void gfx_dxgi_handle_events(void) {
/*MSG msg; MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE) != 0) {
TranslateMessage(&msg); TranslateMessage(&msg);
DispatchMessage(&msg); DispatchMessageW(&msg);
}*/ if (msg.message == WM_QUIT) { break; }
}
if (configWindow.reset) {
dxgi.last_maximized_state = false;
configWindow.reset = false;
configWindow.x = WAPI_WIN_CENTERPOS;
configWindow.y = WAPI_WIN_CENTERPOS;
configWindow.w = DESIRED_SCREEN_WIDTH;
configWindow.h = DESIRED_SCREEN_HEIGHT;
configWindow.fullscreen = false;
configWindow.settings_changed = true;
}
if (configWindow.settings_changed) {
configWindow.settings_changed = false;
update_screen_settings();
}
} }
static uint64_t qpc_to_us(uint64_t qpc) { static uint64_t qpc_to_us(uint64_t qpc) {

View File

@ -186,13 +186,22 @@ static void gfx_sdl_onkeyup(int scancode) {
} }
static void gfx_sdl_ondropfile(char* path) { static void gfx_sdl_ondropfile(char* path) {
#ifdef _WIN32
char portable_path[SYS_MAX_PATH];
if (sys_windows_short_path_from_mbs(portable_path, SYS_MAX_PATH, path)) {
if (!gRomIsValid) {
rom_on_drop_file(portable_path);
} else if (gGameInited) {
mod_import_file(portable_path);
}
}
#else
if (!gRomIsValid) { if (!gRomIsValid) {
rom_on_drop_file(path); rom_on_drop_file(path);
return; } else if (gGameInited) {
}
if (gGameInited) {
mod_import_file(path); mod_import_file(path);
} }
#endif
} }
static void gfx_sdl_handle_events(void) { static void gfx_sdl_handle_events(void) {

View File

@ -168,7 +168,14 @@ bool str_ends_with(const char* string, const char* suffix) {
if (suffixLength > stringLength) { return false; } if (suffixLength > stringLength) { return false; }
return !strcmp(&string[stringLength - suffixLength], suffix); #ifdef _WIN32
// Paths on Windows are case-insensitive and might have
// upper-case or mixed-case endings.
return (0 == _stricmp(&(string[stringLength - suffixLength]), suffix));
#else
// Always expecting lower-case file paths and extensions
return (0 == strcmp(&(string[stringLength - suffixLength]), suffix));
#endif
} }
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////

View File

@ -353,17 +353,28 @@ int main(int argc, char *argv[]) {
// handle terminal arguments // handle terminal arguments
if (!parse_cli_opts(argc, argv)) { return 0; } if (!parse_cli_opts(argc, argv)) { return 0; }
#if defined(_WIN32) || defined(_WIN64) #ifdef _WIN32
// handle Windows console // handle Windows console
if (!gCLIOpts.console) { if (gCLIOpts.console) {
SetConsoleOutputCP(CP_UTF8);
} else {
FreeConsole(); FreeConsole();
freopen("NUL", "w", stdout); freopen("NUL", "w", stdout);
} }
#endif #endif
const char* userPath = gCLIOpts.savePath[0] ? gCLIOpts.savePath : sys_user_path(); #ifdef _WIN32
fs_init(userPath); if (gCLIOpts.savePath[0]) {
char portable_path[SYS_MAX_PATH] = {};
sys_windows_short_path_from_mbs(portable_path, SYS_MAX_PATH, gCLIOpts.savePath);
fs_init(portable_path);
} else {
fs_init(sys_user_path());
}
#else
fs_init(gCLIOpts.savePath[0] ? gCLIOpts.savePath : sys_user_path());
#endif
configfile_load(); configfile_load();
legacy_folder_handler(); legacy_folder_handler();

View File

@ -5,7 +5,7 @@
#include <string.h> #include <string.h>
#include <ctype.h> #include <ctype.h>
#if defined(_WIN32) || defined(_WIN64) #ifdef _WIN32
#include <windows.h> #include <windows.h>
#include <shlobj.h> #include <shlobj.h>
#include <shlwapi.h> #include <shlwapi.h>
@ -82,18 +82,120 @@ void sys_fatal(const char *fmt, ...) {
sys_fatal_impl(msg); sys_fatal_impl(msg);
} }
#if defined(_WIN32) || defined(_WIN64) #ifdef _WIN32
static BOOL sys_windows_short_path(LPSTR destPath, SIZE_T destSize, LPWSTR wideLongPath) static bool sys_windows_pathname_is_portable(const wchar_t *name, size_t size)
{ {
WCHAR wideShortPath[SYS_MAX_PATH]; for (size_t i = 0; i < size; i++) {
wchar_t c = name[i];
// character outside the ASCII printable range
if ((c < L' ') || (c > L'~')) { return false; }
// characters unallowed in filenames
switch (c) {
// skipping ':', as it will appear with the drive specifier
case L'<': case L'>': case L'/': case L'\\':
case L'"': case L'|': case L'?': case L'*':
return false;
}
}
return true;
}
static wchar_t *sys_windows_pathname_get_delim(const wchar_t *name)
{
const wchar_t *sep1 = wcschr(name, L'/');
const wchar_t *sep2 = wcschr(name, L'\\');
if (NULL == sep1) { return (wchar_t*)sep2; }
if (NULL == sep2) { return (wchar_t*)sep1; }
return (sep1 < sep2) ? (wchar_t*)sep1 : (wchar_t*)sep2;
}
bool sys_windows_short_path_from_wcs(char *destPath, size_t destSize, const wchar_t *wcsLongPath)
{
wchar_t wcsShortPath[SYS_MAX_PATH]; // converted with WinAPI
wchar_t wcsPortablePath[SYS_MAX_PATH]; // non-unicode parts replaced back with long forms
// Convert the Long Path in Wide Format to the alternate short form. // Convert the Long Path in Wide Format to the alternate short form.
// It will still point to already existing directory. // It will still point to already existing directory or file.
if (0 == GetShortPathNameW(wideLongPath, wideShortPath, SYS_MAX_PATH)) { return FALSE; } if (0 == GetShortPathNameW(wcsLongPath, wcsShortPath, SYS_MAX_PATH)) { return FALSE; }
// Scanning the paths side-by-side, to keep the portable (ASCII)
// parts of the absolute path unchanged (in the long form)
wcsPortablePath[0] = L'\0';
const wchar_t *longPart = wcsLongPath;
wchar_t *shortPart = wcsShortPath;
while (true) {
int longLength;
int shortLength;
const wchar_t *sourcePart;
int sourceLength;
int bufferLength;
const wchar_t *longDelim = sys_windows_pathname_get_delim(longPart);
wchar_t *shortDelim = sys_windows_pathname_get_delim(shortPart);
if (NULL == longDelim) {
longLength = wcslen(longPart); // final part of the scanned path
} else {
longLength = longDelim - longPart; // ptr diff measured in WCHARs
}
if (NULL == shortDelim) {
shortLength = wcslen(shortPart); // final part of the scanned path
} else {
shortLength = shortDelim - shortPart; // ptr diff measured in WCHARs
}
if (sys_windows_pathname_is_portable(longPart, longLength)) {
// take the original name (subdir or filename)
sourcePart = longPart;
sourceLength = longLength;
} else {
// take the converted alternate (short) name
sourcePart = shortPart;
sourceLength = shortLength;
}
// take into account the slash-or-backslash separator
if (L'\0' != sourcePart[sourceLength]) { sourceLength++; }
// how many WCHARs are still left in the buffer
bufferLength = (SYS_MAX_PATH - 1) - wcslen(wcsPortablePath);
if (sourceLength > bufferLength) { return false; }
wcsncat(wcsPortablePath, sourcePart, sourceLength);
// path end reached?
if ((NULL == longDelim) || (NULL == shortDelim)) { break; }
// compare the next name
longPart = longDelim + 1;
shortPart = shortDelim + 1;
}
// Short Path can be safely represented by the US-ASCII Charset. // Short Path can be safely represented by the US-ASCII Charset.
return (WideCharToMultiByte(CP_ACP, 0, wideShortPath, (-1), destPath, destSize, NULL, NULL) > 0); return (WideCharToMultiByte(CP_ACP, 0, wcsPortablePath, (-1), destPath, destSize, NULL, NULL) > 0);
}
bool sys_windows_short_path_from_mbs(char *destPath, size_t destSize, const char *mbsLongPath)
{
// Converting the absolute path in UTF-8 format (MultiByte String)
// to an alternate (portable) format usable on Windows.
// Assuming the given paths points to an already existing file or folder.
wchar_t wcsWidePath[SYS_MAX_PATH];
if (MultiByteToWideChar(CP_UTF8, 0, mbsLongPath, (-1), wcsWidePath, SYS_MAX_PATH) > 0)
{
return sys_windows_short_path_from_wcs(destPath, destSize, wcsWidePath);
}
return false;
} }
const char *sys_user_path(void) const char *sys_user_path(void)
@ -142,7 +244,7 @@ const char *sys_user_path(void)
if (ERROR_ALREADY_EXISTS != GetLastError()) { return NULL; } if (ERROR_ALREADY_EXISTS != GetLastError()) { return NULL; }
} }
return sys_windows_short_path(shortPath, SYS_MAX_PATH, widePath) ? shortPath : NULL; return sys_windows_short_path_from_wcs(shortPath, SYS_MAX_PATH, widePath) ? shortPath : NULL;
} }
const char *sys_exe_path(void) const char *sys_exe_path(void)
@ -157,7 +259,7 @@ const char *sys_exe_path(void)
if (NULL != lastBackslash) { *lastBackslash = L'\0'; } if (NULL != lastBackslash) { *lastBackslash = L'\0'; }
else { return NULL; } else { return NULL; }
return sys_windows_short_path(shortPath, SYS_MAX_PATH, widePath) ? shortPath : NULL; return sys_windows_short_path_from_wcs(shortPath, SYS_MAX_PATH, widePath) ? shortPath : NULL;
} }
static void sys_fatal_impl(const char *msg) { static void sys_fatal_impl(const char *msg) {

View File

@ -15,6 +15,10 @@ char *sys_strlwr(char *src);
int sys_strcasecmp(const char *s1, const char *s2); int sys_strcasecmp(const char *s1, const char *s2);
// path stuff // path stuff
#ifdef _WIN32
bool sys_windows_short_path_from_wcs(char *destPath, size_t destSize, const wchar_t *wcsLongPath);
bool sys_windows_short_path_from_mbs(char* destPath, size_t destSize, const char *mbsLongPath);
#endif
const char *sys_user_path(void); const char *sys_user_path(void);
const char *sys_exe_path(void); const char *sys_exe_path(void);
const char *sys_file_extension(const char *fpath); const char *sys_file_extension(const char *fpath);