Skip to content
42 changes: 26 additions & 16 deletions apps/shelldrive-host/agent.c
Original file line number Diff line number Diff line change
Expand Up @@ -1194,18 +1194,26 @@ static void wr_u64(sd_wr *w, uint64_t v)
}
}

/* prompt.request: the stored data object (without its closing brace) plus queued and the remaining
deadline in ms at send time (03 §2.5, 1.7.4). `out` must be initialized by the caller. */
/* The data object of prompt.request: the stored fields plus queued and the remaining deadline in ms at
send time (03 §2.5, 1.7.4). prompt.list emits the same object for every prompt it still holds
(03 §2.1 row 11), so a UI that (re)connects late can show the dialogs it was not sent. `out` must be
initialized. */
static void prompt_request_data(const host_prompt *r, int queued, uint64_t now, sd_wr *out)
{
uint64_t remain = r->deadline > now ? r->deadline - now : 0;
sd_wr_bytes(out, r->body, r->body_len);
wr_raw(out, queued ? ",\"queued\":true,\"deadline_ms\":" : ",\"queued\":false,\"deadline_ms\":");
wr_u64(out, remain);
wr_raw(out, "}");
}

/* prompt.request: the data object above as one event frame. `out` must be initialized by the caller. */
static sd_err prompt_request_event(const host_prompt *r, int queued, uint64_t now, sd_wr *out)
{
sd_wr d;
sd_err e;
uint64_t remain = r->deadline > now ? r->deadline - now : 0;
sd_wr_init(&d, r->body_len + 128);
sd_wr_bytes(&d, r->body, r->body_len);
wr_raw(&d, queued ? ",\"queued\":true,\"deadline_ms\":" : ",\"queued\":false,\"deadline_ms\":");
wr_u64(&d, remain);
wr_raw(&d, "}");
prompt_request_data(r, queued, now, &d);
e = sd_ipc_build_event("prompt.request", (const char *)d.p, d.len, out);
sd_wr_free(&d);
return e;
Expand Down Expand Up @@ -1578,6 +1586,7 @@ static void ipc_on_subscribe(void *ctx, sd_ipc_client *client, const char *topic
if (strcmp(topic, "prompt") != 0 || !(sd_ipc_client_caps(client) & SD_IPC_CAP_PROMPTS_UI))
return;
cid = sd_ipc_client_id(client);
SD_LOGI("agent", "client %llu subscribed to prompt", (unsigned long long)cid);
sd_mutex_lock(&g.lock);
for (;;) {
host_prompt *r = NULL, *o;
Expand Down Expand Up @@ -3044,7 +3053,8 @@ static sd_err handle_prompt_answer(sd_ipc_client *client, const sd_json *params,
return SD_OK;
}

/* prompt.list {}: the open and queued prompts (03 §2.1 row 11). */
/* prompt.list {}: the open and queued prompts, each with the fields of prompt.request (03 §2.1 row 11),
so a UI that (re)connects late shows the dialogs of prompts it was not sent. */
static sd_err handle_prompt_list(sd_json_writer *res)
{
host_prompt *r;
Expand All @@ -3055,14 +3065,13 @@ static sd_err handle_prompt_list(sd_json_writer *res)
for (r = g.prompts; r; r = r->next) {
if (r->expire)
continue;
sd_jw_obj_begin(res);
sd_jw_key(res, "prompt_id"); sd_jw_cstr(res, r->pid);
sd_jw_key(res, "mount_id"); sd_jw_cstr(res, r->mount_id);
sd_jw_key(res, "profile_id"); sd_jw_cstr(res, r->profile_id);
sd_jw_key(res, "kind"); sd_jw_cstr(res, r->kind);
sd_jw_key(res, "queued"); sd_jw_bool(res, r->queued);
sd_jw_key(res, "deadline_ms"); sd_jw_int(res, (int64_t)(r->deadline > now ? r->deadline - now : 0));
sd_jw_obj_end(res);
{
sd_wr d;
sd_wr_init(&d, r->body_len + 128);
prompt_request_data(r, r->queued, now, &d);
sd_jw_raw(res, (const char *)d.p, d.len);
sd_wr_free(&d);
}
}
sd_jw_arr_end(res);
sd_mutex_unlock(&g.lock);
Expand Down Expand Up @@ -3491,6 +3500,7 @@ static void ipc_on_disconnect(void *ctx, uint64_t client_id)
{
uint64_t *p = (uint64_t *)sd_alloc(sizeof(uint64_t));
SD_UNUSED(ctx);
SD_LOGI("agent", "client %llu disconnected", (unsigned long long)client_id);
if (p == NULL) return;
*p = client_id;
post(NULL, work_cancel_client, p);
Expand Down
8 changes: 7 additions & 1 deletion apps/shelldrive-shellext/shellext_com.c
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,13 @@ static void sx_l10n_ready(void)
{
unsigned spins;
if (InterlockedCompareExchange(&g_l10n_state, 1, 0) == 0) {
sd_l10n_init(&sd_l10n_catalog_shell, NULL);
/* Language resolution (03 §9.3): a shell extension has no command line, so SHELLDRIVE_LANG (a user
variable, inherited by Explorer) ranks before the user UI language, as in the CLI and the tray app. */
wchar_t lang[32];
if (GetEnvironmentVariableW(L"SHELLDRIVE_LANG", lang, SD_COUNTOF(lang)) - 1u < SD_COUNTOF(lang) - 1u)
sd_l10n_init(&sd_l10n_catalog_shell, lang);
else
sd_l10n_init(&sd_l10n_catalog_shell, NULL);
InterlockedExchange(&g_l10n_state, 2);
return;
}
Expand Down
21 changes: 14 additions & 7 deletions apps/shelldrive-ui/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -528,16 +528,23 @@ void ui_app_on_event(sd_json *ev)
sd_json_free(ev);
}

/* the list carries the same fields as prompt.request: a prompt this UI does not show becomes a dialog
(ui_prompt.c keeps the dialog of one that is already open, so none is shown twice) */
static void prompts_from_list(const sd_json *result)
{
const sd_json *arr = sd_json_get(result, "prompts");
size_t i, n = sd_json_count(arr);
for (i = 0; i < n; i++)
ui_prompt_on_request(sd_json_at(arr, i));
}

void ui_app_on_prompt_list(sd_json *result)
{
ui_prompt_on_list(result);
if (g_app.elevated_admin) {
/* the admin mode shows every open service prompt: the list carries the same fields as prompt.request */
const sd_json *arr = sd_json_get(result, "prompts");
size_t i, n = sd_json_count(arr);
for (i = 0; i < n; i++)
ui_prompt_on_request(sd_json_at(arr, i));
}
/* the agent's list (every mode) and the service's list (admin mode, ui_svc.c) both carry every prompt
their sender still holds: one that was showing when the connection dropped is shown again, with its
remaining deadline (03 §2.1 prompt.list) */
prompts_from_list(result);
sd_json_free(result);
}

Expand Down
165 changes: 161 additions & 4 deletions apps/shelldrive-ui/ui_editor.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
*
* Ad-hoc mount (UI_EDIT_ADHOC): the same pages without the name; the caller sends the connection to the agent
* (ui_mount_params_json). A drop-down list grows while it is open (combo_drop): the template height of a combo
* box is its closed height. */
* box is its closed height.
*
* The sheet is one size, as tall as its tallest page; on a small screen at high DPI (1080 px at 150 %) that
* would push the sheet's own OK and Cancel off the screen, so the page height is capped to the work area and
* a page taller than the cap scrolls (page_scroll_*). */
#include <windows.h>
#include <commctrl.h>
#include <commdlg.h>
Expand All @@ -27,9 +31,11 @@
#include "sd_l10n_ui_ids.h"
#include "sd_policy/sd_policy.h"
#include "ui_dlg.h"
#include "ui_dpi.h"
#include "ui_int.h"
#include "ui_measure.h"
#include "ui_profile_model.h"
#include "ui_win.h"

#define ED_WIDTH 300 /* dialog units; every page of the sheet has this width */
#define ED_PAGES 5
Expand All @@ -42,6 +48,7 @@
#define ED_ID_LEGACY 1503
#define ED_ID_WMNOTE 1504
#define ED_ERR_CHARS 1024 /* wide characters of the error text of one page */
#define ED_SHEET_CHROME 112 /* 96-DPI pixels of the sheet around the page: tab strip, buttons, slack */

enum { PG_GENERAL = 0, PG_AUTH, PG_CONN, PG_MOUNT, PG_SECURITY };

Expand All @@ -67,6 +74,8 @@ typedef struct ed_page {
ed_ctx *ed;
int index;
HWND hwnd; /* set by WM_INITDIALOG */
int scroll; /* px the content is scrolled up; 0 while the page fits its template */
int scroll_max; /* px of content hidden below the capped template height */
} ed_page;

struct ed_ctx {
Expand Down Expand Up @@ -540,6 +549,91 @@ static void browse_identity(ed_ctx *ed, HWND page)
SetWindowTextW(edit, path);
}

/* ---- scrolling a page that does not fit the capped sheet ---- */

/* The pages of one sheet all get the height of the tallest page (build_pages), capped to the work area. A
page whose content is taller keeps its controls below the fold: they are created and work, and are brought
into view with the page's scroll bar, the mouse wheel, Tab and the error box of a failed apply. The hidden
range is measured from the real controls, so no build-time geometry is carried into the page procedure. */

/* Greatest bottom of a child window, in client coordinates of page. */
static int page_content_bottom(HWND page)
{
HWND c = GetWindow(page, GW_CHILD);
int bottom = 0;
for (; c != NULL; c = GetWindow(c, GW_HWNDNEXT)) {
RECT rc;
POINT tl, br;
if (!GetWindowRect(c, &rc))
continue;
tl.x = rc.left; tl.y = rc.top;
br.x = rc.right; br.y = rc.bottom;
ScreenToClient(page, &tl);
ScreenToClient(page, &br);
if (br.y > bottom)
bottom = br.y;
}
return bottom;
}

/* Scrolls the page so pos is the new offset. ScrollWindowEx moves the children with the content; the closed
height of a combo box travels with it (combo_drop reads the box position back on every open). */
static void page_scroll_to(ed_page *pg, HWND page, int pos)
{
SCROLLINFO si;
int old = pg->scroll, dy;
if (pos < 0)
pos = 0;
if (pos > pg->scroll_max)
pos = pg->scroll_max;
pg->scroll = pos;
dy = pg->scroll - old;
if (dy != 0)
ScrollWindowEx(page, 0, dy, NULL, NULL, NULL, NULL, SW_SCROLLCHILDREN | SW_INVALIDATE);
memset(&si, 0, sizeof(si));
si.cbSize = sizeof(si);
si.fMask = SIF_POS;
si.nPos = pg->scroll;
SetScrollInfo(page, SB_VERT, &si, TRUE);
}

/* Brings ctl into view; a no-op while the page fits. */
static void page_scroll_visible(ed_page *pg, HWND page, HWND ctl)
{
RECT rc, cl;
POINT tl, br;
if (pg->scroll_max <= 0 || ctl == NULL || !GetWindowRect(ctl, &rc) || !GetClientRect(page, &cl))
return;
tl.x = rc.left; tl.y = rc.top;
br.x = rc.right; br.y = rc.bottom;
ScreenToClient(page, &tl);
ScreenToClient(page, &br);
if (tl.y < 0)
page_scroll_to(pg, page, pg->scroll + tl.y);
else if (br.y > cl.bottom)
page_scroll_to(pg, page, pg->scroll + br.y - cl.bottom);
}

/* Adds a vertical scroll bar when the content is taller than the page (the capped template height). */
static void page_scroll_setup(ed_page *pg, HWND page)
{
RECT cl;
SCROLLINFO si;
int bottom = page_content_bottom(page);
if (!GetClientRect(page, &cl) || bottom <= cl.bottom)
return;
pg->scroll_max = bottom - cl.bottom;
SetWindowLongPtrW(page, GWL_STYLE, GetWindowLongPtrW(page, GWL_STYLE) | WS_VSCROLL);
SetWindowPos(page, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
memset(&si, 0, sizeof(si));
si.cbSize = sizeof(si);
si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS;
si.nMin = 0;
si.nMax = pg->scroll_max + cl.bottom;
si.nPage = (UINT)cl.bottom;
SetScrollInfo(page, SB_VERT, &si, TRUE);
}

/* ---- the page procedure ---- */

static INT_PTR CALLBACK page_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
Expand All @@ -553,13 +647,21 @@ static INT_PTR CALLBACK page_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
SetWindowLongPtrW(hwnd, DWLP_USER, (LONG_PTR)pg);
pg->hwnd = hwnd;
write_page(pg->ed, pg->index, hwnd);
page_scroll_setup(pg, hwnd);
return TRUE;
}
case WM_NOTIFY: {
const NMHDR *nm = (const NMHDR *)lp;
ed_page *pg = (ed_page *)GetWindowLongPtrW(hwnd, DWLP_USER);
if (pg == NULL)
break;
if (nm->code == PSN_SETACTIVE) {
/* a page that failed the apply comes to the front; show its error box, which may sit below the fold */
HWND err = GetDlgItem(hwnd, ED_ID_ERR);
if (err != NULL && GetWindowTextLengthW(err) > 0)
page_scroll_visible(pg, hwnd, err);
break;
}
if (nm->code == PSN_KILLACTIVE) {
SetWindowLongPtrW(hwnd, DWLP_MSGRESULT, FALSE);
return TRUE;
Expand Down Expand Up @@ -587,12 +689,48 @@ static INT_PTR CALLBACK page_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
browse_identity(pg->ed, hwnd);
return TRUE;
}
if (code == EN_SETFOCUS || code == CBN_SETFOCUS) {
page_scroll_visible(pg, hwnd, (HWND)lp);
return TRUE;
}
if (code == CBN_DROPDOWN || code == CBN_CLOSEUP) {
combo_drop((HWND)lp, code == CBN_DROPDOWN);
return TRUE;
}
break;
}
case WM_VSCROLL: {
/* the scroll bar of a capped page (page_scroll_setup); a page that fits has none and never gets here */
ed_page *pg = (ed_page *)GetWindowLongPtrW(hwnd, DWLP_USER);
RECT cl;
int line, page_px;
if (pg == NULL || pg->scroll_max <= 0)
break;
line = GetSystemMetrics(SM_CYVSCROLL);
if (!GetClientRect(hwnd, &cl) || cl.bottom <= 0)
break;
page_px = cl.bottom;
switch (LOWORD(wp)) {
case SB_LINEUP: page_scroll_to(pg, hwnd, pg->scroll - line); break;
case SB_LINEDOWN: page_scroll_to(pg, hwnd, pg->scroll + line); break;
case SB_PAGEUP: page_scroll_to(pg, hwnd, pg->scroll - page_px); break;
case SB_PAGEDOWN: page_scroll_to(pg, hwnd, pg->scroll + page_px); break;
case SB_THUMBPOSITION:
case SB_THUMBTRACK: page_scroll_to(pg, hwnd, (short)HIWORD(wp)); break;
default: break;
}
return TRUE;
}
case WM_MOUSEWHEEL: {
/* DefWindowProc passes the wheel on to the parent when the focused child does not use it */
ed_page *pg = (ed_page *)GetWindowLongPtrW(hwnd, DWLP_USER);
RECT cl;
int delta = (short)HIWORD(wp);
if (pg == NULL || pg->scroll_max <= 0 || delta == 0 || !GetClientRect(hwnd, &cl) || cl.bottom <= 0)
break;
page_scroll_to(pg, hwnd, pg->scroll - delta / WHEEL_DELTA * 3 * GetSystemMetrics(SM_CYVSCROLL));
return TRUE;
}
default:
break;
}
Expand Down Expand Up @@ -702,14 +840,29 @@ static void add_page_security(ui_dlg *d)
add_errors(d);
}

/* Builds the five page templates. All pages get the height of the tallest one, so the sheet is one size. */
/* Height of a page template, in dialog units, beyond which the sheet's own buttons would leave the work area:
the work area minus the caption, the frame and the rest of the sheet (tab strip, buttons, slack). 0 when the
work area cannot be read; the pages are then left uncapped. */
static int page_height_cap(int dpi, const ui_measure *m)
{
RECT wa;
int chrome;
if (!SystemParametersInfoW(SPI_GETWORKAREA, 0, &wa, 0) || m->base_y <= 0)
return 0;
chrome = GetSystemMetrics(SM_CYCAPTION) + 2 * GetSystemMetrics(SM_CYFIXEDFRAME) +
ui_dpi_scale(ED_SHEET_CHROME, dpi);
return ((wa.bottom - wa.top) - chrome) * 8 / m->base_y;
}

/* Builds the five page templates. All pages get the height of the tallest one, so the sheet is one size; a
tallest page beyond the work area is capped and the pages that do not fit scroll (page_scroll_setup). */
static sd_err build_pages(ed_ctx *ed, unsigned char *blob[ED_PAGES], size_t len[ED_PAGES])
{
static const sd_msg_id titles[ED_PAGES] = { SD_MSG_UI_TAB_GENERAL, SD_MSG_UI_TAB_AUTH, SD_MSG_UI_TAB_CONN,
SD_MSG_UI_TAB_MOUNT, SD_MSG_UI_TAB_SECURITY };
wchar_t face[UI_DLG_FACE_MAX];
WORD points = 8;
int dpi = 96, tallest = 0, pg;
int dpi = 96, tallest = 0, cap, pg;
ui_measure m;
ui_dlg *d[ED_PAGES];
sd_err e = SD_OK;
Expand Down Expand Up @@ -740,6 +893,9 @@ static sd_err build_pages(ed_ctx *ed, unsigned char *blob[ED_PAGES], size_t len[
if (d[pg]->cursor + d[pg]->margin > tallest)
tallest = d[pg]->cursor + d[pg]->margin;
}
cap = page_height_cap(dpi, &m);
if (cap > 0 && tallest > cap)
tallest = cap; /* the hidden content of the capped pages scrolls at run time */
for (pg = 0; pg < ED_PAGES && e == SD_OK; pg++) {
d[pg]->min_height = tallest;
e = ui_dlg_build(d[pg], &blob[pg], &len[pg]);
Expand Down Expand Up @@ -823,7 +979,8 @@ int ui_editor_run(HWND owner, ui_edit_mode mode, sd_profile *p)
}
memset(&hdr, 0, sizeof(hdr));
hdr.dwSize = sizeof(hdr);
hdr.dwFlags = PSH_PROPSHEETPAGE | PSH_NOAPPLYNOW | PSH_NOCONTEXTHELP;
hdr.dwFlags = PSH_PROPSHEETPAGE | PSH_NOAPPLYNOW | PSH_NOCONTEXTHELP | PSH_USECALLBACK;
hdr.pfnCallback = ui_win_sheet_callback;
hdr.hwndParent = owner;
hdr.hInstance = g_app.inst;
hdr.pszCaption = sd_l10n_get(mode == UI_EDIT_NEW ? SD_MSG_UI_EDITOR_CAPTION_NEW
Expand Down
Loading