2018-07-11 13:35:31 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// The Win32-based implementation of platform-dependent GUI functionality.
|
|
|
|
//
|
|
|
|
// Copyright 2018 whitequark
|
|
|
|
//-----------------------------------------------------------------------------
|
2018-07-13 03:29:44 +08:00
|
|
|
#include "config.h"
|
2018-07-11 13:35:31 +08:00
|
|
|
#include "solvespace.h"
|
|
|
|
// Include after solvespace.h to avoid identifier clashes.
|
|
|
|
#include <windows.h>
|
2018-07-13 03:29:44 +08:00
|
|
|
#include <windowsx.h>
|
|
|
|
#include <commctrl.h>
|
2018-07-18 02:51:00 +08:00
|
|
|
#include <commdlg.h>
|
2018-07-17 20:32:58 +08:00
|
|
|
#include <shellapi.h>
|
2018-07-13 03:29:44 +08:00
|
|
|
|
2019-05-23 16:01:26 +08:00
|
|
|
// Macros to compile under XP
|
|
|
|
#if !defined(LSTATUS)
|
|
|
|
# define LSTATUS LONG
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#if !defined(MAPVK_VK_TO_CHAR)
|
|
|
|
# define MAPVK_VK_TO_CHAR 2
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#if !defined(USER_DEFAULT_SCREEN_DPI)
|
|
|
|
# define USER_DEFAULT_SCREEN_DPI 96
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#if !defined(TTM_POPUP)
|
|
|
|
# define TTM_POPUP (WM_USER + 34)
|
|
|
|
#endif
|
|
|
|
// End macros to compile under XP
|
|
|
|
|
2018-07-18 08:48:49 +08:00
|
|
|
#if !defined(WM_DPICHANGED)
|
|
|
|
# define WM_DPICHANGED 0x02E0
|
2018-07-13 03:29:44 +08:00
|
|
|
#endif
|
|
|
|
|
2018-07-17 23:00:46 +08:00
|
|
|
// These interfere with our identifiers.
|
2018-07-13 03:29:44 +08:00
|
|
|
#undef CreateWindow
|
2018-07-17 23:00:46 +08:00
|
|
|
#undef ERROR
|
2018-07-13 03:29:44 +08:00
|
|
|
|
|
|
|
#if HAVE_OPENGL == 3
|
2018-07-18 08:48:49 +08:00
|
|
|
# define EGLAPI /*static linkage*/
|
2019-05-23 18:58:31 +08:00
|
|
|
# define EGL_EGLEXT_PROTOTYPES
|
2018-07-18 08:48:49 +08:00
|
|
|
# include <EGL/egl.h>
|
2019-05-23 18:58:31 +08:00
|
|
|
# include <EGL/eglext.h>
|
2018-07-18 08:48:49 +08:00
|
|
|
#endif
|
|
|
|
|
|
|
|
#if defined(HAVE_SPACEWARE)
|
|
|
|
# include <si.h>
|
|
|
|
# include <siapp.h>
|
|
|
|
# undef uint32_t
|
2018-07-13 03:29:44 +08:00
|
|
|
#endif
|
2018-07-11 13:35:31 +08:00
|
|
|
|
2019-05-21 06:12:45 +08:00
|
|
|
#if defined(__GNUC__)
|
|
|
|
// Disable bogus warning emitted by GCC on GetProcAddress, since there seems to be no way
|
|
|
|
// of restructuring the code to easily disable it just at the call site.
|
|
|
|
#pragma GCC diagnostic ignored "-Wcast-function-type"
|
|
|
|
#endif
|
|
|
|
|
2018-07-11 13:35:31 +08:00
|
|
|
namespace SolveSpace {
|
|
|
|
namespace Platform {
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Windows API bridging
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
#define sscheck(expr) do { \
|
|
|
|
SetLastError(0); \
|
|
|
|
if(!(expr)) \
|
|
|
|
CheckLastError(__FILE__, __LINE__, __func__, #expr); \
|
|
|
|
} while(0)
|
|
|
|
|
|
|
|
void CheckLastError(const char *file, int line, const char *function, const char *expr) {
|
|
|
|
if(GetLastError() != S_OK) {
|
|
|
|
LPWSTR messageW;
|
|
|
|
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM,
|
|
|
|
NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
|
|
(LPWSTR)&messageW, 0, NULL);
|
|
|
|
|
|
|
|
std::string message;
|
|
|
|
message += ssprintf("File %s, line %u, function %s:\n", file, line, function);
|
|
|
|
message += ssprintf("Win32 API call failed: %s.\n", expr);
|
|
|
|
message += ssprintf("Error: %s", Narrow(messageW).c_str());
|
|
|
|
FatalError(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
typedef UINT (WINAPI *LPFNGETDPIFORWINDOW)(HWND);
|
|
|
|
|
|
|
|
UINT ssGetDpiForWindow(HWND hwnd) {
|
|
|
|
static bool checked;
|
|
|
|
static LPFNGETDPIFORWINDOW lpfnGetDpiForWindow;
|
|
|
|
if(!checked) {
|
|
|
|
checked = true;
|
|
|
|
lpfnGetDpiForWindow = (LPFNGETDPIFORWINDOW)
|
|
|
|
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow");
|
|
|
|
}
|
|
|
|
if(lpfnGetDpiForWindow) {
|
|
|
|
return lpfnGetDpiForWindow(hwnd);
|
|
|
|
} else {
|
|
|
|
HDC hDc;
|
|
|
|
sscheck(hDc = GetDC(HWND_DESKTOP));
|
|
|
|
UINT dpi;
|
|
|
|
sscheck(dpi = GetDeviceCaps(hDc, LOGPIXELSX));
|
|
|
|
sscheck(ReleaseDC(HWND_DESKTOP, hDc));
|
|
|
|
return dpi;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
typedef BOOL (WINAPI *LPFNADJUSTWINDOWRECTEXFORDPI)(LPRECT, DWORD, BOOL, DWORD, UINT);
|
|
|
|
|
|
|
|
BOOL ssAdjustWindowRectExForDpi(LPRECT lpRect, DWORD dwStyle, BOOL bMenu,
|
|
|
|
DWORD dwExStyle, UINT dpi) {
|
|
|
|
static bool checked;
|
|
|
|
static LPFNADJUSTWINDOWRECTEXFORDPI lpfnAdjustWindowRectExForDpi;
|
|
|
|
if(!checked) {
|
|
|
|
checked = true;
|
|
|
|
lpfnAdjustWindowRectExForDpi = (LPFNADJUSTWINDOWRECTEXFORDPI)
|
|
|
|
GetProcAddress(GetModuleHandleW(L"user32.dll"), "AdjustWindowRectExForDpi");
|
|
|
|
}
|
|
|
|
if(lpfnAdjustWindowRectExForDpi) {
|
|
|
|
return lpfnAdjustWindowRectExForDpi(lpRect, dwStyle, bMenu, dwExStyle, dpi);
|
|
|
|
} else {
|
|
|
|
return AdjustWindowRectEx(lpRect, dwStyle, bMenu, dwExStyle);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Utility functions
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2018-07-17 23:00:46 +08:00
|
|
|
static std::wstring PrepareTitle(const std::string &s) {
|
2018-07-13 03:29:44 +08:00
|
|
|
return Widen("SolveSpace - " + s);
|
|
|
|
}
|
|
|
|
|
2019-04-13 19:00:35 +08:00
|
|
|
static std::string NegateMnemonics(const std::string &label) {
|
|
|
|
std::string newLabel;
|
|
|
|
for(char c : label) {
|
|
|
|
newLabel.push_back(c);
|
|
|
|
if(c == '&') newLabel.push_back(c);
|
|
|
|
}
|
|
|
|
return newLabel;
|
|
|
|
}
|
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
static int Clamp(int x, int a, int b) {
|
|
|
|
return max(a, min(x, b));
|
|
|
|
}
|
|
|
|
|
2018-07-17 20:32:58 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Fatal errors
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
bool handlingFatalError = false;
|
|
|
|
|
|
|
|
void FatalError(std::string message) {
|
|
|
|
// Indicate that we're handling a fatal error, to avoid re-entering application code
|
|
|
|
// and potentially crashing even harder.
|
|
|
|
handlingFatalError = true;
|
|
|
|
|
|
|
|
message += "\nGenerate debug report?";
|
|
|
|
switch(MessageBoxW(NULL, Platform::Widen(message).c_str(),
|
|
|
|
L"Fatal error — SolveSpace",
|
|
|
|
MB_ICONERROR|MB_TASKMODAL|MB_SETFOREGROUND|MB_TOPMOST|
|
|
|
|
MB_OKCANCEL|MB_DEFBUTTON2)) {
|
|
|
|
case IDOK:
|
|
|
|
abort();
|
|
|
|
|
|
|
|
case IDCANCEL:
|
|
|
|
default: {
|
|
|
|
WCHAR appPath[MAX_PATH] = {};
|
|
|
|
GetModuleFileNameW(NULL, appPath, sizeof(appPath));
|
|
|
|
ShellExecuteW(NULL, L"open", appPath, NULL, NULL, SW_SHOW);
|
|
|
|
_exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-07-16 18:37:41 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Settings
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class SettingsImplWin32 final : public Settings {
|
2018-07-16 18:37:41 +08:00
|
|
|
public:
|
|
|
|
HKEY hKey = NULL;
|
|
|
|
|
|
|
|
HKEY GetKey() {
|
|
|
|
if(hKey == NULL) {
|
|
|
|
sscheck(RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\SolveSpace", 0, NULL, 0,
|
|
|
|
KEY_ALL_ACCESS, NULL, &hKey, NULL));
|
|
|
|
}
|
|
|
|
return hKey;
|
|
|
|
}
|
|
|
|
|
|
|
|
~SettingsImplWin32() {
|
|
|
|
if(hKey != NULL) {
|
|
|
|
sscheck(RegCloseKey(hKey));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void FreezeInt(const std::string &key, uint32_t value) {
|
|
|
|
sscheck(RegSetValueExW(GetKey(), &Widen(key)[0], 0,
|
|
|
|
REG_DWORD, (const BYTE *)&value, sizeof(value)));
|
|
|
|
}
|
|
|
|
|
|
|
|
uint32_t ThawInt(const std::string &key, uint32_t defaultValue) {
|
|
|
|
DWORD value;
|
|
|
|
DWORD type, length = sizeof(value);
|
|
|
|
LSTATUS result = RegQueryValueEx(GetKey(), &Widen(key)[0], 0,
|
|
|
|
&type, (BYTE *)&value, &length);
|
|
|
|
if(result == ERROR_SUCCESS && type == REG_DWORD) {
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
void FreezeFloat(const std::string &key, double value) {
|
|
|
|
sscheck(RegSetValueExW(GetKey(), &Widen(key)[0], 0,
|
|
|
|
REG_QWORD, (const BYTE *)&value, sizeof(value)));
|
|
|
|
}
|
|
|
|
|
|
|
|
double ThawFloat(const std::string &key, double defaultValue) {
|
|
|
|
double value;
|
|
|
|
DWORD type, length = sizeof(value);
|
|
|
|
LSTATUS result = RegQueryValueEx(GetKey(), &Widen(key)[0], 0,
|
|
|
|
&type, (BYTE *)&value, &length);
|
|
|
|
if(result == ERROR_SUCCESS && type == REG_QWORD) {
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
void FreezeString(const std::string &key, const std::string &value) {
|
|
|
|
ssassert(value.length() == strlen(value.c_str()),
|
|
|
|
"illegal null byte in middle of a string setting");
|
|
|
|
std::wstring valueW = Widen(value);
|
|
|
|
sscheck(RegSetValueExW(GetKey(), &Widen(key)[0], 0,
|
|
|
|
REG_SZ, (const BYTE *)&valueW[0], (valueW.length() + 1) * 2));
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ThawString(const std::string &key, const std::string &defaultValue) {
|
|
|
|
DWORD type, length = 0;
|
|
|
|
LSTATUS result = RegQueryValueEx(GetKey(), &Widen(key)[0], 0,
|
|
|
|
&type, NULL, &length);
|
|
|
|
if(result == ERROR_SUCCESS && type == REG_SZ) {
|
|
|
|
std::wstring valueW;
|
|
|
|
valueW.resize(length / 2 - 1);
|
|
|
|
sscheck(RegQueryValueEx(GetKey(), &Widen(key)[0], 0,
|
|
|
|
&type, (BYTE *)&valueW[0], &length));
|
|
|
|
return Narrow(valueW);
|
|
|
|
}
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
SettingsRef GetSettings() {
|
|
|
|
return std::make_shared<SettingsImplWin32>();
|
|
|
|
}
|
|
|
|
|
2018-07-11 13:35:31 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Timers
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class TimerImplWin32 final : public Timer {
|
2018-07-11 13:35:31 +08:00
|
|
|
public:
|
|
|
|
static HWND WindowHandle() {
|
|
|
|
static HWND hTimerWnd;
|
|
|
|
if(hTimerWnd == NULL) {
|
|
|
|
sscheck(hTimerWnd = CreateWindowExW(0, L"Message", NULL, 0, 0, 0, 0, 0,
|
|
|
|
HWND_MESSAGE, NULL, NULL, NULL));
|
|
|
|
}
|
|
|
|
return hTimerWnd;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void CALLBACK TimerFunc(HWND hwnd, UINT msg, UINT_PTR event, DWORD time) {
|
|
|
|
sscheck(KillTimer(WindowHandle(), event));
|
|
|
|
|
|
|
|
TimerImplWin32 *timer = (TimerImplWin32*)event;
|
|
|
|
if(timer->onTimeout) {
|
|
|
|
timer->onTimeout();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Eliminate imperative redraws.
This commit removes Platform::Window::Redraw function, and rewrites
its uses to run on timer events. Most UI toolkits have obscure issues
with recursive event handling loops, and Emscripten is purely event-
driven and cannot handle imperative redraws at all.
As a part of this change, the Platform::Timer::WindUp function
is split into three to make the interpretation of its argument
less magical. The new functions are RunAfter (a regular timeout,
setTimeout in browser terms), RunAfterNextFrame (an animation
request, requestAnimationFrame in browser terms), and
RunAfterProcessingEvents (a request to run something after all
events for the current frame are processed, used for coalescing
expensive operations in face of input event queues).
This commit changes two uses of Redraw(): the AnimateOnto() and
ScreenStepDimGo() functions. The latter was actually broken in that
on small sketches, it would run very quickly and not animate
the dimension change at all; this has been fixed.
While we're at it, get rid of unused Platform::Window::NativePtr
function as well.
2018-07-19 07:11:49 +08:00
|
|
|
void RunAfter(unsigned milliseconds) override {
|
2018-07-11 13:35:31 +08:00
|
|
|
// FIXME(platform/gui): use SetCoalescableTimer when it's available (8+)
|
|
|
|
sscheck(SetTimer(WindowHandle(), (UINT_PTR)this,
|
|
|
|
milliseconds, &TimerImplWin32::TimerFunc));
|
|
|
|
}
|
|
|
|
|
|
|
|
~TimerImplWin32() {
|
|
|
|
// FIXME(platform/gui): there's a race condition here--WM_TIMER messages already
|
|
|
|
// posted to the queue are not removed, so this destructor is at most "best effort".
|
|
|
|
KillTimer(WindowHandle(), (UINT_PTR)this);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
TimerRef CreateTimer() {
|
2018-07-19 07:49:51 +08:00
|
|
|
return std::make_shared<TimerImplWin32>();
|
2018-07-11 13:35:31 +08:00
|
|
|
}
|
|
|
|
|
2018-07-11 18:48:38 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Menus
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
class MenuImplWin32;
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class MenuItemImplWin32 final : public MenuItem {
|
2018-07-11 18:48:38 +08:00
|
|
|
public:
|
|
|
|
std::shared_ptr<MenuImplWin32> menu;
|
|
|
|
|
|
|
|
HMENU Handle();
|
|
|
|
|
|
|
|
MENUITEMINFOW GetInfo(UINT mask) {
|
|
|
|
MENUITEMINFOW mii = {};
|
|
|
|
mii.cbSize = sizeof(mii);
|
|
|
|
mii.fMask = mask;
|
|
|
|
sscheck(GetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
|
|
|
return mii;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetAccelerator(KeyboardEvent accel) override {
|
|
|
|
MENUITEMINFOW mii = GetInfo(MIIM_TYPE);
|
|
|
|
|
|
|
|
std::wstring nameW(mii.cch, L'\0');
|
|
|
|
mii.dwTypeData = &nameW[0];
|
|
|
|
mii.cch++;
|
|
|
|
sscheck(GetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
|
|
|
|
|
|
|
std::string name = Narrow(nameW);
|
|
|
|
if(name.find('\t') != std::string::npos) {
|
|
|
|
name = name.substr(0, name.find('\t'));
|
|
|
|
}
|
|
|
|
name += '\t';
|
|
|
|
name += AcceleratorDescription(accel);
|
|
|
|
|
|
|
|
nameW = Widen(name);
|
|
|
|
mii.fMask = MIIM_STRING;
|
|
|
|
mii.dwTypeData = &nameW[0];
|
|
|
|
sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetIndicator(Indicator type) override {
|
|
|
|
MENUITEMINFOW mii = GetInfo(MIIM_FTYPE);
|
|
|
|
switch(type) {
|
|
|
|
case Indicator::NONE:
|
|
|
|
case Indicator::CHECK_MARK:
|
|
|
|
mii.fType &= ~MFT_RADIOCHECK;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Indicator::RADIO_MARK:
|
|
|
|
mii.fType |= MFT_RADIOCHECK;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetActive(bool active) override {
|
|
|
|
MENUITEMINFOW mii = GetInfo(MIIM_STATE);
|
|
|
|
if(active) {
|
|
|
|
mii.fState |= MFS_CHECKED;
|
|
|
|
} else {
|
|
|
|
mii.fState &= ~MFS_CHECKED;
|
|
|
|
}
|
|
|
|
sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetEnabled(bool enabled) override {
|
|
|
|
MENUITEMINFOW mii = GetInfo(MIIM_STATE);
|
|
|
|
if(enabled) {
|
|
|
|
mii.fState &= ~(MFS_DISABLED|MFS_GRAYED);
|
|
|
|
} else {
|
|
|
|
mii.fState |= MFS_DISABLED|MFS_GRAYED;
|
|
|
|
}
|
|
|
|
sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
int64_t contextMenuPopTime = 0;
|
2018-07-11 18:48:38 +08:00
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class MenuImplWin32 final : public Menu {
|
2018-07-11 18:48:38 +08:00
|
|
|
public:
|
|
|
|
HMENU hMenu;
|
|
|
|
|
|
|
|
std::weak_ptr<MenuImplWin32> weakThis;
|
|
|
|
std::vector<std::shared_ptr<MenuItemImplWin32>> menuItems;
|
|
|
|
std::vector<std::shared_ptr<MenuImplWin32>> subMenus;
|
|
|
|
|
|
|
|
MenuImplWin32() {
|
|
|
|
sscheck(hMenu = CreatePopupMenu());
|
|
|
|
}
|
|
|
|
|
|
|
|
MenuItemRef AddItem(const std::string &label,
|
2019-04-13 19:00:35 +08:00
|
|
|
std::function<void()> onTrigger = NULL,
|
|
|
|
bool mnemonics = true) override {
|
2018-07-11 18:48:38 +08:00
|
|
|
auto menuItem = std::make_shared<MenuItemImplWin32>();
|
|
|
|
menuItem->menu = weakThis.lock();
|
|
|
|
menuItem->onTrigger = onTrigger;
|
|
|
|
menuItems.push_back(menuItem);
|
|
|
|
|
2019-04-13 19:00:35 +08:00
|
|
|
sscheck(AppendMenuW(hMenu, MF_STRING, (UINT_PTR)menuItem.get(),
|
|
|
|
Widen(mnemonics ? label : NegateMnemonics(label)).c_str()));
|
2018-07-13 03:29:44 +08:00
|
|
|
|
|
|
|
// uID is just an UINT, which isn't large enough to hold a pointer on 64-bit Windows,
|
|
|
|
// so we use dwItemData, which is.
|
|
|
|
MENUITEMINFOW mii = {};
|
|
|
|
mii.cbSize = sizeof(mii);
|
|
|
|
mii.fMask = MIIM_DATA;
|
|
|
|
mii.dwItemData = (LONG_PTR)menuItem.get();
|
|
|
|
sscheck(SetMenuItemInfoW(hMenu, (UINT_PTR)menuItem.get(), FALSE, &mii));
|
2018-07-11 18:48:38 +08:00
|
|
|
|
|
|
|
return menuItem;
|
|
|
|
}
|
|
|
|
|
|
|
|
MenuRef AddSubMenu(const std::string &label) override {
|
|
|
|
auto subMenu = std::make_shared<MenuImplWin32>();
|
|
|
|
subMenu->weakThis = subMenu;
|
|
|
|
subMenus.push_back(subMenu);
|
|
|
|
|
|
|
|
sscheck(AppendMenuW(hMenu, MF_STRING|MF_POPUP,
|
|
|
|
(UINT_PTR)subMenu->hMenu, Widen(label).c_str()));
|
|
|
|
|
|
|
|
return subMenu;
|
|
|
|
}
|
|
|
|
|
|
|
|
void AddSeparator() override {
|
|
|
|
sscheck(AppendMenuW(hMenu, MF_SEPARATOR, 0, L""));
|
|
|
|
}
|
|
|
|
|
|
|
|
void PopUp() override {
|
2018-07-13 03:29:44 +08:00
|
|
|
MENUINFO mi = {};
|
|
|
|
mi.cbSize = sizeof(mi);
|
|
|
|
mi.fMask = MIM_APPLYTOSUBMENUS|MIM_STYLE;
|
|
|
|
mi.dwStyle = MNS_NOTIFYBYPOS;
|
|
|
|
sscheck(SetMenuInfo(hMenu, &mi));
|
|
|
|
|
2018-07-11 18:48:38 +08:00
|
|
|
POINT pt;
|
|
|
|
sscheck(GetCursorPos(&pt));
|
2018-07-13 03:29:44 +08:00
|
|
|
|
|
|
|
sscheck(TrackPopupMenu(hMenu, TPM_TOPALIGN, pt.x, pt.y, 0, GetActiveWindow(), NULL));
|
|
|
|
contextMenuPopTime = GetMilliseconds();
|
2018-07-11 18:48:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void Clear() override {
|
|
|
|
for(int n = GetMenuItemCount(hMenu) - 1; n >= 0; n--) {
|
|
|
|
sscheck(RemoveMenu(hMenu, n, MF_BYPOSITION));
|
|
|
|
}
|
|
|
|
menuItems.clear();
|
|
|
|
subMenus.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
~MenuImplWin32() {
|
|
|
|
Clear();
|
|
|
|
sscheck(DestroyMenu(hMenu));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
HMENU MenuItemImplWin32::Handle() {
|
|
|
|
return menu->hMenu;
|
|
|
|
}
|
|
|
|
|
|
|
|
MenuRef CreateMenu() {
|
|
|
|
auto menu = std::make_shared<MenuImplWin32>();
|
|
|
|
// std::enable_shared_from_this fails for some reason, not sure why
|
|
|
|
menu->weakThis = menu;
|
|
|
|
return menu;
|
|
|
|
}
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class MenuBarImplWin32 final : public MenuBar {
|
2018-07-11 18:48:38 +08:00
|
|
|
public:
|
|
|
|
HMENU hMenuBar;
|
|
|
|
|
|
|
|
std::vector<std::shared_ptr<MenuImplWin32>> subMenus;
|
|
|
|
|
|
|
|
MenuBarImplWin32() {
|
|
|
|
sscheck(hMenuBar = ::CreateMenu());
|
|
|
|
}
|
|
|
|
|
|
|
|
MenuRef AddSubMenu(const std::string &label) override {
|
|
|
|
auto subMenu = std::make_shared<MenuImplWin32>();
|
|
|
|
subMenu->weakThis = subMenu;
|
|
|
|
subMenus.push_back(subMenu);
|
|
|
|
|
|
|
|
sscheck(AppendMenuW(hMenuBar, MF_STRING|MF_POPUP,
|
|
|
|
(UINT_PTR)subMenu->hMenu, Widen(label).c_str()));
|
|
|
|
|
|
|
|
return subMenu;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Clear() override {
|
|
|
|
for(int n = GetMenuItemCount(hMenuBar) - 1; n >= 0; n--) {
|
|
|
|
sscheck(RemoveMenu(hMenuBar, n, MF_BYPOSITION));
|
|
|
|
}
|
|
|
|
subMenus.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
~MenuBarImplWin32() {
|
|
|
|
Clear();
|
|
|
|
sscheck(DestroyMenu(hMenuBar));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|
|
|
*unique = false;
|
|
|
|
return std::make_shared<MenuBarImplWin32>();
|
|
|
|
}
|
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Windows
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
#define SCROLLBAR_UNIT 65536
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class WindowImplWin32 final : public Window {
|
2018-07-13 03:29:44 +08:00
|
|
|
public:
|
|
|
|
HWND hWindow = NULL;
|
|
|
|
HWND hTooltip = NULL;
|
|
|
|
HWND hEditor = NULL;
|
|
|
|
WNDPROC editorWndProc = NULL;
|
|
|
|
|
|
|
|
#if HAVE_OPENGL == 1
|
|
|
|
HGLRC hGlRc = NULL;
|
|
|
|
#elif HAVE_OPENGL == 3
|
2019-05-23 18:58:31 +08:00
|
|
|
static EGLDisplay eglDisplay;
|
|
|
|
EGLSurface eglSurface = EGL_NO_SURFACE;
|
|
|
|
EGLContext eglContext = EGL_NO_CONTEXT;
|
2018-07-13 03:29:44 +08:00
|
|
|
#endif
|
|
|
|
|
|
|
|
WINDOWPLACEMENT placement = {};
|
|
|
|
int minWidth = 0, minHeight = 0;
|
|
|
|
|
2018-07-18 08:48:49 +08:00
|
|
|
#if defined(HAVE_SPACEWARE)
|
|
|
|
SiOpenData sod = {};
|
|
|
|
SiHdl hSpaceWare = SI_NO_HANDLE;
|
|
|
|
#endif
|
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
std::shared_ptr<MenuBarImplWin32> menuBar;
|
|
|
|
std::string tooltipText;
|
|
|
|
bool scrollbarVisible = false;
|
|
|
|
|
|
|
|
static void RegisterWindowClass() {
|
|
|
|
static bool registered;
|
|
|
|
if(registered) return;
|
|
|
|
|
|
|
|
WNDCLASSEX wc = {};
|
|
|
|
wc.cbSize = sizeof(wc);
|
|
|
|
wc.style = CS_BYTEALIGNCLIENT|CS_BYTEALIGNWINDOW|CS_OWNDC|CS_DBLCLKS;
|
|
|
|
wc.lpfnWndProc = WndProc;
|
|
|
|
wc.cbWndExtra = sizeof(WindowImplWin32 *);
|
|
|
|
wc.hIcon = (HICON)LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(4000),
|
|
|
|
IMAGE_ICON, 32, 32, 0);
|
|
|
|
wc.hIconSm = (HICON)LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(4000),
|
|
|
|
IMAGE_ICON, 16, 16, 0);
|
|
|
|
wc.hCursor = LoadCursorW(NULL, IDC_ARROW);
|
|
|
|
wc.lpszClassName = L"SolveSpace";
|
|
|
|
sscheck(RegisterClassEx(&wc));
|
|
|
|
registered = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
WindowImplWin32(Window::Kind kind, std::shared_ptr<WindowImplWin32> parentWindow) {
|
|
|
|
placement.length = sizeof(placement);
|
|
|
|
|
|
|
|
RegisterWindowClass();
|
|
|
|
|
|
|
|
HWND hParentWindow = NULL;
|
|
|
|
if(parentWindow) {
|
|
|
|
hParentWindow = parentWindow->hWindow;
|
|
|
|
}
|
|
|
|
|
|
|
|
DWORD style = WS_SIZEBOX|WS_CLIPCHILDREN;
|
|
|
|
switch(kind) {
|
|
|
|
case Window::Kind::TOPLEVEL:
|
|
|
|
style |= WS_OVERLAPPEDWINDOW|WS_CLIPSIBLINGS;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Window::Kind::TOOL:
|
|
|
|
style |= WS_POPUPWINDOW|WS_CAPTION;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
sscheck(hWindow = CreateWindowExW(0, L"SolveSpace", L"", style,
|
|
|
|
CW_USEDEFAULT, CW_USEDEFAULT,
|
|
|
|
CW_USEDEFAULT, CW_USEDEFAULT,
|
|
|
|
hParentWindow, NULL, NULL, NULL));
|
|
|
|
sscheck(SetWindowLongPtr(hWindow, 0, (LONG_PTR)this));
|
|
|
|
|
|
|
|
sscheck(hTooltip = CreateWindowExW(0, TOOLTIPS_CLASS, NULL,
|
|
|
|
WS_POPUP|TTS_NOPREFIX|TTS_ALWAYSTIP,
|
|
|
|
CW_USEDEFAULT, CW_USEDEFAULT,
|
|
|
|
CW_USEDEFAULT, CW_USEDEFAULT,
|
|
|
|
hWindow, NULL, NULL, NULL));
|
|
|
|
sscheck(SetWindowPos(hTooltip, HWND_TOPMOST, 0, 0, 0, 0,
|
|
|
|
SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE));
|
|
|
|
|
|
|
|
TOOLINFOW ti = {};
|
|
|
|
ti.cbSize = sizeof(ti);
|
2019-05-23 21:53:16 +08:00
|
|
|
ti.uFlags = TTF_SUBCLASS;
|
2018-07-13 03:29:44 +08:00
|
|
|
ti.hwnd = hWindow;
|
|
|
|
ti.lpszText = (LPWSTR)L"";
|
|
|
|
sscheck(SendMessageW(hTooltip, TTM_ADDTOOLW, 0, (LPARAM)&ti));
|
|
|
|
sscheck(SendMessageW(hTooltip, TTM_ACTIVATE, FALSE, 0));
|
|
|
|
|
|
|
|
DWORD editorStyle = WS_CLIPSIBLINGS|WS_CHILD|WS_TABSTOP|ES_AUTOHSCROLL;
|
|
|
|
sscheck(hEditor = CreateWindowExW(WS_EX_CLIENTEDGE, WC_EDIT, L"", editorStyle,
|
|
|
|
0, 0, 0, 0, hWindow, NULL, NULL, NULL));
|
|
|
|
sscheck(editorWndProc =
|
|
|
|
(WNDPROC)SetWindowLongPtr(hEditor, GWLP_WNDPROC, (LONG_PTR)EditorWndProc));
|
|
|
|
|
|
|
|
HDC hDc;
|
|
|
|
sscheck(hDc = GetDC(hWindow));
|
|
|
|
|
|
|
|
#if HAVE_OPENGL == 1
|
|
|
|
PIXELFORMATDESCRIPTOR pfd = {};
|
|
|
|
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
|
|
|
|
pfd.nVersion = 1;
|
|
|
|
pfd.dwFlags = PFD_DRAW_TO_WINDOW|PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER;
|
|
|
|
pfd.dwLayerMask = PFD_MAIN_PLANE;
|
|
|
|
pfd.iPixelType = PFD_TYPE_RGBA;
|
|
|
|
pfd.cColorBits = 32;
|
|
|
|
pfd.cDepthBits = 24;
|
|
|
|
pfd.cAccumBits = 0;
|
|
|
|
pfd.cStencilBits = 0;
|
|
|
|
int pixelFormat;
|
|
|
|
sscheck(pixelFormat = ChoosePixelFormat(hDc, &pfd));
|
|
|
|
sscheck(SetPixelFormat(hDc, pixelFormat, &pfd));
|
|
|
|
|
|
|
|
sscheck(hGlRc = wglCreateContext(hDc));
|
|
|
|
#elif HAVE_OPENGL == 3
|
2019-05-23 18:58:31 +08:00
|
|
|
if(eglDisplay == EGL_NO_DISPLAY) {
|
|
|
|
ssassert(eglBindAPI(EGL_OPENGL_ES_API), "Cannot bind EGL API");
|
|
|
|
|
|
|
|
EGLBoolean initialized = EGL_FALSE;
|
|
|
|
for(auto &platformType : {
|
|
|
|
// Try platform types from least to most amount of software translation required.
|
|
|
|
std::make_pair("OpenGL ES", EGL_PLATFORM_ANGLE_TYPE_OPENGLES_ANGLE),
|
|
|
|
std::make_pair("OpenGL", EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE),
|
|
|
|
std::make_pair("Direct3D 11", EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE),
|
|
|
|
std::make_pair("Direct3D 9", EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE),
|
|
|
|
}) {
|
|
|
|
dbp("Initializing ANGLE with %s backend", platformType.first);
|
|
|
|
EGLint displayAttributes[] = {
|
|
|
|
EGL_PLATFORM_ANGLE_TYPE_ANGLE, platformType.second,
|
|
|
|
EGL_NONE
|
|
|
|
};
|
|
|
|
eglDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, hDc,
|
|
|
|
displayAttributes);
|
|
|
|
if(eglDisplay != EGL_NO_DISPLAY) {
|
|
|
|
initialized = eglInitialize(eglDisplay, NULL, NULL);
|
|
|
|
if(initialized) break;
|
|
|
|
eglTerminate(eglDisplay);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ssassert(initialized, "Cannot find a suitable EGL display");
|
|
|
|
}
|
2018-07-13 03:29:44 +08:00
|
|
|
|
|
|
|
EGLint configAttributes[] = {
|
|
|
|
EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER,
|
|
|
|
EGL_RED_SIZE, 8,
|
|
|
|
EGL_GREEN_SIZE, 8,
|
|
|
|
EGL_BLUE_SIZE, 8,
|
|
|
|
EGL_DEPTH_SIZE, 24,
|
|
|
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
|
|
|
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
|
|
|
|
EGL_NONE
|
|
|
|
};
|
|
|
|
EGLint numConfigs;
|
|
|
|
EGLConfig windowConfig;
|
|
|
|
ssassert(eglChooseConfig(eglDisplay, configAttributes, &windowConfig, 1, &numConfigs),
|
|
|
|
"Cannot choose EGL configuration");
|
|
|
|
|
|
|
|
EGLint surfaceAttributes[] = {
|
|
|
|
EGL_NONE
|
|
|
|
};
|
|
|
|
eglSurface = eglCreateWindowSurface(eglDisplay, windowConfig, hWindow, surfaceAttributes);
|
|
|
|
ssassert(eglSurface != EGL_NO_SURFACE, "Cannot create EGL window surface");
|
|
|
|
|
|
|
|
EGLint contextAttributes[] = {
|
|
|
|
EGL_CONTEXT_CLIENT_VERSION, 2,
|
|
|
|
EGL_NONE
|
|
|
|
};
|
|
|
|
eglContext = eglCreateContext(eglDisplay, windowConfig, NULL, contextAttributes);
|
|
|
|
ssassert(eglContext != EGL_NO_CONTEXT, "Cannot create EGL context");
|
|
|
|
#endif
|
|
|
|
|
|
|
|
sscheck(ReleaseDC(hWindow, hDc));
|
|
|
|
}
|
|
|
|
|
2018-07-18 08:48:49 +08:00
|
|
|
~WindowImplWin32() {
|
2019-05-13 17:02:54 +08:00
|
|
|
// Make sure any of our child windows get destroyed before we call DestroyWindow, or their
|
|
|
|
// own destructors may fail.
|
|
|
|
menuBar.reset();
|
|
|
|
|
2018-07-18 08:48:49 +08:00
|
|
|
sscheck(DestroyWindow(hWindow));
|
|
|
|
#if defined(HAVE_SPACEWARE)
|
|
|
|
if(hSpaceWare != SI_NO_HANDLE) {
|
|
|
|
SiClose(hSpaceWare);
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
static LRESULT CALLBACK WndProc(HWND h, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|
|
|
if(handlingFatalError) return TRUE;
|
|
|
|
|
|
|
|
WindowImplWin32 *window;
|
|
|
|
sscheck(window = (WindowImplWin32 *)GetWindowLongPtr(h, 0));
|
|
|
|
|
|
|
|
// The wndproc may be called from within CreateWindowEx, and before we've associated
|
|
|
|
// the window with the WindowImplWin32. In that case, just defer to the default wndproc.
|
|
|
|
if(window == NULL) {
|
|
|
|
return DefWindowProc(h, msg, wParam, lParam);
|
|
|
|
}
|
|
|
|
|
2018-07-18 08:48:49 +08:00
|
|
|
#if defined(HAVE_SPACEWARE)
|
|
|
|
if(window->hSpaceWare != SI_NO_HANDLE) {
|
|
|
|
SiGetEventData sged;
|
|
|
|
SiGetEventWinInit(&sged, msg, wParam, lParam);
|
|
|
|
|
|
|
|
SiSpwEvent sse;
|
|
|
|
if(SiGetEvent(window->hSpaceWare, 0, &sged, &sse) == SI_IS_EVENT) {
|
|
|
|
SixDofEvent event = {};
|
|
|
|
event.shiftDown = ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0);
|
|
|
|
event.controlDown = ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0);
|
|
|
|
if(sse.type == SI_MOTION_EVENT) {
|
|
|
|
// The Z axis translation and rotation are both
|
|
|
|
// backwards in the default mapping.
|
|
|
|
event.type = SixDofEvent::Type::MOTION;
|
|
|
|
event.translationX = sse.u.spwData.mData[SI_TX]*1.0,
|
|
|
|
event.translationY = sse.u.spwData.mData[SI_TY]*1.0,
|
|
|
|
event.translationZ = -sse.u.spwData.mData[SI_TZ]*1.0,
|
|
|
|
event.rotationX = sse.u.spwData.mData[SI_RX]*0.001,
|
|
|
|
event.rotationY = sse.u.spwData.mData[SI_RY]*0.001,
|
|
|
|
event.rotationZ = -sse.u.spwData.mData[SI_RZ]*0.001;
|
|
|
|
} else if(sse.type == SI_BUTTON_EVENT) {
|
|
|
|
if(SiButtonPressed(&sse) == SI_APP_FIT_BUTTON) {
|
|
|
|
event.type = SixDofEvent::Type::PRESS;
|
|
|
|
event.button = SixDofEvent::Button::FIT;
|
|
|
|
}
|
|
|
|
if(SiButtonReleased(&sse) == SI_APP_FIT_BUTTON) {
|
|
|
|
event.type = SixDofEvent::Type::RELEASE;
|
|
|
|
event.button = SixDofEvent::Button::FIT;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
switch (msg) {
|
|
|
|
case WM_ERASEBKGND:
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WM_PAINT: {
|
|
|
|
PAINTSTRUCT ps;
|
|
|
|
HDC hDc = BeginPaint(window->hWindow, &ps);
|
|
|
|
if(window->onRender) {
|
|
|
|
#if HAVE_OPENGL == 1
|
|
|
|
wglMakeCurrent(hDc, window->hGlRc);
|
|
|
|
#elif HAVE_OPENGL == 3
|
|
|
|
eglMakeCurrent(window->eglDisplay, window->eglSurface,
|
|
|
|
window->eglSurface, window->eglContext);
|
|
|
|
#endif
|
|
|
|
window->onRender();
|
|
|
|
#if HAVE_OPENGL == 1
|
|
|
|
SwapBuffers(hDc);
|
|
|
|
#elif HAVE_OPENGL == 3
|
|
|
|
eglSwapBuffers(window->eglDisplay, window->eglSurface);
|
|
|
|
(void)hDc;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
EndPaint(window->hWindow, &ps);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case WM_CLOSE:
|
|
|
|
if(window->onClose) {
|
|
|
|
window->onClose();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WM_SIZE:
|
|
|
|
window->Invalidate();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WM_SIZING: {
|
|
|
|
int pixelRatio = window->GetDevicePixelRatio();
|
|
|
|
|
|
|
|
RECT rcw, rcc;
|
|
|
|
sscheck(GetWindowRect(window->hWindow, &rcw));
|
|
|
|
sscheck(GetClientRect(window->hWindow, &rcc));
|
|
|
|
int nonClientWidth = (rcw.right - rcw.left) - (rcc.right - rcc.left);
|
|
|
|
int nonClientHeight = (rcw.bottom - rcw.top) - (rcc.bottom - rcc.top);
|
|
|
|
|
|
|
|
RECT *rc = (RECT *)lParam;
|
|
|
|
int adjWidth = rc->right - rc->left;
|
|
|
|
int adjHeight = rc->bottom - rc->top;
|
|
|
|
|
|
|
|
adjWidth -= nonClientWidth;
|
|
|
|
adjWidth = max(window->minWidth * pixelRatio, adjWidth);
|
|
|
|
adjWidth += nonClientWidth;
|
|
|
|
adjHeight -= nonClientHeight;
|
|
|
|
adjHeight = max(window->minHeight * pixelRatio, adjHeight);
|
|
|
|
adjHeight += nonClientHeight;
|
|
|
|
switch(wParam) {
|
|
|
|
case WMSZ_RIGHT:
|
|
|
|
case WMSZ_BOTTOMRIGHT:
|
|
|
|
case WMSZ_TOPRIGHT:
|
|
|
|
rc->right = rc->left + adjWidth;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WMSZ_LEFT:
|
|
|
|
case WMSZ_BOTTOMLEFT:
|
|
|
|
case WMSZ_TOPLEFT:
|
|
|
|
rc->left = rc->right - adjWidth;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
switch(wParam) {
|
|
|
|
case WMSZ_BOTTOM:
|
|
|
|
case WMSZ_BOTTOMLEFT:
|
|
|
|
case WMSZ_BOTTOMRIGHT:
|
|
|
|
rc->bottom = rc->top + adjHeight;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WMSZ_TOP:
|
|
|
|
case WMSZ_TOPLEFT:
|
|
|
|
case WMSZ_TOPRIGHT:
|
|
|
|
rc->top = rc->bottom - adjHeight;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case WM_DPICHANGED: {
|
|
|
|
RECT rc = *(RECT *)lParam;
|
|
|
|
sscheck(SendMessage(window->hWindow, WM_SIZING, WMSZ_BOTTOMRIGHT, (LPARAM)&rc));
|
|
|
|
sscheck(SetWindowPos(window->hWindow, NULL, rc.left, rc.top,
|
|
|
|
rc.right - rc.left, rc.bottom - rc.top,
|
|
|
|
SWP_NOZORDER|SWP_NOACTIVATE));
|
|
|
|
window->Invalidate();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case WM_LBUTTONDOWN:
|
|
|
|
case WM_MBUTTONDOWN:
|
|
|
|
case WM_RBUTTONDOWN:
|
|
|
|
case WM_LBUTTONDBLCLK:
|
|
|
|
case WM_MBUTTONDBLCLK:
|
|
|
|
case WM_RBUTTONDBLCLK:
|
|
|
|
case WM_LBUTTONUP:
|
|
|
|
case WM_MBUTTONUP:
|
|
|
|
case WM_RBUTTONUP:
|
|
|
|
if(GetMilliseconds() - Platform::contextMenuPopTime < 100) {
|
|
|
|
// Ignore the mouse click that dismisses a context menu, to avoid
|
|
|
|
// (e.g.) clearing a selection.
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
// fallthrough
|
|
|
|
case WM_MOUSEMOVE:
|
|
|
|
case WM_MOUSEWHEEL:
|
|
|
|
case WM_MOUSELEAVE: {
|
|
|
|
int pixelRatio = window->GetDevicePixelRatio();
|
|
|
|
|
|
|
|
MouseEvent event = {};
|
|
|
|
event.x = GET_X_LPARAM(lParam) / pixelRatio;
|
|
|
|
event.y = GET_Y_LPARAM(lParam) / pixelRatio;
|
|
|
|
event.button = MouseEvent::Button::NONE;
|
|
|
|
|
|
|
|
event.shiftDown = (wParam & MK_SHIFT) != 0;
|
|
|
|
event.controlDown = (wParam & MK_CONTROL) != 0;
|
|
|
|
|
2019-05-21 06:58:07 +08:00
|
|
|
bool consumed = false;
|
2018-07-13 03:29:44 +08:00
|
|
|
switch(msg) {
|
|
|
|
case WM_LBUTTONDOWN:
|
|
|
|
event.button = MouseEvent::Button::LEFT;
|
|
|
|
event.type = MouseEvent::Type::PRESS;
|
|
|
|
break;
|
|
|
|
case WM_MBUTTONDOWN:
|
|
|
|
event.button = MouseEvent::Button::MIDDLE;
|
|
|
|
event.type = MouseEvent::Type::PRESS;
|
|
|
|
break;
|
|
|
|
case WM_RBUTTONDOWN:
|
|
|
|
event.button = MouseEvent::Button::RIGHT;
|
|
|
|
event.type = MouseEvent::Type::PRESS;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WM_LBUTTONDBLCLK:
|
|
|
|
event.button = MouseEvent::Button::LEFT;
|
|
|
|
event.type = MouseEvent::Type::DBL_PRESS;
|
|
|
|
break;
|
|
|
|
case WM_MBUTTONDBLCLK:
|
|
|
|
event.button = MouseEvent::Button::MIDDLE;
|
|
|
|
event.type = MouseEvent::Type::DBL_PRESS;
|
|
|
|
break;
|
|
|
|
case WM_RBUTTONDBLCLK:
|
|
|
|
event.button = MouseEvent::Button::RIGHT;
|
|
|
|
event.type = MouseEvent::Type::DBL_PRESS;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WM_LBUTTONUP:
|
|
|
|
event.button = MouseEvent::Button::LEFT;
|
|
|
|
event.type = MouseEvent::Type::RELEASE;
|
|
|
|
break;
|
|
|
|
case WM_MBUTTONUP:
|
|
|
|
event.button = MouseEvent::Button::MIDDLE;
|
|
|
|
event.type = MouseEvent::Type::RELEASE;
|
|
|
|
break;
|
|
|
|
case WM_RBUTTONUP:
|
|
|
|
event.button = MouseEvent::Button::RIGHT;
|
|
|
|
event.type = MouseEvent::Type::RELEASE;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WM_MOUSEWHEEL:
|
|
|
|
// Make the mousewheel work according to which window the mouse is
|
|
|
|
// over, not according to which window is active.
|
|
|
|
POINT pt;
|
|
|
|
pt.x = LOWORD(lParam);
|
|
|
|
pt.y = HIWORD(lParam);
|
|
|
|
HWND hWindowUnderMouse;
|
|
|
|
sscheck(hWindowUnderMouse = WindowFromPoint(pt));
|
|
|
|
if(hWindowUnderMouse && hWindowUnderMouse != h) {
|
|
|
|
SendMessageW(hWindowUnderMouse, msg, wParam, lParam);
|
2019-05-21 06:58:07 +08:00
|
|
|
consumed = true;
|
2018-07-13 03:29:44 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
event.type = MouseEvent::Type::SCROLL_VERT;
|
|
|
|
event.scrollDelta = GET_WHEEL_DELTA_WPARAM(wParam) > 0 ? 1 : -1;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WM_MOUSELEAVE:
|
|
|
|
event.type = MouseEvent::Type::LEAVE;
|
|
|
|
break;
|
|
|
|
case WM_MOUSEMOVE: {
|
|
|
|
event.type = MouseEvent::Type::MOTION;
|
|
|
|
|
|
|
|
if(wParam & MK_LBUTTON) {
|
|
|
|
event.button = MouseEvent::Button::LEFT;
|
|
|
|
} else if(wParam & MK_MBUTTON) {
|
|
|
|
event.button = MouseEvent::Button::MIDDLE;
|
|
|
|
} else if(wParam & MK_RBUTTON) {
|
|
|
|
event.button = MouseEvent::Button::RIGHT;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We need this in order to get the WM_MOUSELEAVE
|
|
|
|
TRACKMOUSEEVENT tme = {};
|
|
|
|
tme.cbSize = sizeof(tme);
|
|
|
|
tme.dwFlags = TME_LEAVE;
|
|
|
|
tme.hwndTrack = window->hWindow;
|
|
|
|
sscheck(TrackMouseEvent(&tme));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-21 06:58:07 +08:00
|
|
|
if(!consumed && window->onMouseEvent) {
|
2018-07-13 03:29:44 +08:00
|
|
|
window->onMouseEvent(event);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case WM_KEYDOWN:
|
|
|
|
case WM_KEYUP: {
|
|
|
|
Platform::KeyboardEvent event = {};
|
|
|
|
if(msg == WM_KEYDOWN) {
|
|
|
|
event.type = Platform::KeyboardEvent::Type::PRESS;
|
|
|
|
} else if(msg == WM_KEYUP) {
|
|
|
|
event.type = Platform::KeyboardEvent::Type::RELEASE;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(GetKeyState(VK_SHIFT) & 0x8000)
|
|
|
|
event.shiftDown = true;
|
|
|
|
if(GetKeyState(VK_CONTROL) & 0x8000)
|
|
|
|
event.controlDown = true;
|
|
|
|
|
|
|
|
if(wParam >= VK_F1 && wParam <= VK_F12) {
|
|
|
|
event.key = Platform::KeyboardEvent::Key::FUNCTION;
|
|
|
|
event.num = wParam - VK_F1 + 1;
|
|
|
|
} else {
|
|
|
|
event.key = Platform::KeyboardEvent::Key::CHARACTER;
|
|
|
|
event.chr = tolower(MapVirtualKeyW(wParam, MAPVK_VK_TO_CHAR));
|
|
|
|
if(event.chr == 0) {
|
|
|
|
if(wParam == VK_DELETE) {
|
|
|
|
event.chr = '\x7f';
|
|
|
|
} else {
|
|
|
|
// Non-mappable key.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else if(event.chr == '.' && event.shiftDown) {
|
|
|
|
event.chr = '>';
|
|
|
|
event.shiftDown = false;;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(window->onKeyboardEvent) {
|
|
|
|
window->onKeyboardEvent(event);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case WM_SYSKEYDOWN: {
|
|
|
|
HWND hParent;
|
|
|
|
sscheck(hParent = GetParent(h));
|
|
|
|
if(hParent != NULL) {
|
|
|
|
// If the user presses the Alt key when a tool window has focus,
|
|
|
|
// then that should probably go to the main window instead.
|
|
|
|
sscheck(SetForegroundWindow(hParent));
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
return DefWindowProc(h, msg, wParam, lParam);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
case WM_VSCROLL: {
|
|
|
|
SCROLLINFO si = {};
|
|
|
|
si.cbSize = sizeof(si);
|
|
|
|
si.fMask = SIF_POS|SIF_TRACKPOS|SIF_RANGE|SIF_PAGE;
|
|
|
|
sscheck(GetScrollInfo(window->hWindow, SB_VERT, &si));
|
|
|
|
|
|
|
|
switch(LOWORD(wParam)) {
|
|
|
|
case SB_LINEUP: si.nPos -= SCROLLBAR_UNIT; break;
|
|
|
|
case SB_PAGEUP: si.nPos -= si.nPage; break;
|
|
|
|
case SB_LINEDOWN: si.nPos += SCROLLBAR_UNIT; break;
|
|
|
|
case SB_PAGEDOWN: si.nPos += si.nPage; break;
|
|
|
|
case SB_TOP: si.nPos = si.nMin; break;
|
|
|
|
case SB_BOTTOM: si.nPos = si.nMax; break;
|
|
|
|
case SB_THUMBTRACK:
|
|
|
|
case SB_THUMBPOSITION: si.nPos = si.nTrackPos; break;
|
|
|
|
}
|
|
|
|
|
|
|
|
si.nPos = min((UINT)si.nPos, (UINT)(si.nMax - si.nPage));
|
|
|
|
|
|
|
|
if(window->onScrollbarAdjusted) {
|
|
|
|
window->onScrollbarAdjusted((double)si.nPos / SCROLLBAR_UNIT);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case WM_MENUCOMMAND: {
|
|
|
|
MENUITEMINFOW mii = {};
|
|
|
|
mii.cbSize = sizeof(mii);
|
|
|
|
mii.fMask = MIIM_DATA;
|
|
|
|
sscheck(GetMenuItemInfoW((HMENU)lParam, wParam, TRUE, &mii));
|
|
|
|
|
|
|
|
MenuItemImplWin32 *menuItem = (MenuItemImplWin32 *)mii.dwItemData;
|
|
|
|
if(menuItem->onTrigger) {
|
|
|
|
menuItem->onTrigger();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
|
|
|
return DefWindowProc(h, msg, wParam, lParam);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static LRESULT CALLBACK EditorWndProc(HWND h, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|
|
|
if(handlingFatalError) return 0;
|
|
|
|
|
|
|
|
HWND hWindow;
|
|
|
|
sscheck(hWindow = GetParent(h));
|
|
|
|
|
|
|
|
WindowImplWin32 *window;
|
|
|
|
sscheck(window = (WindowImplWin32 *)GetWindowLongPtr(hWindow, 0));
|
|
|
|
|
|
|
|
switch(msg) {
|
|
|
|
case WM_KEYDOWN:
|
|
|
|
if(wParam == VK_RETURN) {
|
|
|
|
if(window->onEditingDone) {
|
|
|
|
int length;
|
|
|
|
sscheck(length = GetWindowTextLength(h));
|
|
|
|
|
|
|
|
std::wstring resultW;
|
|
|
|
resultW.resize(length);
|
|
|
|
sscheck(GetWindowTextW(h, &resultW[0], resultW.length() + 1));
|
|
|
|
|
|
|
|
window->onEditingDone(Narrow(resultW));
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
} else if(wParam == VK_ESCAPE) {
|
|
|
|
sscheck(SendMessageW(hWindow, msg, wParam, lParam));
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return CallWindowProc(window->editorWndProc, h, msg, wParam, lParam);
|
|
|
|
}
|
|
|
|
|
|
|
|
double GetPixelDensity() override {
|
|
|
|
UINT dpi;
|
|
|
|
sscheck(dpi = ssGetDpiForWindow(hWindow));
|
|
|
|
return (double)dpi;
|
|
|
|
}
|
|
|
|
|
|
|
|
int GetDevicePixelRatio() override {
|
|
|
|
UINT dpi;
|
|
|
|
sscheck(dpi = ssGetDpiForWindow(hWindow));
|
|
|
|
return dpi / USER_DEFAULT_SCREEN_DPI;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool IsVisible() override {
|
|
|
|
BOOL isVisible;
|
|
|
|
sscheck(isVisible = IsWindowVisible(hWindow));
|
|
|
|
return isVisible == TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetVisible(bool visible) override {
|
|
|
|
sscheck(ShowWindow(hWindow, visible ? SW_SHOW : SW_HIDE));
|
|
|
|
}
|
|
|
|
|
|
|
|
void Focus() override {
|
|
|
|
sscheck(SetActiveWindow(hWindow));
|
|
|
|
}
|
|
|
|
|
|
|
|
bool IsFullScreen() override {
|
|
|
|
DWORD style;
|
|
|
|
sscheck(style = GetWindowLongPtr(hWindow, GWL_STYLE));
|
|
|
|
return !(style & WS_OVERLAPPEDWINDOW);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetFullScreen(bool fullScreen) override {
|
|
|
|
DWORD style;
|
|
|
|
sscheck(style = GetWindowLongPtr(hWindow, GWL_STYLE));
|
|
|
|
if(fullScreen) {
|
|
|
|
sscheck(GetWindowPlacement(hWindow, &placement));
|
|
|
|
|
|
|
|
MONITORINFO mi;
|
|
|
|
mi.cbSize = sizeof(mi);
|
|
|
|
sscheck(GetMonitorInfo(MonitorFromWindow(hWindow, MONITOR_DEFAULTTONEAREST), &mi));
|
|
|
|
|
|
|
|
sscheck(SetWindowLong(hWindow, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW));
|
|
|
|
sscheck(SetWindowPos(hWindow, HWND_TOP,
|
|
|
|
mi.rcMonitor.left, mi.rcMonitor.top,
|
|
|
|
mi.rcMonitor.right - mi.rcMonitor.left,
|
|
|
|
mi.rcMonitor.bottom - mi.rcMonitor.top,
|
|
|
|
SWP_NOOWNERZORDER|SWP_FRAMECHANGED));
|
|
|
|
} else {
|
|
|
|
sscheck(SetWindowLong(hWindow, GWL_STYLE, style | WS_OVERLAPPEDWINDOW));
|
|
|
|
sscheck(SetWindowPlacement(hWindow, &placement));
|
|
|
|
sscheck(SetWindowPos(hWindow, NULL, 0, 0, 0, 0,
|
|
|
|
SWP_NOMOVE|SWP_NOSIZE|SWP_NOZORDER|
|
|
|
|
SWP_NOOWNERZORDER|SWP_FRAMECHANGED));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetTitle(const std::string &title) override {
|
2018-07-17 23:00:46 +08:00
|
|
|
sscheck(SetWindowTextW(hWindow, PrepareTitle(title).c_str()));
|
2018-07-13 03:29:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void SetMenuBar(MenuBarRef newMenuBar) override {
|
|
|
|
menuBar = std::static_pointer_cast<MenuBarImplWin32>(newMenuBar);
|
|
|
|
|
|
|
|
MENUINFO mi = {};
|
|
|
|
mi.cbSize = sizeof(mi);
|
|
|
|
mi.fMask = MIM_APPLYTOSUBMENUS|MIM_STYLE;
|
|
|
|
mi.dwStyle = MNS_NOTIFYBYPOS;
|
|
|
|
sscheck(SetMenuInfo(menuBar->hMenuBar, &mi));
|
|
|
|
|
|
|
|
sscheck(SetMenu(hWindow, menuBar->hMenuBar));
|
|
|
|
}
|
|
|
|
|
|
|
|
void GetContentSize(double *width, double *height) override {
|
|
|
|
int pixelRatio = GetDevicePixelRatio();
|
|
|
|
|
|
|
|
RECT rc;
|
|
|
|
sscheck(GetClientRect(hWindow, &rc));
|
|
|
|
*width = (rc.right - rc.left) / pixelRatio;
|
|
|
|
*height = (rc.bottom - rc.top) / pixelRatio;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetMinContentSize(double width, double height) {
|
|
|
|
minWidth = (int)width;
|
|
|
|
minHeight = (int)height;
|
|
|
|
|
|
|
|
int pixelRatio = GetDevicePixelRatio();
|
|
|
|
|
|
|
|
RECT rc;
|
|
|
|
sscheck(GetClientRect(hWindow, &rc));
|
|
|
|
if(rc.right - rc.left < minWidth * pixelRatio) {
|
|
|
|
rc.right = rc.left + minWidth * pixelRatio;
|
|
|
|
}
|
|
|
|
if(rc.bottom - rc.top < minHeight * pixelRatio) {
|
|
|
|
rc.bottom = rc.top + minHeight * pixelRatio;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-16 18:37:41 +08:00
|
|
|
void FreezePosition(SettingsRef settings, const std::string &key) override {
|
2018-07-13 03:29:44 +08:00
|
|
|
sscheck(GetWindowPlacement(hWindow, &placement));
|
|
|
|
|
|
|
|
BOOL isMaximized;
|
|
|
|
sscheck(isMaximized = IsZoomed(hWindow));
|
|
|
|
|
|
|
|
RECT rc = placement.rcNormalPosition;
|
2018-07-16 18:37:41 +08:00
|
|
|
settings->FreezeInt(key + "_Left", rc.left);
|
|
|
|
settings->FreezeInt(key + "_Right", rc.right);
|
|
|
|
settings->FreezeInt(key + "_Top", rc.top);
|
|
|
|
settings->FreezeInt(key + "_Bottom", rc.bottom);
|
2018-07-18 09:13:05 +08:00
|
|
|
settings->FreezeBool(key + "_Maximized", isMaximized == TRUE);
|
2018-07-13 03:29:44 +08:00
|
|
|
}
|
|
|
|
|
2018-07-16 18:37:41 +08:00
|
|
|
void ThawPosition(SettingsRef settings, const std::string &key) override {
|
2018-07-13 03:29:44 +08:00
|
|
|
sscheck(GetWindowPlacement(hWindow, &placement));
|
|
|
|
|
|
|
|
RECT rc = placement.rcNormalPosition;
|
2018-07-16 18:37:41 +08:00
|
|
|
rc.left = settings->ThawInt(key + "_Left", rc.left);
|
|
|
|
rc.right = settings->ThawInt(key + "_Right", rc.right);
|
|
|
|
rc.top = settings->ThawInt(key + "_Top", rc.top);
|
|
|
|
rc.bottom = settings->ThawInt(key + "_Bottom", rc.bottom);
|
2018-07-13 03:29:44 +08:00
|
|
|
|
|
|
|
MONITORINFO mi;
|
|
|
|
mi.cbSize = sizeof(mi);
|
|
|
|
sscheck(GetMonitorInfo(MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST), &mi));
|
|
|
|
|
|
|
|
// If it somehow ended up off-screen, then put it back.
|
|
|
|
RECT mrc = mi.rcMonitor;
|
|
|
|
rc.left = Clamp(rc.left, mrc.left, mrc.right);
|
|
|
|
rc.right = Clamp(rc.right, mrc.left, mrc.right);
|
|
|
|
rc.top = Clamp(rc.top, mrc.top, mrc.bottom);
|
|
|
|
rc.bottom = Clamp(rc.bottom, mrc.top, mrc.bottom);
|
|
|
|
|
|
|
|
// And make sure the minimum size is respected. (We can freeze a size smaller
|
|
|
|
// than minimum size if the DPI changed between runs.)
|
|
|
|
sscheck(SendMessageW(hWindow, WM_SIZING, WMSZ_BOTTOMRIGHT, (LPARAM)&rc));
|
|
|
|
|
|
|
|
placement.flags = 0;
|
2018-07-16 18:37:41 +08:00
|
|
|
if(settings->ThawBool(key + "_Maximized", false)) {
|
2018-07-13 03:29:44 +08:00
|
|
|
placement.showCmd = SW_SHOWMAXIMIZED;
|
|
|
|
} else {
|
|
|
|
placement.showCmd = SW_SHOW;
|
|
|
|
}
|
|
|
|
placement.rcNormalPosition = rc;
|
|
|
|
sscheck(SetWindowPlacement(hWindow, &placement));
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetCursor(Cursor cursor) override {
|
|
|
|
LPWSTR cursorName;
|
|
|
|
switch(cursor) {
|
|
|
|
case Cursor::POINTER: cursorName = IDC_ARROW; break;
|
|
|
|
case Cursor::HAND: cursorName = IDC_HAND; break;
|
|
|
|
}
|
|
|
|
|
|
|
|
HCURSOR hCursor;
|
|
|
|
sscheck(hCursor = LoadCursorW(NULL, cursorName));
|
|
|
|
sscheck(::SetCursor(hCursor));
|
|
|
|
}
|
|
|
|
|
2019-05-23 21:53:16 +08:00
|
|
|
void SetTooltip(const std::string &newText, double x, double y,
|
|
|
|
double width, double height) override {
|
|
|
|
if(newText == tooltipText) return;
|
|
|
|
tooltipText = newText;
|
|
|
|
|
|
|
|
if(!newText.empty()) {
|
|
|
|
int pixelRatio = GetDevicePixelRatio();
|
|
|
|
RECT toolRect;
|
|
|
|
toolRect.left = (int)(x * pixelRatio);
|
|
|
|
toolRect.top = (int)(y * pixelRatio);
|
|
|
|
toolRect.right = toolRect.left + (int)(width * pixelRatio);
|
|
|
|
toolRect.bottom = toolRect.top + (int)(height * pixelRatio);
|
2018-07-13 03:29:44 +08:00
|
|
|
|
|
|
|
std::wstring newTextW = Widen(newText);
|
|
|
|
TOOLINFOW ti = {};
|
|
|
|
ti.cbSize = sizeof(ti);
|
|
|
|
ti.hwnd = hWindow;
|
2019-05-23 21:53:16 +08:00
|
|
|
ti.rect = toolRect;
|
2018-07-13 03:29:44 +08:00
|
|
|
ti.lpszText = &newTextW[0];
|
2019-05-23 21:53:16 +08:00
|
|
|
sscheck(SendMessageW(hTooltip, TTM_UPDATETIPTEXTW, 0, (LPARAM)&ti));
|
|
|
|
sscheck(SendMessageW(hTooltip, TTM_NEWTOOLRECTW, 0, (LPARAM)&ti));
|
2018-07-13 03:29:44 +08:00
|
|
|
}
|
2019-05-23 21:53:16 +08:00
|
|
|
// The following SendMessage call sometimes fails with ERROR_ACCESS_DENIED for
|
|
|
|
// no discernible reason, but only on wine.
|
|
|
|
SendMessageW(hTooltip, TTM_ACTIVATE, !newText.empty(), 0);
|
2018-07-13 03:29:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
bool IsEditorVisible() override {
|
|
|
|
BOOL visible;
|
|
|
|
sscheck(visible = IsWindowVisible(hEditor));
|
|
|
|
return visible == TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShowEditor(double x, double y, double fontHeight, double minWidth,
|
|
|
|
bool isMonospace, const std::string &text) override {
|
|
|
|
if(IsEditorVisible()) return;
|
|
|
|
|
|
|
|
int pixelRatio = GetDevicePixelRatio();
|
|
|
|
|
|
|
|
HFONT hFont = CreateFontW(-(LONG)fontHeight * GetDevicePixelRatio(), 0, 0, 0,
|
|
|
|
FW_REGULAR, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
|
|
|
DEFAULT_QUALITY, FF_DONTCARE, isMonospace ? L"Lucida Console" : L"Arial");
|
|
|
|
if(hFont == NULL) {
|
|
|
|
sscheck(hFont = (HFONT)GetStockObject(SYSTEM_FONT));
|
|
|
|
}
|
|
|
|
sscheck(SendMessageW(hEditor, WM_SETFONT, (WPARAM)hFont, FALSE));
|
|
|
|
sscheck(SendMessageW(hEditor, EM_SETMARGINS, EC_LEFTMARGIN|EC_RIGHTMARGIN, 0));
|
|
|
|
|
|
|
|
std::wstring textW = Widen(text);
|
|
|
|
|
|
|
|
HDC hDc;
|
|
|
|
TEXTMETRICW tm;
|
|
|
|
SIZE ts;
|
|
|
|
sscheck(hDc = GetDC(hEditor));
|
|
|
|
sscheck(SelectObject(hDc, hFont));
|
|
|
|
sscheck(GetTextMetricsW(hDc, &tm));
|
|
|
|
sscheck(GetTextExtentPoint32W(hDc, textW.c_str(), textW.length(), &ts));
|
|
|
|
sscheck(ReleaseDC(hEditor, hDc));
|
|
|
|
|
|
|
|
RECT rc;
|
|
|
|
rc.left = (LONG)x * pixelRatio;
|
|
|
|
rc.top = (LONG)y * pixelRatio - tm.tmAscent;
|
|
|
|
// Add one extra char width to avoid scrolling.
|
|
|
|
rc.right = (LONG)x * pixelRatio +
|
|
|
|
std::max((LONG)minWidth * pixelRatio, ts.cx + tm.tmAveCharWidth);
|
|
|
|
rc.bottom = (LONG)y * pixelRatio + tm.tmDescent;
|
|
|
|
sscheck(ssAdjustWindowRectExForDpi(&rc, 0, /*bMenu=*/FALSE, WS_EX_CLIENTEDGE,
|
|
|
|
ssGetDpiForWindow(hWindow)));
|
|
|
|
|
|
|
|
sscheck(MoveWindow(hEditor, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
|
|
|
|
/*bRepaint=*/true));
|
|
|
|
sscheck(ShowWindow(hEditor, SW_SHOW));
|
|
|
|
if(!textW.empty()) {
|
|
|
|
sscheck(SendMessageW(hEditor, WM_SETTEXT, 0, (LPARAM)textW.c_str()));
|
|
|
|
sscheck(SendMessageW(hEditor, EM_SETSEL, 0, textW.length()));
|
|
|
|
sscheck(SetFocus(hEditor));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void HideEditor() override {
|
|
|
|
if(!IsEditorVisible()) return;
|
|
|
|
|
|
|
|
sscheck(ShowWindow(hEditor, SW_HIDE));
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetScrollbarVisible(bool visible) override {
|
|
|
|
scrollbarVisible = visible;
|
|
|
|
sscheck(ShowScrollBar(hWindow, SB_VERT, visible));
|
|
|
|
}
|
|
|
|
|
|
|
|
void ConfigureScrollbar(double min, double max, double pageSize) override {
|
|
|
|
SCROLLINFO si = {};
|
|
|
|
si.cbSize = sizeof(si);
|
|
|
|
si.fMask = SIF_RANGE|SIF_PAGE;
|
|
|
|
si.nMin = (UINT)(min * SCROLLBAR_UNIT);
|
|
|
|
si.nMax = (UINT)(max * SCROLLBAR_UNIT);
|
|
|
|
si.nPage = (UINT)(pageSize * SCROLLBAR_UNIT);
|
|
|
|
sscheck(SetScrollInfo(hWindow, SB_VERT, &si, /*redraw=*/TRUE));
|
|
|
|
}
|
|
|
|
|
|
|
|
double GetScrollbarPosition() override {
|
|
|
|
if(!scrollbarVisible) return 0.0;
|
|
|
|
|
|
|
|
SCROLLINFO si = {};
|
|
|
|
si.cbSize = sizeof(si);
|
|
|
|
si.fMask = SIF_POS;
|
|
|
|
sscheck(GetScrollInfo(hWindow, SB_VERT, &si));
|
|
|
|
return (double)si.nPos / SCROLLBAR_UNIT;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetScrollbarPosition(double pos) override {
|
|
|
|
if(!scrollbarVisible) return;
|
|
|
|
|
|
|
|
SCROLLINFO si = {};
|
|
|
|
si.cbSize = sizeof(si);
|
|
|
|
si.fMask = SIF_POS;
|
|
|
|
si.nPos = (UINT)(pos * SCROLLBAR_UNIT);
|
|
|
|
sscheck(SetScrollInfo(hWindow, SB_VERT, &si, /*redraw=*/TRUE));
|
|
|
|
|
|
|
|
// Windows won't synthesize a WM_VSCROLL for us here.
|
|
|
|
if(onScrollbarAdjusted) {
|
|
|
|
onScrollbarAdjusted((double)si.nPos / SCROLLBAR_UNIT);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Invalidate() override {
|
|
|
|
sscheck(InvalidateRect(hWindow, NULL, /*bErase=*/FALSE));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-05-29 23:06:25 +08:00
|
|
|
#if HAVE_OPENGL == 3
|
2019-05-23 18:58:31 +08:00
|
|
|
EGLDisplay WindowImplWin32::eglDisplay = EGL_NO_DISPLAY;
|
2019-05-29 23:06:25 +08:00
|
|
|
#endif
|
2019-05-23 18:58:31 +08:00
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) {
|
|
|
|
return std::make_shared<WindowImplWin32>(kind,
|
|
|
|
std::static_pointer_cast<WindowImplWin32>(parentWindow));
|
|
|
|
}
|
|
|
|
|
2018-07-18 08:48:49 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// 3DConnexion support
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
#if defined(HAVE_SPACEWARE)
|
|
|
|
static HWND hSpaceWareDriverClass;
|
|
|
|
|
|
|
|
void Open3DConnexion() {
|
|
|
|
HWND hSpaceWareDriverClass = FindWindowW(L"SpaceWare Driver Class", NULL);
|
|
|
|
if(hSpaceWareDriverClass != NULL) {
|
|
|
|
SiInitialize();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Close3DConnexion() {
|
|
|
|
if(hSpaceWareDriverClass != NULL) {
|
|
|
|
SiTerminate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Request3DConnexionEventsForWindow(WindowRef window) {
|
|
|
|
std::shared_ptr<WindowImplWin32> windowImpl =
|
|
|
|
std::static_pointer_cast<WindowImplWin32>(window);
|
|
|
|
if(hSpaceWareDriverClass != NULL) {
|
|
|
|
SiOpenWinInit(&windowImpl->sod, windowImpl->hWindow);
|
|
|
|
windowImpl->hSpaceWare = SiOpen("SolveSpace", SI_ANY_DEVICE, SI_NO_MASK, SI_EVENT,
|
|
|
|
&windowImpl->sod);
|
|
|
|
SiSetUiMode(windowImpl->hSpaceWare, SI_UI_NO_CONTROLS);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
void Open3DConnexion() {}
|
|
|
|
void Close3DConnexion() {}
|
|
|
|
void Request3DConnexionEventsForWindow(WindowRef window) {}
|
|
|
|
#endif
|
|
|
|
|
2018-07-17 23:00:46 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
2018-07-18 02:51:00 +08:00
|
|
|
// Message dialogs
|
2018-07-17 23:00:46 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class MessageDialogImplWin32 final : public MessageDialog {
|
2018-07-17 23:00:46 +08:00
|
|
|
public:
|
|
|
|
MSGBOXPARAMSW mbp = {};
|
|
|
|
|
|
|
|
int style;
|
|
|
|
|
|
|
|
std::wstring titleW;
|
|
|
|
std::wstring messageW;
|
|
|
|
std::wstring descriptionW;
|
|
|
|
std::wstring textW;
|
|
|
|
|
|
|
|
std::vector<int> buttons;
|
|
|
|
int defaultButton;
|
|
|
|
|
|
|
|
MessageDialogImplWin32() {
|
|
|
|
mbp.cbSize = sizeof(mbp);
|
|
|
|
SetTitle("Message");
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetType(Type type) override {
|
|
|
|
switch(type) {
|
|
|
|
case Type::INFORMATION:
|
|
|
|
style = MB_ICONINFORMATION;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Type::QUESTION:
|
|
|
|
style = MB_ICONQUESTION;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Type::WARNING:
|
|
|
|
style = MB_ICONWARNING;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Type::ERROR:
|
|
|
|
style = MB_ICONERROR;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetTitle(std::string title) override {
|
|
|
|
titleW = PrepareTitle(title);
|
|
|
|
mbp.lpszCaption = titleW.c_str();
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetMessage(std::string message) override {
|
|
|
|
messageW = Widen(message);
|
|
|
|
UpdateText();
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetDescription(std::string description) override {
|
|
|
|
descriptionW = Widen(description);
|
|
|
|
UpdateText();
|
|
|
|
}
|
|
|
|
|
|
|
|
void UpdateText() {
|
|
|
|
textW = messageW + L"\n\n" + descriptionW;
|
|
|
|
mbp.lpszText = textW.c_str();
|
|
|
|
}
|
|
|
|
|
2019-05-21 08:48:20 +08:00
|
|
|
void AddButton(std::string _label, Response response, bool isDefault) override {
|
2018-07-17 23:00:46 +08:00
|
|
|
int button;
|
|
|
|
switch(response) {
|
|
|
|
case Response::NONE: ssassert(false, "Invalid response");
|
|
|
|
case Response::OK: button = IDOK; break;
|
|
|
|
case Response::YES: button = IDYES; break;
|
|
|
|
case Response::NO: button = IDNO; break;
|
|
|
|
case Response::CANCEL: button = IDCANCEL; break;
|
|
|
|
}
|
|
|
|
buttons.push_back(button);
|
|
|
|
if(isDefault) {
|
|
|
|
defaultButton = button;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Response RunModal() override {
|
|
|
|
mbp.dwStyle = style;
|
|
|
|
|
|
|
|
std::sort(buttons.begin(), buttons.end());
|
|
|
|
if(buttons == std::vector<int>({ IDOK })) {
|
|
|
|
mbp.dwStyle |= MB_OK;
|
|
|
|
} else if(buttons == std::vector<int>({ IDOK, IDCANCEL })) {
|
|
|
|
mbp.dwStyle |= MB_OKCANCEL;
|
|
|
|
} else if(buttons == std::vector<int>({ IDYES, IDNO })) {
|
|
|
|
mbp.dwStyle |= MB_YESNO;
|
|
|
|
} else if(buttons == std::vector<int>({ IDCANCEL, IDYES, IDNO })) {
|
|
|
|
mbp.dwStyle |= MB_YESNOCANCEL;
|
|
|
|
} else {
|
|
|
|
ssassert(false, "Unexpected button set");
|
|
|
|
}
|
|
|
|
|
|
|
|
switch(MessageBoxIndirectW(&mbp)) {
|
|
|
|
case IDOK: return Response::OK; break;
|
|
|
|
case IDYES: return Response::YES; break;
|
|
|
|
case IDNO: return Response::NO; break;
|
|
|
|
case IDCANCEL: return Response::CANCEL; break;
|
|
|
|
default: ssassert(false, "Unexpected response");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
MessageDialogRef CreateMessageDialog(WindowRef parentWindow) {
|
|
|
|
std::shared_ptr<MessageDialogImplWin32> dialog = std::make_shared<MessageDialogImplWin32>();
|
|
|
|
dialog->mbp.hwndOwner = std::static_pointer_cast<WindowImplWin32>(parentWindow)->hWindow;
|
|
|
|
return dialog;
|
|
|
|
}
|
|
|
|
|
2018-07-18 02:51:00 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// File dialogs
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2018-07-19 08:11:04 +08:00
|
|
|
class FileDialogImplWin32 final : public FileDialog {
|
2018-07-18 02:51:00 +08:00
|
|
|
public:
|
|
|
|
OPENFILENAMEW ofn = {};
|
|
|
|
bool isSaveDialog;
|
|
|
|
std::wstring titleW;
|
|
|
|
std::wstring filtersW;
|
|
|
|
std::wstring defExtW;
|
|
|
|
std::wstring initialDirW;
|
|
|
|
// UNC paths may be as long as 32767 characters.
|
|
|
|
// Unfortunately, the Get*FileName API does not provide any way to use it
|
|
|
|
// except with a preallocated buffer of fixed size, so we use something
|
|
|
|
// reasonably large.
|
|
|
|
wchar_t filenameWC[32768] = {};
|
|
|
|
|
|
|
|
FileDialogImplWin32() {
|
|
|
|
ofn.lStructSize = sizeof(ofn);
|
|
|
|
ofn.lpstrFile = filenameWC;
|
|
|
|
ofn.nMaxFile = sizeof(filenameWC) / sizeof(wchar_t);
|
|
|
|
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY |
|
|
|
|
OFN_OVERWRITEPROMPT;
|
|
|
|
if(isSaveDialog) {
|
|
|
|
SetTitle(C_("title", "Save File"));
|
|
|
|
} else {
|
|
|
|
SetTitle(C_("title", "Open File"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetTitle(std::string title) override {
|
|
|
|
titleW = PrepareTitle(title);
|
|
|
|
ofn.lpstrTitle = titleW.c_str();
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetCurrentName(std::string name) override {
|
|
|
|
SetFilename(GetFilename().Parent().Join(name));
|
|
|
|
}
|
|
|
|
|
|
|
|
Platform::Path GetFilename() override {
|
|
|
|
return Path::From(Narrow(filenameWC));
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetFilename(Platform::Path path) override {
|
|
|
|
wcsncpy(filenameWC, Widen(path.raw).c_str(), sizeof(filenameWC) / sizeof(wchar_t) - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AddFilter(std::string name, std::vector<std::string> extensions) override {
|
|
|
|
std::string desc, patterns;
|
|
|
|
for(auto extension : extensions) {
|
|
|
|
std::string pattern = "*." + extension;
|
|
|
|
if(!desc.empty()) desc += ", ";
|
|
|
|
desc += pattern;
|
|
|
|
if(!patterns.empty()) patterns += ";";
|
|
|
|
patterns += pattern;
|
|
|
|
}
|
|
|
|
filtersW += Widen(name + " (" + desc + ")" + '\0' + patterns + '\0');
|
|
|
|
ofn.lpstrFilter = filtersW.c_str();
|
|
|
|
if(ofn.lpstrDefExt == NULL) {
|
|
|
|
defExtW = Widen(extensions.front());
|
|
|
|
ofn.lpstrDefExt = defExtW.c_str();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void FreezeChoices(SettingsRef settings, const std::string &key) override {
|
|
|
|
settings->FreezeString("Dialog_" + key + "_Folder", GetFilename().Parent().raw);
|
|
|
|
settings->FreezeInt("Dialog_" + key + "_Filter", ofn.nFilterIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ThawChoices(SettingsRef settings, const std::string &key) override {
|
|
|
|
initialDirW = Widen(settings->ThawString("Dialog_" + key + "_Folder", ""));
|
|
|
|
ofn.lpstrInitialDir = initialDirW.c_str();
|
|
|
|
ofn.nFilterIndex = settings->ThawInt("Dialog_" + key + "_Filter", 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool RunModal() override {
|
|
|
|
if(GetFilename().IsEmpty()) {
|
|
|
|
SetFilename(Path::From(_("untitled")));
|
|
|
|
}
|
|
|
|
|
|
|
|
if(isSaveDialog) {
|
2018-07-18 09:13:05 +08:00
|
|
|
return GetSaveFileNameW(&ofn) == TRUE;
|
2018-07-18 02:51:00 +08:00
|
|
|
} else {
|
2018-07-18 09:13:05 +08:00
|
|
|
return GetOpenFileNameW(&ofn) == TRUE;
|
2018-07-18 02:51:00 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) {
|
|
|
|
std::shared_ptr<FileDialogImplWin32> dialog = std::make_shared<FileDialogImplWin32>();
|
|
|
|
dialog->ofn.hwndOwner = std::static_pointer_cast<WindowImplWin32>(parentWindow)->hWindow;
|
|
|
|
dialog->isSaveDialog = false;
|
|
|
|
return dialog;
|
|
|
|
}
|
|
|
|
|
|
|
|
FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) {
|
|
|
|
std::shared_ptr<FileDialogImplWin32> dialog = std::make_shared<FileDialogImplWin32>();
|
|
|
|
dialog->ofn.hwndOwner = std::static_pointer_cast<WindowImplWin32>(parentWindow)->hWindow;
|
|
|
|
dialog->isSaveDialog = true;
|
|
|
|
return dialog;
|
|
|
|
}
|
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Application-wide APIs
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
2018-07-18 10:20:25 +08:00
|
|
|
std::vector<Platform::Path> GetFontFiles() {
|
|
|
|
std::vector<Platform::Path> fonts;
|
|
|
|
|
|
|
|
std::wstring fontsDirW(MAX_PATH, '\0');
|
|
|
|
fontsDirW.resize(GetWindowsDirectoryW(&fontsDirW[0], fontsDirW.length()));
|
|
|
|
fontsDirW += L"\\fonts\\";
|
|
|
|
Platform::Path fontsDir = Platform::Path::From(Narrow(fontsDirW));
|
|
|
|
|
|
|
|
WIN32_FIND_DATAW wfd;
|
|
|
|
HANDLE h = FindFirstFileW((fontsDirW + L"*").c_str(), &wfd);
|
|
|
|
while(h != INVALID_HANDLE_VALUE) {
|
|
|
|
fonts.push_back(fontsDir.Join(Narrow(wfd.cFileName)));
|
|
|
|
if(!FindNextFileW(h, &wfd)) break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return fonts;
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenInBrowser(const std::string &url) {
|
|
|
|
ShellExecuteW(NULL, L"open", Widen(url).c_str(), NULL, NULL, SW_SHOWNORMAL);
|
|
|
|
}
|
|
|
|
|
|
|
|
void InitGui(int argc, char **argv) {
|
|
|
|
INITCOMMONCONTROLSEX icc;
|
|
|
|
icc.dwSize = sizeof(icc);
|
|
|
|
icc.dwICC = ICC_STANDARD_CLASSES|ICC_BAR_CLASSES;
|
|
|
|
InitCommonControlsEx(&icc);
|
|
|
|
|
|
|
|
if(!SetLocale((uint16_t)GetUserDefaultLCID())) {
|
|
|
|
SetLocale("en_US");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void RunGui() {
|
|
|
|
MSG msg;
|
|
|
|
// The return value of the following functions doesn't indicate success or failure.
|
|
|
|
while(GetMessage(&msg, NULL, 0, 0)) {
|
|
|
|
TranslateMessage(&msg);
|
|
|
|
DispatchMessage(&msg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ExitGui() {
|
2018-07-13 03:29:44 +08:00
|
|
|
PostQuitMessage(0);
|
|
|
|
}
|
|
|
|
|
2018-07-11 13:35:31 +08:00
|
|
|
}
|
|
|
|
}
|