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-17 20:32:58 +08:00
|
|
|
#include <shellapi.h>
|
2018-07-13 03:29:44 +08:00
|
|
|
|
|
|
|
#ifndef WM_DPICHANGED
|
|
|
|
#define WM_DPICHANGED 0x02E0
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// We have our own CreateWindow.
|
|
|
|
#undef CreateWindow
|
|
|
|
|
|
|
|
#if HAVE_OPENGL == 3
|
|
|
|
#define EGLAPI /*static linkage*/
|
|
|
|
#include <EGL/egl.h>
|
|
|
|
#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
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
std::wstring Title(const std::string &s) {
|
|
|
|
return Widen("SolveSpace - " + s);
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
class SettingsImplWin32 : public Settings {
|
|
|
|
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
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
class TimerImplWin32 : public Timer {
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void WindUp(unsigned milliseconds) override {
|
|
|
|
// 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() {
|
|
|
|
return std::unique_ptr<TimerImplWin32>(new TimerImplWin32);
|
|
|
|
}
|
|
|
|
|
2018-07-11 18:48:38 +08:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Menus
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
class MenuImplWin32;
|
|
|
|
|
|
|
|
class MenuItemImplWin32 : public MenuItem {
|
|
|
|
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
|
|
|
|
|
|
|
class MenuImplWin32 : public Menu {
|
|
|
|
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,
|
|
|
|
std::function<void()> onTrigger = NULL) override {
|
|
|
|
auto menuItem = std::make_shared<MenuItemImplWin32>();
|
|
|
|
menuItem->menu = weakThis.lock();
|
|
|
|
menuItem->onTrigger = onTrigger;
|
|
|
|
menuItems.push_back(menuItem);
|
|
|
|
|
2018-07-13 03:29:44 +08:00
|
|
|
sscheck(AppendMenuW(hMenu, MF_STRING, (UINT_PTR)menuItem.get(), Widen(label).c_str()));
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
class MenuBarImplWin32 : public MenuBar {
|
|
|
|
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
|
|
|
|
|
|
|
|
class WindowImplWin32 : public Window {
|
|
|
|
public:
|
|
|
|
HWND hWindow = NULL;
|
|
|
|
HWND hTooltip = NULL;
|
|
|
|
HWND hEditor = NULL;
|
|
|
|
WNDPROC editorWndProc = NULL;
|
|
|
|
|
|
|
|
#if HAVE_OPENGL == 1
|
|
|
|
HGLRC hGlRc = NULL;
|
|
|
|
#elif HAVE_OPENGL == 3
|
|
|
|
EGLDisplay eglDisplay = NULL;
|
|
|
|
EGLSurface eglSurface = NULL;
|
|
|
|
EGLContext eglContext = NULL;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
WINDOWPLACEMENT placement = {};
|
|
|
|
int minWidth = 0, minHeight = 0;
|
|
|
|
|
|
|
|
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));
|
|
|
|
if(hParentWindow != NULL) {
|
|
|
|
sscheck(SetWindowPos(hWindow, HWND_TOPMOST, 0, 0, 0, 0,
|
|
|
|
SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE));
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
ti.uFlags = TTF_IDISHWND|TTF_SUBCLASS;
|
|
|
|
ti.hwnd = hWindow;
|
|
|
|
ti.uId = (UINT_PTR)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
|
|
|
|
ssassert(eglBindAPI(EGL_OPENGL_ES_API), "Cannot bind EGL API");
|
|
|
|
|
|
|
|
eglDisplay = eglGetDisplay(hDc);
|
|
|
|
ssassert(eglInitialize(eglDisplay, NULL, NULL), "Cannot initialize EGL");
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
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:
|
|
|
|
case WM_DESTROY:
|
|
|
|
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;
|
|
|
|
|
|
|
|
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);
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(window->onMouseEvent) {
|
|
|
|
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);
|
|
|
|
} else {
|
|
|
|
HWND hParent;
|
|
|
|
sscheck(hParent = GetParent(h));
|
|
|
|
if(hParent != NULL) {
|
|
|
|
sscheck(SetForegroundWindow(hParent));
|
|
|
|
sscheck(SendMessageW(hParent, msg, wParam, lParam));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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 {
|
|
|
|
sscheck(SetWindowTextW(hWindow, Title(title).c_str()));
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
settings->FreezeBool(key + "_Maximized", isMaximized);
|
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));
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetTooltip(const std::string &newText) override {
|
|
|
|
// The following SendMessage calls sometimes fail with ERROR_ACCESS_DENIED for
|
|
|
|
// no discernible reason, but only on wine.
|
|
|
|
if(newText.empty()) {
|
|
|
|
SendMessageW(hTooltip, TTM_ACTIVATE, FALSE, 0);
|
|
|
|
SendMessageW(hTooltip, TTM_POP, 0, 0);
|
|
|
|
} else if(newText != tooltipText) {
|
|
|
|
tooltipText = newText;
|
|
|
|
|
|
|
|
std::wstring newTextW = Widen(newText);
|
|
|
|
TOOLINFOW ti = {};
|
|
|
|
ti.cbSize = sizeof(ti);
|
|
|
|
ti.uFlags = TTF_IDISHWND;
|
|
|
|
ti.hwnd = hWindow;
|
|
|
|
ti.uId = (UINT_PTR)hWindow;
|
|
|
|
ti.lpszText = &newTextW[0];
|
|
|
|
SendMessageW(hTooltip, TTM_UPDATETIPTEXTW, 0, (LPARAM)&ti);
|
|
|
|
|
|
|
|
SendMessageW(hTooltip, TTM_ACTIVATE, TRUE, 0);
|
|
|
|
SendMessageW(hTooltip, TTM_POPUP, 0, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
|
|
|
void Redraw() override {
|
|
|
|
Invalidate();
|
|
|
|
sscheck(UpdateWindow(hWindow));
|
|
|
|
}
|
|
|
|
|
|
|
|
void *NativePtr() override {
|
|
|
|
return hWindow;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) {
|
|
|
|
return std::make_shared<WindowImplWin32>(kind,
|
|
|
|
std::static_pointer_cast<WindowImplWin32>(parentWindow));
|
|
|
|
}
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Application-wide APIs
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
void Exit() {
|
|
|
|
PostQuitMessage(0);
|
|
|
|
}
|
|
|
|
|
2018-07-11 13:35:31 +08:00
|
|
|
}
|
|
|
|
}
|