дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
Compare with Local, Compare Versions and the other revision comparisons open in a separate window that belongs to their project window. The window can be moved outside the main window, resized from its edges and corners, and shows the full diff. Edits made there and in the project window are mirrored both ways. Root cause of #798: the comparison was an in-window dialog with a fixed size. Settings and comparison windows now share one auxiliary-window mechanism (`AuxKind`, `open_auxiliary_window`). It handles labels, the title bar, placement over the parent, event forwarding, document-edit mirroring, menu routing, closing together with the parent, and remembered geometry per kind. Co-authored-by: xdpan1234 <[email protected]>
228 рядки
8.3 KiB
TypeScript
228 рядки
8.3 KiB
TypeScript
/**
|
||
* Settings: the Settings window (⌘,, action `file.settings`) with built-in
|
||
* pages (Appearance, System Settings, Keymap, Editor › General / Font / Code
|
||
* Style / Files) plus pages contributed via `registerSettingsPage`, and the
|
||
* runtime application of keymap / UI font size / high-contrast settings.
|
||
*
|
||
* On the desktop Settings open in a native child window (`activateSettingsWindow`
|
||
* runs there); in the browser build they open as an in-page dialog.
|
||
*
|
||
* `executeAction("file.settings", "keymap")` opens a specific page;
|
||
* `file.newProjectsSettings` (IDEA's Settings for New Projects) edits the
|
||
* global layer only, which new projects inherit.
|
||
*/
|
||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||
import { registerActions, type Action } from "@/platform/actions";
|
||
import { call, commands, isTauri } from "@/platform/ipc";
|
||
import { isPopupOpen, lazyComponent } from "@/ui";
|
||
import { loadSettings, useProject } from "@/platform/project";
|
||
import { logError } from "@/platform/errorLog";
|
||
import { restoreRemote } from "@/platform/remote";
|
||
import { provideSettingsSearchIndex, registerOverlay } from "@/workbench/contributions";
|
||
import { windowBackgroundHex } from "@/workbench/theme";
|
||
import { LazySettingsDialog, loadBuiltinPages, unloadBuiltinPages } from "./dialog/LazySettingsDialog";
|
||
import {
|
||
closeSettings,
|
||
openSettings,
|
||
selectPage,
|
||
setNewProjects,
|
||
setQuery,
|
||
settingsTitle,
|
||
useSettingsDialog,
|
||
} from "./model/staging";
|
||
import { zoomActions } from "./appearanceRuntime";
|
||
import { activateFileTypes } from "./fileTypesRuntime";
|
||
import { trackRuntimeSettings } from "./runtime";
|
||
import { installActionProxies, installSettingsHost } from "./window/bridge";
|
||
import { AssignShortcutOverlay, showAssignShortcut } from "./keymap/AssignShortcut";
|
||
import { settingsSearchEntries } from "./searchIndex";
|
||
import { activatePythonStatus } from "./languages/PythonStatus";
|
||
import { nativeToAsciiAction } from "./nativeToAscii";
|
||
import { isImeKey } from "@/platform/ime";
|
||
|
||
/** Settings-driven runtime state (zoom, fonts, theme) of a secondary window. */
|
||
export const activateRuntimeSettings = trackRuntimeSettings;
|
||
export const SettingsWindow = lazyComponent(() => import("./window/SettingsWindow").then((m) => m.SettingsWindow));
|
||
|
||
/** Same event as `windows::LAUNCH_EVENT`: a pending launch (page) is waiting. */
|
||
const LAUNCH_EVENT = "openidea://launch";
|
||
|
||
/** Java's page first: JDK, Maven and the language server are what new projects most often need. */
|
||
const NEW_PROJECTS_PAGE = "languages.java";
|
||
|
||
function settingsActions(open: (page?: string, newProjects?: boolean) => void): Action[] {
|
||
return [
|
||
nativeToAsciiAction(),
|
||
{
|
||
id: "file.settings",
|
||
title: "Settings…",
|
||
category: "File",
|
||
icon: "settings",
|
||
keywords: ["preferences", "options", "configure"],
|
||
run: ({ arg }) => open(typeof arg === "string" ? arg : undefined),
|
||
},
|
||
{
|
||
id: "file.newProjectsSettings",
|
||
title: "Settings for New Projects…",
|
||
category: "File",
|
||
icon: "settings",
|
||
keywords: ["new projects setup", "default settings", "default project", "default jdk", "global settings"],
|
||
run: ({ arg }) => open(typeof arg === "string" ? arg : NEW_PROJECTS_PAGE, true),
|
||
},
|
||
{
|
||
id: "settings.keymap",
|
||
title: "Keymap…",
|
||
category: "File",
|
||
icon: "keyboard",
|
||
keywords: ["shortcuts", "keyboard", "bindings"],
|
||
run: () => open("keymap"),
|
||
},
|
||
{
|
||
id: "settings.appearance",
|
||
title: "Appearance…",
|
||
category: "View",
|
||
icon: "sun",
|
||
keywords: ["theme", "accent", "font size"],
|
||
run: () => open("appearance"),
|
||
},
|
||
];
|
||
}
|
||
|
||
/** Native Settings window when the backend can make one, else the dialog. */
|
||
async function showSettings(page?: string, newProjects = false) {
|
||
if (isTauri()) {
|
||
try {
|
||
await call(commands.windowOpenSettings(page ?? null, newProjects, windowBackgroundHex()));
|
||
return;
|
||
} catch (e) {
|
||
console.warn("settings window unavailable, using the dialog", e);
|
||
}
|
||
}
|
||
await loadBuiltinPages();
|
||
openSettings(page, { newProjects });
|
||
}
|
||
|
||
export function activate(): () => void {
|
||
const disposers = [
|
||
trackRuntimeSettings(),
|
||
activatePythonStatus(),
|
||
unloadBuiltinPages,
|
||
registerOverlay({ id: "settings.dialog", component: LazySettingsDialog }),
|
||
registerActions(zoomActions()),
|
||
registerActions(settingsActions((page, newProjects) => void showSettings(page, newProjects))),
|
||
activateFileTypes(),
|
||
registerOverlay({ id: "settings.assignShortcut", component: AssignShortcutOverlay }),
|
||
registerActions([
|
||
{
|
||
// Search Everywhere's ⌥↵ on an action (arg: the action id).
|
||
id: "settings.assignShortcut",
|
||
title: "Assign Shortcut…",
|
||
category: "Settings",
|
||
hidden: true,
|
||
run: ({ arg }) => {
|
||
if (typeof arg === "string") showAssignShortcut(arg);
|
||
},
|
||
},
|
||
]),
|
||
provideSettingsSearchIndex(settingsSearchEntries),
|
||
isTauri() ? installSettingsHost() : () => {},
|
||
];
|
||
return () => disposers.reverse().forEach((d) => d());
|
||
}
|
||
|
||
function navigate(page: string | undefined) {
|
||
if (!page) return;
|
||
setQuery("");
|
||
selectPage(page);
|
||
}
|
||
|
||
interface SettingsLaunch {
|
||
page?: string;
|
||
newProjects: boolean;
|
||
}
|
||
|
||
async function takeLaunch(): Promise<SettingsLaunch | null> {
|
||
const launch = await commands.windowTakeLaunch().catch(() => null);
|
||
return launch && { page: launch.settingsPage ?? undefined, newProjects: launch.newProjects };
|
||
}
|
||
|
||
/** A launch for the open window: its page, and the mode when it asks for new projects. */
|
||
function relaunch(launch: SettingsLaunch | null) {
|
||
if (!launch) return;
|
||
if (launch.newProjects) setNewProjects(true);
|
||
navigate(launch.page);
|
||
}
|
||
|
||
function listenLaunch(): () => void {
|
||
let disposed = false;
|
||
let off: (() => void) | undefined;
|
||
void getCurrentWebviewWindow()
|
||
.listen(LAUNCH_EVENT, () => void takeLaunch().then(relaunch))
|
||
.then((u) => {
|
||
if (disposed) return u();
|
||
off = u;
|
||
// A page requested before we listened: its event is gone, the launch is not.
|
||
void takeLaunch().then(relaunch);
|
||
});
|
||
return () => {
|
||
disposed = true;
|
||
off?.();
|
||
};
|
||
}
|
||
|
||
/** Esc and ⌘W cancel, like closing the window; ⌘, stays here. */
|
||
function installWindowKeys(): () => void {
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.defaultPrevented || isImeKey(e)) return;
|
||
const mod = e.metaKey || e.ctrlKey;
|
||
if ((e.key === "Escape" && !isPopupOpen()) || (mod && !e.altKey && !e.shiftKey && e.key.toLowerCase() === "w")) {
|
||
e.preventDefault();
|
||
closeSettings();
|
||
} else if (mod && e.key === ",") {
|
||
e.preventDefault();
|
||
}
|
||
};
|
||
window.addEventListener("keydown", onKey);
|
||
return () => window.removeEventListener("keydown", onKey);
|
||
}
|
||
|
||
/**
|
||
* The settings window (`settings-<parent>`): settings pages only, working on
|
||
* the parent's project; Cancel / OK / Esc close the window.
|
||
*/
|
||
export function activateSettingsWindow(parent: string): () => void {
|
||
const disposers = [
|
||
trackRuntimeSettings(),
|
||
unloadBuiltinPages,
|
||
registerActions(settingsActions((page, newProjects) => relaunch({ page, newProjects: !!newProjects }))),
|
||
installActionProxies(parent),
|
||
listenLaunch(),
|
||
installWindowKeys(),
|
||
useSettingsDialog.subscribe((s, prev) => {
|
||
if (prev.open && !s.open) void getCurrentWebviewWindow().close();
|
||
if (s.newProjects !== prev.newProjects) {
|
||
void getCurrentWebviewWindow()
|
||
.setTitle(settingsTitle(s.newProjects))
|
||
.catch(() => {});
|
||
}
|
||
}),
|
||
];
|
||
void (async () => {
|
||
let launch: SettingsLaunch | null = null;
|
||
try {
|
||
// A page that fails to register is reported; the window still opens with the others.
|
||
await loadBuiltinPages().catch((e) => logError("cannot register settings pages", e));
|
||
await restoreRemote();
|
||
const project = await commands.projectCurrent().catch(() => null);
|
||
if (project) useProject.setState({ project, settings: project.settings });
|
||
else await loadSettings().catch((e) => console.warn("cannot load settings", e));
|
||
// `listenLaunch` may have taken the launch first and applied it already.
|
||
launch = await takeLaunch();
|
||
} finally {
|
||
// Without this the window would stay an empty title bar.
|
||
openSettings(launch?.page, { newProjects: launch?.newProjects ?? useSettingsDialog.getState().newProjects });
|
||
}
|
||
})();
|
||
return () => disposers.reverse().forEach((d) => d());
|
||
}
|