og-edopro/gframe/utils_gui.cpp

438 lines
16 KiB
C++

#include "utils_gui.h"
#include <irrlicht.h>
#include <ICursorControl.h>
#include "config.h"
#include "utils.h"
#include "game_config.h"
#include "text_types.h"
#include "porting.h"
#include "fmt.h"
#include "localtime.h"
#if EDOPRO_WINDOWS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <vector>
#include "logging.h"
#include "Base64.h"
#if !(IRRLICHT_VERSION_MAJOR==1 && IRRLICHT_VERSION_MINOR==9)
#include "IrrlichtCommonIncludes/CCursorControl.h"
using CCursorControl = irr::CCursorControl;
#endif
#elif EDOPRO_MACOS
#include <CoreFoundation/CoreFoundation.h>
#elif EDOPRO_LINUX
#if !(IRRLICHT_VERSION_MAJOR==1 && IRRLICHT_VERSION_MINOR==9)
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#endif
#include <unistd.h>
#include <sys/wait.h>
#include <memory>
static inline bool try_guess_wayland() {
const char* env = getenv("XDG_SESSION_TYPE");
return env == nullptr || env != "x11"sv;
}
#endif //EDOPRO_WINDOWS
namespace ygo {
#if EDOPRO_WINDOWS
static HWND GetWindowHandle(irr::video::IVideoDriver* driver) {
switch(driver->getDriverType()) {
#if !(IRRLICHT_VERSION_MAJOR==1 && IRRLICHT_VERSION_MINOR==9)
case irr::video::EDT_DIRECT3D8:
return static_cast<HWND>(driver->getExposedVideoData().D3D8.HWnd);
#endif
case irr::video::EDT_DIRECT3D9:
#if (IRRLICHT_VERSION_MAJOR==1 && IRRLICHT_VERSION_MINOR==9)
case irr::video::EDT_DIRECT3D9_ON_12:
#endif
return static_cast<HWND>(driver->getExposedVideoData().D3D9.HWnd);
case irr::video::EDT_OPENGL:
#if (IRRLICHT_VERSION_MAJOR==1 && IRRLICHT_VERSION_MINOR==9)
case irr::video::EDT_OGLES1:
case irr::video::EDT_OGLES2:
#endif
return static_cast<HWND>(driver->getExposedVideoData().OpenGLWin32.HWnd);
default:
break;
}
return nullptr;
}
#endif
static inline irr::video::E_DRIVER_TYPE getDefaultDriver([[maybe_unused]] irr::E_DEVICE_TYPE device_type) {
#if EDOPRO_ANDROID
return irr::video::EDT_OGLES2;
#elif EDOPRO_IOS
return irr::video::EDT_OGLES1;
#elif EDOPRO_LINUX && (IRRLICHT_VERSION_MAJOR==1 && IRRLICHT_VERSION_MINOR==9)
if(device_type == irr::E_DEVICE_TYPE::EIDT_WAYLAND)
return irr::video::EDT_OGLES2;
return irr::video::EDT_OPENGL;
#elif EDOPRO_WINDOWS && defined(IRR_COMPILE_WITH_DX9_DEV_PACK)
return irr::video::EDT_DIRECT3D9;
#else
return irr::video::EDT_OPENGL;
#endif
}
std::shared_ptr<irr::IrrlichtDevice> GUIUtils::CreateDevice(GameConfig* configs) {
irr::SIrrlichtCreationParameters params{};
params.AntiAlias = configs->antialias;
params.Vsync = (!EDOPRO_MACOS) && configs->vsync;
#if (IRRLICHT_VERSION_MAJOR==1 && IRRLICHT_VERSION_MINOR==9)
// This correspond to the program's class name, used by window managers and
// desktop environments to group multiple instances with their desktop file
params.ClassName = EPRO_TEXT("edopro");
params.WindowCaption = EPRO_TEXT("Project Ignis: EDOPro");
params.OGLES2ShaderPath = EPRO_TEXT("BUNDLED");
params.WindowResizable = true;
#if EDOPRO_WINDOWS
params.WindowIcon = MAKEINTRESOURCE(1);
#endif
#if EDOPRO_LINUX
if(configs->useWayland == 2) {
if(!try_guess_wayland())
configs->useWayland = 0;
} else if(configs->useWayland == 1 && try_guess_wayland()) {
params.DeviceType = irr::E_DEVICE_TYPE::EIDT_WAYLAND;
epro::print("You're using the wayland device backend.\n"
"Keep in mind that it's still experimental and might be unstable.\n"
"If you are getting any major issues, or the game doesn't start,\n"
"you can manually disable this option from the system.conf file by toggling the useWayland option.\n"
"Feel free to report any issues you encounter.\n");
}
#endif
#if EDOPRO_MACOS
params.UseIntegratedGPU = configs->useIntegratedGpu > 0;
#endif
#endif
#if !EDOPRO_ANDROID
params.WindowSize = { (irr::u32)(1024 * configs->dpi_scale), (irr::u32)(640 * configs->dpi_scale) };
#else
params.PrivateData = porting::app_global;
params.Bits = 24;
params.ZBufferBits = 16;
params.AntiAlias = 0;
params.WindowSize = {};
#endif
if(configs->driver_type == irr::video::EDT_COUNT)
params.DriverType = getDefaultDriver(params.DeviceType);
else
params.DriverType = configs->driver_type;
irr::IrrlichtDevice* device = irr::createDeviceEx(params);
if(!device)
throw std::runtime_error("Failed to create Irrlicht Engine device!");
const auto driver = device->getVideoDriver();
if(!driver)
throw std::runtime_error("Failed to create video driver!");
Utils::irrTimer = device->getTimer();
#if EDOPRO_ANDROID
auto filesystem = device->getFileSystem();
// The Android assets file-system does not know which sub-directories it has (blame google).
// So we have to add all sub-directories in assets manually. Otherwise we could still open the files,
// but existFile checks will fail (which are for example needed by getFont).
for(irr::u32 i = 0; i < filesystem->getFileArchiveCount(); ++i) {
auto archive = filesystem->getFileArchive(i);
if(archive->getType() == irr::io::EFAT_ANDROID_ASSET) {
archive->addDirectoryToFileList("media/");
break;
}
}
#endif
#if EDOPRO_ANDROID || EDOPRO_IOS
device->getGUIEnvironment()->setOSOperator(Utils::OSOperator);
#endif
#if (IRRLICHT_VERSION_MAJOR==1 && IRRLICHT_VERSION_MINOR==9)
if(auto driver_type = driver->getDriverType(); driver_type == irr::video::EDT_OGLES1 || driver_type == irr::video::EDT_OGLES2) {
auto& InitMaterial2D = driver->getMaterial2D();
for (auto& layer : InitMaterial2D.TextureLayer)
{
layer.TextureWrapU = irr::video::ETC_CLAMP_TO_EDGE;
layer.TextureWrapV = irr::video::ETC_CLAMP_TO_EDGE;
layer.TextureWrapW = irr::video::ETC_CLAMP_TO_EDGE;
}
driver->enableMaterial2D(true);
}
#endif
driver->setTextureCreationFlag(irr::video::ETCF_CREATE_MIP_MAPS, false);
driver->setTextureCreationFlag(irr::video::ETCF_OPTIMIZED_FOR_QUALITY, true);
#if !(IRRLICHT_VERSION_MAJOR==1 && IRRLICHT_VERSION_MINOR==9)
device->setWindowCaption(L"Project Ignis: EDOPro");
device->setResizable(true);
#if EDOPRO_WINDOWS
auto hInstance = static_cast<HINSTANCE>(GetModuleHandle(nullptr));
auto hSmallIcon = static_cast<HICON>(LoadImage(hInstance, MAKEINTRESOURCE(1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CXSMICON), LR_DEFAULTCOLOR));
auto hBigIcon = static_cast<HICON>(LoadImage(hInstance, MAKEINTRESOURCE(1), IMAGE_ICON, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR));
{
auto hWnd = GetWindowHandle(driver);
SendMessage(hWnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(hSmallIcon));
SendMessage(hWnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(hBigIcon));
}
#endif
#endif
#if EDOPRO_WINDOWS
if(gGameConfig->windowStruct.size()) {
auto winstruct = base64_decode(gGameConfig->windowStruct);
if(winstruct.size() == sizeof(WINDOWPLACEMENT)) {
WINDOWPLACEMENT wp;
memcpy(&wp, winstruct.data(), sizeof(WINDOWPLACEMENT));
if(wp.length == sizeof(WINDOWPLACEMENT)) {
auto hWnd = GetWindowHandle(driver);
SetWindowPlacement(hWnd, &wp);
}
}
}
#elif EDOPRO_MACOS
if(gGameConfig->windowStruct.size())
porting::setWindowRect(driver->getExposedVideoData().OpenGLOSX.Window, gGameConfig->windowStruct.data());
#endif
device->getLogger()->setLogLevel(irr::ELL_ERROR);
return std::shared_ptr<irr::IrrlichtDevice>(device, [](irr::IrrlichtDevice* ptr){
ptr->drop();
});
}
void GUIUtils::ChangeCursor(irr::IrrlichtDevice* device, /*irr::gui::ECURSOR_ICON*/ int _icon) {
#if !EDOPRO_ANDROID && !EDOPRO_IOS
auto icon = static_cast<irr::gui::ECURSOR_ICON>(_icon);
auto cursor = device->getCursorControl();
if (cursor->getActiveIcon() != icon) {
cursor->setActiveIcon(icon);
}
#endif
}
bool GUIUtils::TakeScreenshot(irr::IrrlichtDevice* device) {
const auto driver = device->getVideoDriver();
const auto image = driver->createScreenShot();
if(!image)
return false;
const auto now = std::time(nullptr);
const auto filename = epro::format(EPRO_TEXT("screenshots/EDOPro {:%Y-%m-%d %H-%M-%S}.png"), epro::localtime(now));
auto written = driver->writeImageToFile(image, { filename.data(), static_cast<irr::u32>(filename.size()) });
if(!written)
device->getLogger()->log(L"Failed to take screenshot.", irr::ELL_WARNING);
image->drop();
return written;
}
#if (IRRLICHT_VERSION_MAJOR==1 && IRRLICHT_VERSION_MINOR==9)
void GUIUtils::ToggleFullscreen(irr::IrrlichtDevice* device, [[maybe_unused]] bool& fullscreen) {
#if EDOPRO_MACOS
porting::toggleFullScreen();
#elif EDOPRO_WINDOWS || EDOPRO_LINUX || EDOPRO_HAIKU
device->toggleFullscreen(!std::exchange(fullscreen, !fullscreen));
#endif
}
#else
#if EDOPRO_WINDOWS
//gcc on mingw can't convert lambda to __stdcall function
static BOOL CALLBACK callback(HMONITOR hMon, HDC hdc, LPRECT lprcMonitor, LPARAM pData) {
auto monitors = reinterpret_cast<std::vector<RECT>*>(pData);
monitors->push_back(*lprcMonitor);
return TRUE;
}
#endif
void GUIUtils::ToggleFullscreen(irr::IrrlichtDevice* device, [[maybe_unused]] bool& fullscreen) {
#if EDOPRO_MACOS
porting::toggleFullScreen();
#elif EDOPRO_WINDOWS
static WINDOWPLACEMENT nonFullscreenSize;
static LONG_PTR nonFullscreenStyle;
static constexpr LONG_PTR fullscreenStyle = WS_POPUP | WS_SYSMENU | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
static const auto monitors = [] {
std::vector<RECT> ret;
EnumDisplayMonitors(0, 0, callback, reinterpret_cast<LPARAM>(&ret));
return ret;
}();
fullscreen = !fullscreen;
const auto driver = device->getVideoDriver();
auto hWnd = GetWindowHandle(driver);
if(fullscreen) {
GetWindowPlacement(hWnd, &nonFullscreenSize);
nonFullscreenStyle = GetWindowLongPtr(hWnd, GWL_STYLE);
RECT curSize{};
GetWindowRect(hWnd, &curSize);
const POINT windowCenter = { (curSize.left + (curSize.right - curSize.left) / 2), (curSize.top + (curSize.bottom - curSize.top) / 2) };
for(const auto& rect : monitors) {
if(PtInRect(&rect, windowCenter)) {
curSize = rect;
break;
}
}
if(!SetWindowLongPtr(hWnd, GWL_STYLE, fullscreenStyle))
ErrorLog("Could not change window style.");
const auto width = curSize.right - curSize.left;
const auto height = curSize.bottom - curSize.top;
SetWindowPos(hWnd, HWND_TOP, curSize.left, curSize.top, width, height, SWP_FRAMECHANGED | SWP_SHOWWINDOW);
} else {
SetWindowPlacement(hWnd, &nonFullscreenSize);
SetWindowLongPtr(hWnd, GWL_STYLE, nonFullscreenStyle);
SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
}
static_cast<CCursorControl*>(device->getCursorControl())->updateBorderSize(fullscreen, true);
#elif EDOPRO_LINUX
// If irrlicht 1.8 is being used, x11 is always hard linked
struct {
unsigned long flags;
unsigned long functions;
unsigned long decorations;
long inputMode;
unsigned long status;
} hints{};
auto display = XOpenDisplay(nullptr);
Window window;
static bool wasHorizontalMaximized = false, wasVerticalMaximized = false;
int revert;
fullscreen = !fullscreen;
XGetInputFocus(display, &window, &revert);
Atom wm_state = XInternAtom(display, "_NET_WM_STATE", false);
Atom max_horz = XInternAtom(display, "_NET_WM_STATE_MAXIMIZED_HORZ", false);
Atom max_vert = XInternAtom(display, "_NET_WM_STATE_MAXIMIZED_VERT", false);
auto checkMaximized = [&]() {
const long maxLength = 1024;
Atom actualType;
int actualFormat;
unsigned long numItems, bytesAfter;
unsigned char* propertyValue = nullptr;
wasVerticalMaximized = false;
wasHorizontalMaximized = false;
if(XGetWindowProperty(display, window, wm_state,
0l, maxLength, false, XA_ATOM, &actualType,
&actualFormat, &numItems, &bytesAfter,
&propertyValue) == Success) {
Atom* atoms = (Atom*)propertyValue;
for(unsigned long i = 0; i < numItems; ++i) {
if(atoms[i] == max_vert) {
wasVerticalMaximized = true;
} else if(atoms[i] == max_horz) {
wasHorizontalMaximized = true;
}
}
XFree(propertyValue);
}
};
if(fullscreen)
checkMaximized();
if(!wasHorizontalMaximized && !wasVerticalMaximized) {
XEvent xev = {};
xev.type = ClientMessage;
xev.xclient.window = window;
xev.xclient.message_type = wm_state;
xev.xclient.format = 32;
xev.xclient.data.l[0] = fullscreen ? 1 : 0;
int i = 1;
if(!wasHorizontalMaximized)
xev.xclient.data.l[i++] = max_horz;
if(!wasVerticalMaximized)
xev.xclient.data.l[i++] = max_vert;
if(i == 2)
xev.xclient.data.l[i] = 0;
XSendEvent(display, DefaultRootWindow(display), False, SubstructureNotifyMask, &xev);
}
Atom property = XInternAtom(display, "_MOTIF_WM_HINTS", true);
hints.flags = 2;
hints.decorations = fullscreen ? 0 : 1;
XChangeProperty(display, window, property, property, 32, PropModeReplace, (unsigned char*)&hints, 5);
XMapWindow(display, window);
XFlush(display);
#endif //EDOPRO_MACOS
}
#endif
void GUIUtils::ShowErrorWindow(epro::stringview context, epro::stringview message) {
#if EDOPRO_WINDOWS
MessageBox(nullptr, Utils::ToPathString(message).data(), Utils::ToPathString(context).data(), MB_OK | MB_ICONERROR);
#elif EDOPRO_MACOS
CFStringRef header_ref = CFStringCreateWithCString(nullptr, context.data(), context.size());
CFStringRef message_ref = CFStringCreateWithCString(nullptr, message.data(), message.size());
CFOptionFlags result; //result code from the message box
//launch the message box
CFUserNotificationDisplayAlert(
0, // no timeout
kCFUserNotificationStopAlertLevel, //change it depending message_type flags ( MB_ICONASTERISK.... etc.)
nullptr, //icon url, use default, you can change it depending message_type flags
nullptr, //not used
nullptr, //localization of strings
header_ref, //header text
message_ref, //message text
nullptr, //default "ok" text in button
nullptr, //alternate button title
nullptr, //other button title, null--> no other button
&result //response flags
);
//Clean up the strings
CFRelease(header_ref);
CFRelease(message_ref);
#elif EDOPRO_LINUX
const auto* context_cstr = context.data();
const auto* message_cstr = message.data();
const auto xmessage = epro::format("{}\n{}", context, message);
const auto xmessage_cstr = xmessage.data();
auto pid = vfork();
if(pid == 0) {
execl("/usr/bin/kdialog", "kdialog", "--title", context_cstr, "--error", message_cstr, nullptr);
execl("/usr/bin/zenity", "zenity", "--title", context_cstr, "--error", message_cstr, nullptr);
execl("/usr/bin/xmessage", "xmessage", xmessage_cstr, nullptr);
_exit(EXIT_FAILURE);
} else if(pid > 0)
(void)waitpid(pid, nullptr, 0);
#elif EDOPRO_ANDROID || EDOPRO_IOS
porting::showErrorDialog(context, message);
#endif
}
template<typename T>
inline auto SetSwapInterval(T* driver, int interval) -> decltype(driver->setSwapInterval(interval)) {
driver->setSwapInterval(interval);
}
template<typename T>
inline auto SetSwapInterval(T* driver, int interval) -> decltype(driver->setVsync(!!interval)) {
driver->setVsync(!!interval);
}
void GUIUtils::ToggleSwapInterval(irr::video::IVideoDriver* driver, int interval) {
SetSwapInterval(driver, interval);
}
std::string GUIUtils::SerializeWindowPosition(irr::IrrlichtDevice* device) {
#if EDOPRO_WINDOWS
auto hWnd = GetWindowHandle(device->getVideoDriver());
WINDOWPLACEMENT wp;
wp.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(hWnd, &wp);
return base64_encode<std::string>(reinterpret_cast<uint8_t*>(&wp), sizeof(wp));
#elif EDOPRO_MACOS
return porting::getWindowRect(device->getVideoDriver()->getExposedVideoData().OpenGLOSX.Window);
#else
return std::string{};
#endif
}
void GUIUtils::TriggerEvent(irr::IrrlichtDevice* device, irr::gui::IGUIElement* target, /*irr::gui::EGUI_EVENT_TYPE*/ int type) {
irr::SEvent event;
event.EventType = irr::EET_GUI_EVENT;
event.GUIEvent.EventType = static_cast<irr::gui::EGUI_EVENT_TYPE>(type);
event.GUIEvent.Caller = target;
device->postEventFromUser(event);
}
void GUIUtils::ClickButton(irr::IrrlichtDevice* device, irr::gui::IGUIElement* btn) {
TriggerEvent(device, btn, irr::gui::EGET_BUTTON_CLICKED);
}
void GUIUtils::SetCheckbox(irr::IrrlichtDevice* device, irr::gui::IGUICheckBox* chk, bool state) {
chk->setChecked(state);
TriggerEvent(device, chk, irr::gui::EGET_CHECKBOX_CHANGED);
}
}