Release v0.2.1: support remote input, safe updates and high-DPI windows
This commit is contained in:
+173
-8
@@ -2,6 +2,7 @@
|
||||
#include "app.h"
|
||||
#include "ocr.h"
|
||||
#include "richtext.h"
|
||||
#include "ui_geometry.h"
|
||||
#include <algorithm>
|
||||
#include <commctrl.h>
|
||||
#include <commdlg.h>
|
||||
@@ -26,6 +27,129 @@ HFONT font(HWND w)
|
||||
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH,
|
||||
L"Segoe UI");
|
||||
}
|
||||
struct WindowLayout
|
||||
{
|
||||
int width{}, height{}, x{}, y{};
|
||||
bool scrolling{}, arranging{};
|
||||
int wheelRemainder{}, horizontalWheelRemainder{};
|
||||
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
|
||||
};
|
||||
WindowLayout *windowLayoutState(HWND w)
|
||||
{
|
||||
return reinterpret_cast<WindowLayout *>(GetPropW(w, L"WindowLayout"));
|
||||
}
|
||||
RECT workArea(RECT rect)
|
||||
{
|
||||
MONITORINFO monitor{sizeof(monitor)};
|
||||
GetMonitorInfoW(MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST), &monitor);
|
||||
return monitor.rcWork;
|
||||
}
|
||||
void fitToMonitor(HWND w, RECT rect)
|
||||
{
|
||||
rect = fitWindowRect(rect, workArea(rect));
|
||||
SetWindowPos(w, nullptr, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
|
||||
SWP_NOACTIVATE | SWP_NOZORDER);
|
||||
}
|
||||
void arrangeWindow(HWND w)
|
||||
{
|
||||
auto *layout = windowLayoutState(w);
|
||||
if (!layout || !layout->scrolling || layout->arranging) return;
|
||||
layout->arranging = true;
|
||||
// Showing either scrollbar can make the other necessary. Let the client size settle.
|
||||
for (int pass = 0; pass < 3; ++pass)
|
||||
{
|
||||
RECT client{};
|
||||
GetClientRect(w, &client);
|
||||
SCROLLINFO horizontal{sizeof(horizontal), SIF_RANGE | SIF_PAGE | SIF_POS,
|
||||
0, scale(w, layout->width) - 1, (UINT)client.right, layout->x};
|
||||
SCROLLINFO vertical{sizeof(vertical), SIF_RANGE | SIF_PAGE | SIF_POS,
|
||||
0, scale(w, layout->height) - 1, (UINT)client.bottom, layout->y};
|
||||
layout->x = SetScrollInfo(w, SB_HORZ, &horizontal, TRUE);
|
||||
layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE);
|
||||
}
|
||||
for (const auto &[child, r] : layout->controls)
|
||||
SetWindowPos(child, nullptr, scale(w, r.left) - layout->x, scale(w, r.top) - layout->y,
|
||||
scale(w, r.right - r.left), scale(w, r.bottom - r.top), SWP_NOACTIVATE | SWP_NOZORDER);
|
||||
layout->arranging = false;
|
||||
InvalidateRect(w, nullptr, TRUE);
|
||||
}
|
||||
void revealControl(HWND child)
|
||||
{
|
||||
HWND parent = GetParent(child);
|
||||
auto *layout = windowLayoutState(parent);
|
||||
if (!layout || !layout->scrolling) return;
|
||||
RECT r{}, client{};
|
||||
GetWindowRect(child, &r);
|
||||
MapWindowPoints(nullptr, parent, reinterpret_cast<POINT *>(&r), 2);
|
||||
GetClientRect(parent, &client);
|
||||
if (r.left < 0) layout->x += r.left;
|
||||
else if (r.right > client.right) layout->x += r.right - client.right;
|
||||
if (r.top < 0) layout->y += r.top;
|
||||
else if (r.bottom > client.bottom) layout->y += r.bottom - client.bottom;
|
||||
arrangeWindow(parent);
|
||||
}
|
||||
LRESULT CALLBACK scrollChildProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||||
{
|
||||
if (msg == WM_SETFOCUS) revealControl(w);
|
||||
if (msg == WM_NCDESTROY) RemoveWindowSubclass(w, scrollChildProc, 2);
|
||||
return DefSubclassProc(w, msg, wp, lp);
|
||||
}
|
||||
void resizeForDpi(HWND, WPARAM, LPARAM);
|
||||
LRESULT CALLBACK layoutProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||||
{
|
||||
auto *layout = windowLayoutState(w);
|
||||
if (!layout) return DefSubclassProc(w, msg, wp, lp);
|
||||
if (msg == WM_DPICHANGED)
|
||||
{
|
||||
resizeForDpi(w, wp, lp);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_SIZE) arrangeWindow(w);
|
||||
if (msg == WM_DISPLAYCHANGE || msg == WM_SETTINGCHANGE || (msg == WM_SHOWWINDOW && wp))
|
||||
{
|
||||
RECT rect{};
|
||||
GetWindowRect(w, &rect);
|
||||
fitToMonitor(w, rect);
|
||||
arrangeWindow(w);
|
||||
}
|
||||
if (layout->scrolling && (msg == WM_VSCROLL || msg == WM_HSCROLL))
|
||||
{
|
||||
bool vertical = msg == WM_VSCROLL;
|
||||
SCROLLINFO info{sizeof(info), SIF_ALL};
|
||||
GetScrollInfo(w, vertical ? SB_VERT : SB_HORZ, &info);
|
||||
int &position = vertical ? layout->y : layout->x;
|
||||
switch (LOWORD(wp))
|
||||
{
|
||||
case SB_LINEUP: position -= scale(w, 32); break;
|
||||
case SB_LINEDOWN: position += scale(w, 32); break;
|
||||
case SB_PAGEUP: position -= (int)info.nPage; break;
|
||||
case SB_PAGEDOWN: position += (int)info.nPage; break;
|
||||
case SB_TOP: position = 0; break;
|
||||
case SB_BOTTOM: position = info.nMax; break;
|
||||
case SB_THUMBTRACK: case SB_THUMBPOSITION: position = info.nTrackPos; break;
|
||||
}
|
||||
arrangeWindow(w);
|
||||
return 0;
|
||||
}
|
||||
if (layout->scrolling && (msg == WM_MOUSEWHEEL || msg == WM_MOUSEHWHEEL))
|
||||
{
|
||||
bool horizontal = msg == WM_MOUSEHWHEEL || (GET_KEYSTATE_WPARAM(wp) & MK_SHIFT);
|
||||
int &remainder = horizontal ? layout->horizontalWheelRemainder : layout->wheelRemainder;
|
||||
remainder += GET_WHEEL_DELTA_WPARAM(wp);
|
||||
int steps = remainder / WHEEL_DELTA;
|
||||
remainder %= WHEEL_DELTA;
|
||||
(horizontal ? layout->x : layout->y) += steps * scale(w, 48) * (msg == WM_MOUSEHWHEEL ? 1 : -1);
|
||||
arrangeWindow(w);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_NCDESTROY)
|
||||
{
|
||||
RemovePropW(w, L"WindowLayout");
|
||||
RemoveWindowSubclass(w, layoutProc, 2);
|
||||
delete layout;
|
||||
}
|
||||
return DefSubclassProc(w, msg, wp, lp);
|
||||
}
|
||||
HWND control(HWND parent, const wchar_t *cls, const wchar_t *title, DWORD style, int id, int x, int y,
|
||||
int width, int height)
|
||||
{
|
||||
@@ -34,6 +158,12 @@ HWND control(HWND parent, const wchar_t *cls, const wchar_t *title, DWORD style,
|
||||
scale(parent, width), scale(parent, height), parent, (HMENU)(INT_PTR)id,
|
||||
GetModuleHandleW(nullptr), nullptr);
|
||||
SendMessageW(w, WM_SETFONT, (WPARAM)GetPropW(parent, L"Font"), TRUE);
|
||||
if (auto *layout = windowLayoutState(parent); layout && layout->scrolling)
|
||||
{
|
||||
layout->controls.push_back({w, {x, y, x + width, y + height}});
|
||||
SetWindowSubclass(w, scrollChildProc, 2, 0);
|
||||
arrangeWindow(parent);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
void label(HWND w, const wchar_t *text, int x, int y, int width = 200)
|
||||
@@ -136,13 +266,31 @@ HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc
|
||||
RegisterClassW(&wc);
|
||||
UINT dpi = GetDpiForSystem();
|
||||
RECT rect{0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)};
|
||||
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | extra;
|
||||
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_THICKFRAME | WS_MAXIMIZEBOX | extra;
|
||||
AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi);
|
||||
POINT cursor{};
|
||||
GetCursorPos(&cursor);
|
||||
MONITORINFO monitor{sizeof(monitor)};
|
||||
GetMonitorInfoW(MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST), &monitor);
|
||||
HWND w =
|
||||
CreateWindowExW(WS_EX_APPWINDOW, cls, title, style, CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
CreateWindowExW(WS_EX_APPWINDOW, cls, title, style, monitor.rcWork.left, monitor.rcWork.top,
|
||||
rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, app.instance, &app);
|
||||
if (!w)
|
||||
throw std::runtime_error(utf8(lastError(L"Cannot create native window")));
|
||||
// The system DPI may still describe the old primary display. The HWND tells
|
||||
// us the actual destination DPI, and must size the frame AND its controls.
|
||||
dpi = GetDpiForWindow(w);
|
||||
rect = {0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)};
|
||||
AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi);
|
||||
int outerWidth = rect.right - rect.left, outerHeight = rect.bottom - rect.top;
|
||||
int left = monitor.rcWork.left + std::max(0L, (monitor.rcWork.right - monitor.rcWork.left - outerWidth) / 2);
|
||||
int top = monitor.rcWork.top + std::max(0L, (monitor.rcWork.bottom - monitor.rcWork.top - outerHeight) / 2);
|
||||
SetPropW(w, L"WindowLayout", new WindowLayout{width, height, 0, 0, !(extra & WS_THICKFRAME)});
|
||||
SetWindowSubclass(w, layoutProc, 2, 0);
|
||||
RECT initial = fitWindowRect({left, top, left + outerWidth, top + outerHeight}, monitor.rcWork);
|
||||
SetWindowPos(w, nullptr, initial.left, initial.top, initial.right - initial.left, initial.bottom - initial.top,
|
||||
SWP_NOACTIVATE | SWP_NOZORDER);
|
||||
arrangeWindow(w);
|
||||
SetWindowTextW(w, title);
|
||||
auto smallIcon = (HICON)LoadImageW(app.instance, MAKEINTRESOURCEW(IDI_KSWITCHER), IMAGE_ICON,
|
||||
GetSystemMetricsForDpi(SM_CXSMICON, dpi),
|
||||
@@ -154,6 +302,12 @@ HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc
|
||||
}
|
||||
void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
// Creation may send WM_DPICHANGED before controls and the original font exist.
|
||||
if (!GetPropW(w, L"Dpi"))
|
||||
{
|
||||
fitToMonitor(w, *(RECT *)lp);
|
||||
return;
|
||||
}
|
||||
int previous = (int)(UINT_PTR)GetPropW(w, L"Dpi");
|
||||
int next = LOWORD(wp);
|
||||
if (!previous)
|
||||
@@ -166,7 +320,8 @@ void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
|
||||
int previous, next;
|
||||
HFONT font;
|
||||
} state{w, previous, next, replacement};
|
||||
EnumChildWindows(
|
||||
auto *layout = windowLayoutState(w);
|
||||
if (!layout || !layout->scrolling) EnumChildWindows(
|
||||
w,
|
||||
[](HWND child, LPARAM data) -> BOOL
|
||||
{
|
||||
@@ -183,12 +338,19 @@ void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
|
||||
return TRUE;
|
||||
},
|
||||
(LPARAM)&state);
|
||||
else
|
||||
{
|
||||
layout->x = MulDiv(layout->x, next, previous);
|
||||
layout->y = MulDiv(layout->y, next, previous);
|
||||
for (auto &[child, rect] : layout->controls)
|
||||
SendMessageW(child, WM_SETFONT, (WPARAM)replacement, TRUE);
|
||||
}
|
||||
SetPropW(w, L"Font", replacement);
|
||||
SetPropW(w, L"Dpi", (HANDLE)(UINT_PTR)next);
|
||||
if (old)
|
||||
DeleteObject(old);
|
||||
auto *r = (RECT *)lp;
|
||||
SetWindowPos(w, nullptr, r->left, r->top, r->right - r->left, r->bottom - r->top, SWP_NOZORDER);
|
||||
fitToMonitor(w, *(RECT *)lp);
|
||||
arrangeWindow(w);
|
||||
}
|
||||
std::filesystem::path pickFile(HWND owner, bool save)
|
||||
{
|
||||
@@ -1447,7 +1609,7 @@ void showLogs(App &app)
|
||||
showRequestedWindow(w);
|
||||
activateWindow(w);
|
||||
}
|
||||
void showSettings(App &app)
|
||||
void showSettings(App &app, bool visible)
|
||||
{
|
||||
if (app.settingsWindow)
|
||||
{
|
||||
@@ -1505,8 +1667,11 @@ void showSettings(App &app)
|
||||
button(w, L"Save", IDOK, 548, 606, 100);
|
||||
button(w, L"Cancel", IDCANCEL, 660, 606, 100);
|
||||
fillSettings(w, (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||
showRequestedWindow(w);
|
||||
activateWindow(w);
|
||||
if (visible)
|
||||
{
|
||||
showRequestedWindow(w);
|
||||
activateWindow(w);
|
||||
}
|
||||
}
|
||||
void showSnippets(App &app)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user