fix: resizable plugins dialog

This commit is contained in:
Ian Chua
2026-07-14 15:13:10 +08:00
parent 06cd8ad0d5
commit 3daa4336e3
5 changed files with 150 additions and 5 deletions

View File

@@ -80,6 +80,11 @@
<div id="pluginList" class="body thin-scroll"></div>
</section>
<!-- Drag to redistribute the dialog's height between the list and the details pane; the hit
area is the whole strip, the visible divider is the line drawn inside it. -->
<div id="paneSplitter" class="pane-splitter" role="separator" aria-orientation="horizontal"
aria-label="Resize the plugin list" title="Drag to resize; double-click to reset"></div>
<section class="pane details-pane">
<div class="detail-tabs" role="tablist" aria-label="Plugin details">
<button id="pluginInfoTab" class="detail-tab active" type="button" role="tab"

View File

@@ -31,6 +31,16 @@ let ctxMenu = null;
let exploreMenu = null;
let exploreMenuButton = null;
// Split pane. The ratio is the list's share of the content height, so it survives a dialog resize.
const SPLIT_STORAGE_KEY = "orca.plugins.split_ratio";
const SPLIT_DEFAULT_RATIO = 0.62;
const SPLIT_MIN_LIST_PX = 180;
const SPLIT_MIN_DETAILS_PX = 160;
let contentPane = null;
let paneSplitter = null;
let splitRatio = SPLIT_DEFAULT_RATIO;
function OnInit() {
pluginList = document.getElementById("pluginList");
ctxMenu = document.getElementById("ctxMenu");
@@ -64,6 +74,8 @@ function OnInit() {
pluginList?.addEventListener("contextmenu", OnPluginContextMenu);
ctxMenu?.addEventListener("click", OnContextMenuClick);
InitPaneSplitter();
document.getElementById("configSidebar")?.addEventListener("click", OnConfigSidebarClick);
document.getElementById("configSaveBtn")?.addEventListener("click", SaveCapabilityConfig);
document.getElementById("configRestoreBtn")?.addEventListener("click", RestoreCapabilityConfig);
@@ -97,6 +109,87 @@ function OnInit() {
RequestPlugins();
}
function InitPaneSplitter() {
contentPane = document.querySelector(".content");
paneSplitter = document.getElementById("paneSplitter");
if (!contentPane || !paneSplitter)
return;
ApplySplitRatio(ReadStoredSplitRatio());
paneSplitter.addEventListener("pointerdown", OnSplitterPointerDown);
paneSplitter.addEventListener("dblclick", () => {
ApplySplitRatio(SPLIT_DEFAULT_RATIO);
StoreSplitRatio(splitRatio);
});
// A resized dialog changes what the ratio is a ratio *of*, so re-clamp it against the new height
// rather than letting a pane fall below its minimum.
window.addEventListener("resize", () => ApplySplitRatio(splitRatio));
}
// Clamped so neither pane drops below its minimum. When the dialog is too short to honor both, the
// panes just split what there is.
function ApplySplitRatio(ratio) {
const available = contentPane.clientHeight;
const sash = paneSplitter.offsetHeight;
let next = Number.isFinite(ratio) ? ratio : SPLIT_DEFAULT_RATIO;
if (available > 0) {
const min = SPLIT_MIN_LIST_PX / available;
const max = (available - sash - SPLIT_MIN_DETAILS_PX) / available;
next = min > max ? 0.5 : Math.min(Math.max(next, min), max);
}
splitRatio = next;
contentPane.style.setProperty("--plugin-list-height", `${(next * 100).toFixed(2)}%`);
}
function OnSplitterPointerDown(event) {
if (event.button !== 0)
return;
const bounds = contentPane.getBoundingClientRect();
// Offset of the grab point inside the strip, so the splitter does not jump under the cursor.
const grabOffset = event.clientY - paneSplitter.getBoundingClientRect().top;
const onMove = (moveEvent) => {
ApplySplitRatio((moveEvent.clientY - bounds.top - grabOffset) / bounds.height);
};
const onUp = (upEvent) => {
paneSplitter.releasePointerCapture(upEvent.pointerId);
paneSplitter.removeEventListener("pointermove", onMove);
paneSplitter.removeEventListener("pointerup", onUp);
paneSplitter.classList.remove("dragging");
document.body.classList.remove("pane-resizing");
StoreSplitRatio(splitRatio);
};
// Captured, so the drag keeps tracking once the pointer leaves the strip (which it does at once).
paneSplitter.setPointerCapture(event.pointerId);
paneSplitter.addEventListener("pointermove", onMove);
paneSplitter.addEventListener("pointerup", onUp);
paneSplitter.classList.add("dragging");
document.body.classList.add("pane-resizing");
event.preventDefault();
}
function ReadStoredSplitRatio() {
try {
const stored = Number.parseFloat(window.localStorage.getItem(SPLIT_STORAGE_KEY));
return Number.isFinite(stored) ? stored : SPLIT_DEFAULT_RATIO;
} catch (err) {
return SPLIT_DEFAULT_RATIO; // storage can be unavailable in the webview; the default still works
}
}
function StoreSplitRatio(ratio) {
try {
window.localStorage.setItem(SPLIT_STORAGE_KEY, String(ratio));
} catch (err) {
// Persisting the position is a nicety, never a reason to break the drag.
}
}
function NormalizeInstallAction(action) {
const normalized = String(action || "");
return pluginInstallActions[normalized] ? normalized : "explore";

View File

@@ -151,11 +151,13 @@ body {
margin-right: auto;
}
/* Split pane: the list keeps --plugin-list-height of the dialog's content height (set by the
splitter drag, see InitPaneSplitter), the details pane takes what is left. Both panes shrink
before the layout overflows, so a short dialog degrades instead of clipping. */
.content {
min-height: 0;
display: grid;
grid-template-rows: minmax(220px, 1fr) 280px;
gap: 8px;
display: flex;
flex-direction: column;
}
.pane {
@@ -166,17 +168,60 @@ body {
display: grid;
}
/* The pane minimums live in JS (SPLIT_MIN_LIST_PX / SPLIT_MIN_DETAILS_PX), which clamps the ratio
against the current height: as CSS min-heights they could not both be honored in a very short
dialog and the panes would overflow instead of shrinking. */
.plugin-list-pane {
flex: 0 1 auto;
height: var(--plugin-list-height, 62%);
grid-template-rows: auto minmax(0, 1fr);
}
/* basis 0, so the details pane takes the space the list leaves instead of letting its own content
height push back and shrink the list below --plugin-list-height. */
.details-pane {
flex: 1 1 0;
grid-template-rows: auto minmax(0, 1fr);
padding: 12px;
box-sizing: border-box;
gap: 10px;
}
/* 9px of grab area for a 2px divider: the strip also supplies the gap between the panes. */
.pane-splitter {
flex: 0 0 auto;
position: relative;
height: 9px;
cursor: ns-resize;
touch-action: none;
}
.pane-splitter::after {
content: "";
position: absolute;
top: 50%;
right: 0;
left: 0;
height: 2px;
transform: translateY(-50%);
border-radius: 1px;
background: transparent;
transition: background-color 0.12s ease;
}
.pane-splitter:hover::after {
background: var(--border-strong);
}
.pane-splitter.dragging::after {
background: var(--row-selected-outline);
}
/* Keep the resize cursor for the whole drag, wherever the pointer travels. */
body.pane-resizing {
cursor: ns-resize;
}
.hdr {
font-weight: 600;
font-size: 12px;

View File

@@ -38,7 +38,7 @@ public:
const wxString& title = wxT(""),
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX);
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
~PluginsDialog();

View File

@@ -21,7 +21,9 @@ public:
const wxString& title = wxT(""),
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX);
// wxRESIZE_BORDER is required for a resizable frame on MSW/GTK; macOS derives
// one from wxMAXIMIZE_BOX alone, which is why these dialogs used to resize only there.
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
~WebViewHostDialog() override = default;
bool create_webview(const std::string& resource_path,