From 5744f4b06f07d7e957d47063fe9e161b2fe4a6f4 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Thu, 5 Dec 2024 18:26:20 -0800 Subject: [PATCH] closeTab fix (#1403) fixes bug with closeTab when the tab didn't exist in the waveWindow cache. also adds Cmd-Shift-W to close a tab (doesn't work for pinned tabs). and restores Cmd-W for killing blocks on pinned tabs --- emain/emain-tabview.ts | 2 +- emain/emain-window.ts | 60 ++++++++++++++++++++-------------- emain/preload.ts | 2 +- emain/updater.ts | 2 +- frontend/app/store/keymodel.ts | 21 ++++++++++-- frontend/app/tab/tabbar.tsx | 5 +-- frontend/types/custom.d.ts | 2 +- 7 files changed, 60 insertions(+), 34 deletions(-) diff --git a/emain/emain-tabview.ts b/emain/emain-tabview.ts index 543276d92..4085521d8 100644 --- a/emain/emain-tabview.ts +++ b/emain/emain-tabview.ts @@ -108,7 +108,7 @@ export class WaveTabView extends WebContentsView { // TODO: circuitous const waveWindow = waveWindowMap.get(this.waveWindowId); if (waveWindow) { - waveWindow.allTabViews.delete(this.waveTabId); + waveWindow.allLoadedTabViews.delete(this.waveTabId); } } } diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 393599c9e..3e0b2e6c9 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -22,7 +22,7 @@ export class WaveBrowserWindow extends BaseWindow { waveWindowId: string; workspaceId: string; waveReadyPromise: Promise; - allTabViews: Map; + allLoadedTabViews: Map; activeTabView: WaveTabView; private canClose: boolean; private deleteAllowed: boolean; @@ -108,7 +108,7 @@ export class WaveBrowserWindow extends BaseWindow { this.tabSwitchQueue = []; this.waveWindowId = waveWindow.oid; this.workspaceId = waveWindow.workspaceid; - this.allTabViews = new Map(); + this.allLoadedTabViews = new Map(); const winBoundsPoller = setInterval(() => { if (this.isDestroyed()) { clearInterval(winBoundsPoller); @@ -237,7 +237,7 @@ export class WaveBrowserWindow extends BaseWindow { console.log("win removing window from backend DB", this.waveWindowId); fireAndForget(() => WindowService.CloseWindow(this.waveWindowId, true)); } - for (const tabView of this.allTabViews.values()) { + for (const tabView of this.allLoadedTabViews.values()) { tabView?.destroy(); } waveWindowMap.delete(this.waveWindowId); @@ -278,15 +278,15 @@ export class WaveBrowserWindow extends BaseWindow { return; } console.log("switchWorkspace newWs", newWs); - if (this.allTabViews.size) { - for (const tab of this.allTabViews.values()) { + if (this.allLoadedTabViews.size) { + for (const tab of this.allLoadedTabViews.values()) { this.contentView.removeChildView(tab); tab?.destroy(); } } console.log("destroyed all tabs", this.waveWindowId); this.workspaceId = workspaceId; - this.allTabViews = new Map(); + this.allLoadedTabViews = new Map(); await this.setActiveTab(newWs.activetabid, false); } @@ -306,17 +306,22 @@ export class WaveBrowserWindow extends BaseWindow { } async closeTab(tabId: string) { - console.log("closeTab", tabId, this.waveWindowId, this.workspaceId); - const tabView = this.allTabViews.get(tabId); - if (tabView) { - const rtn = await WorkspaceService.CloseTab(this.workspaceId, tabId, true); - if (rtn?.closewindow) { - this.close(); - } else if (rtn?.newactivetabid) { - await this.setActiveTab(rtn.newactivetabid, false); - } - this.allTabViews.delete(tabId); + console.log(`closeTab tabid=${tabId} ws=${this.workspaceId} window=${this.waveWindowId}`); + const rtn = await WorkspaceService.CloseTab(this.workspaceId, tabId, true); + if (rtn == null) { + console.log("[error] closeTab: no return value", tabId, this.workspaceId, this.waveWindowId); + return; } + if (rtn.closewindow) { + this.close(); + return; + } + if (!rtn.newactivetabid) { + console.log("[error] closeTab, no new active tab", tabId, this.workspaceId, this.waveWindowId); + return; + } + await this.setActiveTab(rtn.newactivetabid, false); + this.allLoadedTabViews.delete(tabId); } async setTabViewIntoWindow(tabView: WaveTabView, tabInitialized: boolean) { @@ -330,7 +335,7 @@ export class WaveBrowserWindow extends BaseWindow { oldActiveView.isActiveTab = false; } this.activeTabView = tabView; - this.allTabViews.set(tabView.waveTabId, tabView); + this.allLoadedTabViews.set(tabView.waveTabId, tabView); if (!tabInitialized) { console.log("initializing a new tab"); await tabView.initPromise; @@ -407,7 +412,7 @@ export class WaveBrowserWindow extends BaseWindow { } const curBounds = this.getContentBounds(); this.activeTabView?.positionTabOnScreen(curBounds); - for (const tabView of this.allTabViews.values()) { + for (const tabView of this.allLoadedTabViews.values()) { if (tabView == this.activeTabView) { continue; } @@ -465,7 +470,7 @@ export class WaveBrowserWindow extends BaseWindow { export function getWaveWindowByTabId(tabId: string): WaveBrowserWindow { for (const ww of waveWindowMap.values()) { - if (ww.allTabViews.has(tabId)) { + if (ww.allLoadedTabViews.has(tabId)) { return ww; } } @@ -530,17 +535,22 @@ ipcMain.on("set-active-tab", async (event, tabId) => { ipcMain.on("create-tab", async (event, opts) => { const senderWc = event.sender; const ww = getWaveWindowByWebContentsId(senderWc.id); - if (!ww) { - return; + if (ww != null) { + await ww.createTab(); } - await ww.createTab(); event.returnValue = true; return null; }); -ipcMain.on("close-tab", async (event, tabId) => { - const ww = getWaveWindowByTabId(tabId); - await ww.closeTab(tabId); +ipcMain.on("close-tab", async (event, workspaceId, tabId) => { + const ww = getWaveWindowByWorkspaceId(workspaceId); + if (ww == null) { + console.log(`close-tab: no window found for workspace ws=${workspaceId} tab=${tabId}`); + return; + } + if (ww != null) { + await ww.closeTab(tabId); + } event.returnValue = true; return null; }); diff --git a/emain/preload.ts b/emain/preload.ts index 86ecdadeb..1636eda94 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -44,7 +44,7 @@ contextBridge.exposeInMainWorld("api", { deleteWorkspace: (workspaceId) => ipcRenderer.send("delete-workspace", workspaceId), setActiveTab: (tabId) => ipcRenderer.send("set-active-tab", tabId), createTab: () => ipcRenderer.send("create-tab"), - closeTab: (tabId) => ipcRenderer.send("close-tab", tabId), + closeTab: (workspaceId, tabId) => ipcRenderer.send("close-tab", workspaceId, tabId), setWindowInitStatus: (status) => ipcRenderer.send("set-window-init-status", status), onWaveInit: (callback) => ipcRenderer.on("wave-init", (_event, initOpts) => callback(initOpts)), sendLog: (log) => ipcRenderer.send("fe-log", log), diff --git a/emain/updater.ts b/emain/updater.ts index 93ec747a9..03a526e27 100644 --- a/emain/updater.ts +++ b/emain/updater.ts @@ -112,7 +112,7 @@ export class Updater { private set status(value: UpdaterStatus) { this._status = value; getAllWaveWindows().forEach((window) => { - const allTabs = Array.from(window.allTabViews.values()); + const allTabs = Array.from(window.allLoadedTabViews.values()); allTabs.forEach((tab) => { tab.webContents.send("app-update-status", value); }); diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index 82daad8b9..56448b3fb 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -71,15 +71,20 @@ function shouldDispatchToBlock(e: WaveKeyboardEvent): boolean { } function genericClose(tabId: string) { + const ws = globalStore.get(atoms.workspace); const tabORef = WOS.makeORef("tab", tabId); const tabAtom = WOS.getWaveObjectAtom(tabORef); const tabData = globalStore.get(tabAtom); if (tabData == null) { return; } + if (ws.pinnedtabids?.includes(tabId) && tabData.blockids?.length == 1) { + // don't allow closing the last block in a pinned tab + return; + } if (tabData.blockids == null || tabData.blockids.length == 0) { // close tab - getApi().closeTab(tabId); + getApi().closeTab(ws.oid, tabId); deleteLayoutModelForTab(tabId); return; } @@ -246,11 +251,21 @@ function registerGlobalKeys() { return true; }); globalKeyMap.set("Cmd:w", () => { + const tabId = globalStore.get(atoms.staticTabId); + genericClose(tabId); + return true; + }); + globalKeyMap.set("Cmd:Shift:w", () => { const tabId = globalStore.get(atoms.staticTabId); const ws = globalStore.get(atoms.workspace); - if (!ws.pinnedtabids?.includes(tabId)) { - genericClose(tabId); + if (ws.pinnedtabids?.includes(tabId)) { + // switch to first unpinned tab if it exists (for close spamming) + if (ws.tabids != null && ws.tabids.length > 0) { + getApi().setActiveTab(ws.tabids[0]); + } + return true; } + getApi().closeTab(ws.oid, tabId); return true; }); globalKeyMap.set("Cmd:m", () => { diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index 817f48262..2bc6fa853 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -5,7 +5,7 @@ import { Button } from "@/app/element/button"; import { modalsModel } from "@/app/store/modalmodel"; import { WindowDrag } from "@/element/windowdrag"; import { deleteLayoutModelForTab } from "@/layout/index"; -import { atoms, createTab, getApi, isDev, PLATFORM, setActiveTab } from "@/store/global"; +import { atoms, createTab, getApi, globalStore, isDev, PLATFORM, setActiveTab } from "@/store/global"; import { fireAndForget } from "@/util/util"; import { useAtomValue } from "jotai"; import { OverlayScrollbars } from "overlayscrollbars"; @@ -570,7 +570,8 @@ const TabBar = memo(({ workspace }: TabBarProps) => { const handleCloseTab = (event: React.MouseEvent | null, tabId: string) => { event?.stopPropagation(); - getApi().closeTab(tabId); + const ws = globalStore.get(atoms.workspace); + getApi().closeTab(ws.oid, tabId); tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease"); deleteLayoutModelForTab(tabId); }; diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 49dd49803..6c8053bf1 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -93,7 +93,7 @@ declare global { deleteWorkspace: (workspaceId: string) => void; setActiveTab: (tabId: string) => void; createTab: () => void; - closeTab: (tabId: string) => void; + closeTab: (workspaceId: string, tabId: string) => void; setWindowInitStatus: (status: "ready" | "wave-ready") => void; onWaveInit: (callback: (initOpts: WaveInitOpts) => void) => void; sendLog: (log: string) => void;