mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
On macOS, with an editable element focused and an input method active, WebKit can deliver a tap as pointerup then pointerdown, so drag trackers saw every later pointermove as a held button: one tap plus a pointer move started a phantom drag (file drag label, tab detach, tool window move) and the next tap dropped it. A drag now starts only from a move with the primary button down, a move without it cancels the press, and late button-less moves after a drag started are ignored so the drop is kept. Resizers, dialog drag/resize, pan and the review gutter end instead; the Project view also listens for pointercancel. Refs #760 Co-authored-by: ntbang0901 <121003585+ntbang0901@users.noreply.github.com>
124 lines
5.1 KiB
TypeScript
124 lines
5.1 KiB
TypeScript
import { useRef, useState } from "react";
|
|
import { executeAction } from "@/platform/actions";
|
|
import { useTx } from "@/platform/i18n";
|
|
import { reorderedTabs, useWindowGroup } from "@/platform/windowGroups";
|
|
import { Icon, IconButton, showContextMenu } from "@/ui";
|
|
import { notifications } from "./contributions";
|
|
import styles from "./WindowGroupTabs.module.css";
|
|
|
|
function run(work: Promise<unknown>) {
|
|
void work.catch((e) => notifications().notify({ level: "error", title: "Window group", body: String(e) }));
|
|
}
|
|
|
|
export function WindowGroupTabs() {
|
|
const tabs = useWindowGroup((state) => state.tabs);
|
|
const drag = useRef<{ label: string; x: number; moved: boolean } | null>(null);
|
|
const [dragging, setDragging] = useState<string | null>(null);
|
|
const tx = useTx();
|
|
if (tabs.length < 2) return null;
|
|
return (
|
|
<div className={styles.bar} data-window-group>
|
|
<div className={styles.tabs} role="tablist" aria-label={tx("Project windows")}>
|
|
{tabs.map((tab) => (
|
|
<div
|
|
className={`${styles.item} ${tab.current ? styles.selected : ""} ${dragging === tab.label ? styles.dragging : ""}`}
|
|
key={tab.label}
|
|
data-project-tab={tab.label}
|
|
onPointerDown={(e) => {
|
|
drag.current = null;
|
|
if (e.button === 0 && (e.target as Element).closest('[role="tab"]'))
|
|
drag.current = { label: tab.label, x: e.clientX, moved: false };
|
|
}}
|
|
onPointerMove={(e) => {
|
|
if (drag.current && !(e.buttons & 1)) drag.current = null; // reordered macOS tap, not a drag
|
|
if (drag.current && Math.abs(e.clientX - drag.current.x) > 5) {
|
|
drag.current.moved = true;
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
setDragging(tab.label);
|
|
}
|
|
}}
|
|
onPointerUp={(e) => {
|
|
const source = drag.current;
|
|
if (source?.moved) {
|
|
const target = document
|
|
.elementFromPoint(e.clientX, e.clientY)
|
|
?.closest<HTMLElement>("[data-project-tab]")?.dataset.projectTab;
|
|
if (target) run(executeAction("window.reorderTabs", reorderedTabs(tabs, source.label, target)));
|
|
}
|
|
setDragging(null);
|
|
// Keep the moved flag through the click following pointerup.
|
|
if (!source?.moved) drag.current = null;
|
|
}}
|
|
onPointerCancel={() => {
|
|
drag.current = null;
|
|
setDragging(null);
|
|
}}
|
|
onClickCapture={(e) => {
|
|
if (drag.current?.moved) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
drag.current = null;
|
|
}
|
|
}}
|
|
onContextMenu={(e) => {
|
|
e.preventDefault();
|
|
const index = tabs.indexOf(tab);
|
|
showContextMenu(e, [
|
|
{
|
|
label: "Move Tab to New Window",
|
|
disabled: !tab.current,
|
|
run: () => executeAction("window.detachTab"),
|
|
},
|
|
{ kind: "separator" },
|
|
{ label: "Close Tab", run: () => executeAction("window.closeTab", tab.label) },
|
|
{ label: "Close Other Tabs", run: () => executeAction("window.closeOtherTabs", tab.label) },
|
|
{
|
|
label: "Close Tabs to the Left",
|
|
disabled: index <= 0,
|
|
run: () => executeAction("window.closeTabsToLeft", tab.label),
|
|
},
|
|
{
|
|
label: "Close Tabs to the Right",
|
|
disabled: index >= tabs.length - 1,
|
|
run: () => executeAction("window.closeTabsToRight", tab.label),
|
|
},
|
|
]);
|
|
}}
|
|
>
|
|
<button
|
|
className={styles.tab}
|
|
role="tab"
|
|
aria-selected={tab.current}
|
|
tabIndex={tab.current ? 0 : -1}
|
|
title={tab.root ?? tab.name}
|
|
onClick={() => run(executeAction("window.selectTab", tab.label))}
|
|
onKeyDown={(e) => {
|
|
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return;
|
|
e.preventDefault();
|
|
const index = tabs.indexOf(tab);
|
|
const next =
|
|
e.key === "Home"
|
|
? 0
|
|
: e.key === "End"
|
|
? tabs.length - 1
|
|
: (index + (e.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
|
|
run(executeAction("window.selectTab", tabs[next]!.label));
|
|
}}
|
|
>
|
|
<Icon name="folder" size={14} />
|
|
<span>{tab.name}</span>
|
|
</button>
|
|
<IconButton
|
|
icon="close"
|
|
size="small"
|
|
title={tx`Close ${tab.name}`}
|
|
onClick={() => run(executeAction("window.closeTab", tab.label))}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<IconButton icon="external-link" title="Move Tab to New Window" action="window.detachTab" />
|
|
</div>
|
|
);
|
|
}
|