mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
multi-window support (#62)
This commit is contained in:
parent
5c6cfbc112
commit
fb668fd4e5
242
emain/emain.ts
242
emain/emain.ts
@ -21,12 +21,14 @@ const AuthKeyFile = "waveterm.authkey";
|
||||
const DevServerEndpoint = "http://127.0.0.1:8190";
|
||||
const ProdServerEndpoint = "http://127.0.0.1:1719";
|
||||
|
||||
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string };
|
||||
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string; readyPromise: Promise<void> };
|
||||
|
||||
let waveSrvReadyResolve = (value: boolean) => {};
|
||||
let waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
|
||||
waveSrvReadyResolve = resolve;
|
||||
});
|
||||
let globalIsQuitting = false;
|
||||
let globalIsStarting = true;
|
||||
|
||||
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
|
||||
electronApp.setName(isDev ? "NextWave (Dev)" : "NextWave");
|
||||
@ -74,6 +76,11 @@ function getWaveSrvCwd(): string {
|
||||
return getWaveHomeDir();
|
||||
}
|
||||
|
||||
function getWindowForEvent(event: Electron.IpcMainEvent): Electron.BrowserWindow {
|
||||
const windowId = event.sender.id;
|
||||
return electron.BrowserWindow.fromId(windowId);
|
||||
}
|
||||
|
||||
function runWaveSrv(): Promise<boolean> {
|
||||
let pResolve: (value: boolean) => void;
|
||||
let pReject: (reason?: any) => void;
|
||||
@ -99,6 +106,9 @@ function runWaveSrv(): Promise<boolean> {
|
||||
env: envCopy,
|
||||
});
|
||||
proc.on("exit", (e) => {
|
||||
if (globalIsQuitting) {
|
||||
return;
|
||||
}
|
||||
console.log("wavesrv exited, shutting down");
|
||||
electronApp.quit();
|
||||
});
|
||||
@ -189,31 +199,21 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
|
||||
console.log("frame navigation canceled");
|
||||
}
|
||||
|
||||
function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow {
|
||||
const primaryDisplay = electron.screen.getPrimaryDisplay();
|
||||
let winHeight = waveWindow.winsize.height;
|
||||
let winWidth = waveWindow.winsize.width;
|
||||
if (winHeight > primaryDisplay.workAreaSize.height) {
|
||||
winHeight = primaryDisplay.workAreaSize.height;
|
||||
}
|
||||
if (winWidth > primaryDisplay.workAreaSize.width) {
|
||||
winWidth = primaryDisplay.workAreaSize.width;
|
||||
}
|
||||
let winX = waveWindow.pos.x;
|
||||
let winY = waveWindow.pos.y;
|
||||
if (winX + winWidth > primaryDisplay.workAreaSize.width) {
|
||||
winX = Math.floor((primaryDisplay.workAreaSize.width - winWidth) / 2);
|
||||
}
|
||||
if (winY + winHeight > primaryDisplay.workAreaSize.height) {
|
||||
winY = Math.floor((primaryDisplay.workAreaSize.height - winHeight) / 2);
|
||||
}
|
||||
function createBrowserWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow {
|
||||
let winBounds = {
|
||||
x: waveWindow.pos.x,
|
||||
y: waveWindow.pos.y,
|
||||
width: waveWindow.winsize.width,
|
||||
height: waveWindow.winsize.height,
|
||||
};
|
||||
winBounds = ensureBoundsAreVisible(winBounds);
|
||||
const bwin = new electron.BrowserWindow({
|
||||
x: winX,
|
||||
y: winY,
|
||||
titleBarStyle: "hiddenInset",
|
||||
width: winWidth,
|
||||
height: winHeight,
|
||||
minWidth: 500,
|
||||
x: winBounds.x,
|
||||
y: winBounds.y,
|
||||
width: winBounds.width,
|
||||
height: winBounds.height,
|
||||
minWidth: 400,
|
||||
minHeight: 300,
|
||||
icon:
|
||||
unamePlatform == "linux"
|
||||
@ -227,10 +227,11 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
|
||||
backgroundColor: "#000000",
|
||||
});
|
||||
(bwin as any).waveWindowId = waveWindow.oid;
|
||||
const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
|
||||
win.once("ready-to-show", () => {
|
||||
win.show();
|
||||
let readyResolve: (value: void) => void;
|
||||
(bwin as any).readyPromise = new Promise((resolve, _) => {
|
||||
readyResolve = resolve;
|
||||
});
|
||||
const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
|
||||
// const indexHtml = isDev ? "index-dev.html" : "index.html";
|
||||
let usp = new URLSearchParams();
|
||||
usp.set("clientid", client.oid);
|
||||
@ -243,7 +244,9 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
|
||||
console.log("running as file");
|
||||
win.loadFile(path.join(getElectronAppBasePath(), "frontend", indexHtml), { search: usp.toString() });
|
||||
}
|
||||
|
||||
win.once("ready-to-show", () => {
|
||||
readyResolve();
|
||||
});
|
||||
win.webContents.on("will-navigate", shNavHandler);
|
||||
win.webContents.on("will-frame-navigate", shFrameNavHandler);
|
||||
win.on(
|
||||
@ -254,6 +257,33 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
|
||||
"move",
|
||||
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
|
||||
);
|
||||
win.on("focus", () => {
|
||||
if (globalIsStarting) {
|
||||
return;
|
||||
}
|
||||
console.log("focus", waveWindow.oid);
|
||||
services.ClientService.FocusWindow(waveWindow.oid);
|
||||
});
|
||||
win.on("close", (e) => {
|
||||
if (globalIsQuitting) {
|
||||
return;
|
||||
}
|
||||
const choice = electron.dialog.showMessageBoxSync(win, {
|
||||
type: "question",
|
||||
buttons: ["Cancel", "Yes"],
|
||||
title: "Confirm",
|
||||
message: "Are you sure you want to close this window (all tabs and blocks will be deleted)?",
|
||||
});
|
||||
if (choice === 0) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
win.on("closed", () => {
|
||||
if (globalIsQuitting) {
|
||||
return;
|
||||
}
|
||||
services.WindowService.CloseWindow(waveWindow.oid);
|
||||
});
|
||||
win.webContents.on("zoom-changed", (e) => {
|
||||
win.webContents.send("zoom-changed");
|
||||
});
|
||||
@ -268,6 +298,86 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
|
||||
return win;
|
||||
}
|
||||
|
||||
function isWindowFullyVisible(bounds: electron.Rectangle): boolean {
|
||||
const displays = electron.screen.getAllDisplays();
|
||||
|
||||
// Helper function to check if a point is inside any display
|
||||
function isPointInDisplay(x, y) {
|
||||
for (let display of displays) {
|
||||
const { x: dx, y: dy, width, height } = display.bounds;
|
||||
if (x >= dx && x < dx + width && y >= dy && y < dy + height) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check all corners of the window
|
||||
const topLeft = isPointInDisplay(bounds.x, bounds.y);
|
||||
const topRight = isPointInDisplay(bounds.x + bounds.width, bounds.y);
|
||||
const bottomLeft = isPointInDisplay(bounds.x, bounds.y + bounds.height);
|
||||
const bottomRight = isPointInDisplay(bounds.x + bounds.width, bounds.y + bounds.height);
|
||||
|
||||
return topLeft && topRight && bottomLeft && bottomRight;
|
||||
}
|
||||
|
||||
function findDisplayWithMostArea(bounds: electron.Rectangle): electron.Display {
|
||||
const displays = electron.screen.getAllDisplays();
|
||||
let maxArea = 0;
|
||||
let bestDisplay = null;
|
||||
|
||||
for (let display of displays) {
|
||||
const { x, y, width, height } = display.bounds;
|
||||
const overlapX = Math.max(0, Math.min(bounds.x + bounds.width, x + width) - Math.max(bounds.x, x));
|
||||
const overlapY = Math.max(0, Math.min(bounds.y + bounds.height, y + height) - Math.max(bounds.y, y));
|
||||
const overlapArea = overlapX * overlapY;
|
||||
|
||||
if (overlapArea > maxArea) {
|
||||
maxArea = overlapArea;
|
||||
bestDisplay = display;
|
||||
}
|
||||
}
|
||||
|
||||
return bestDisplay;
|
||||
}
|
||||
|
||||
function adjustBoundsToFitDisplay(bounds: electron.Rectangle, display: electron.Display): electron.Rectangle {
|
||||
const { x: dx, y: dy, width: dWidth, height: dHeight } = display.workArea;
|
||||
let { x, y, width, height } = bounds;
|
||||
|
||||
// Adjust width and height to fit within the display's work area
|
||||
width = Math.min(width, dWidth);
|
||||
height = Math.min(height, dHeight);
|
||||
|
||||
// Adjust x to ensure the window fits within the display
|
||||
if (x < dx) {
|
||||
x = dx;
|
||||
} else if (x + width > dx + dWidth) {
|
||||
x = dx + dWidth - width;
|
||||
}
|
||||
|
||||
// Adjust y to ensure the window fits within the display
|
||||
if (y < dy) {
|
||||
y = dy;
|
||||
} else if (y + height > dy + dHeight) {
|
||||
y = dy + dHeight - height;
|
||||
}
|
||||
return { x, y, width, height };
|
||||
}
|
||||
|
||||
function ensureBoundsAreVisible(bounds: electron.Rectangle): electron.Rectangle {
|
||||
if (!isWindowFullyVisible(bounds)) {
|
||||
let targetDisplay = findDisplayWithMostArea(bounds);
|
||||
|
||||
if (!targetDisplay) {
|
||||
targetDisplay = electron.screen.getPrimaryDisplay();
|
||||
}
|
||||
|
||||
return adjustBoundsToFitDisplay(bounds, targetDisplay);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
electron.ipcMain.on("isDev", (event) => {
|
||||
event.returnValue = isDev;
|
||||
});
|
||||
@ -287,7 +397,13 @@ electron.ipcMain.on("getCursorPoint", (event) => {
|
||||
event.returnValue = retVal;
|
||||
});
|
||||
|
||||
electron.ipcMain.on("openNewWindow", (event) => {});
|
||||
async function createNewWaveWindow() {
|
||||
let clientData = await services.ClientService.GetClientData();
|
||||
const newWindow = await services.ClientService.MakeWindow();
|
||||
createBrowserWindow(clientData, newWindow);
|
||||
}
|
||||
|
||||
electron.ipcMain.on("openNewWindow", createNewWaveWindow);
|
||||
|
||||
electron.ipcMain.on("context-editmenu", (_, { x, y }, opts) => {
|
||||
if (opts == null) {
|
||||
@ -337,7 +453,45 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
|
||||
return electron.Menu.buildFromTemplate(menuItems);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
function makeAppMenu() {
|
||||
let fileMenu: Electron.MenuItemConstructorOptions[] = [];
|
||||
fileMenu.push({
|
||||
label: "New Window",
|
||||
click: createNewWaveWindow,
|
||||
});
|
||||
fileMenu.push({
|
||||
label: "Close Window",
|
||||
click: () => {
|
||||
electron.BrowserWindow.getFocusedWindow()?.close();
|
||||
},
|
||||
});
|
||||
const menuTemplate: Electron.MenuItemConstructorOptions[] = [
|
||||
{
|
||||
role: "appMenu",
|
||||
},
|
||||
{
|
||||
role: "fileMenu",
|
||||
submenu: fileMenu,
|
||||
},
|
||||
{
|
||||
role: "editMenu",
|
||||
},
|
||||
{
|
||||
role: "viewMenu",
|
||||
},
|
||||
{
|
||||
role: "windowMenu",
|
||||
},
|
||||
];
|
||||
const menu = electron.Menu.buildFromTemplate(menuTemplate);
|
||||
electron.Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
electron.app.on("before-quit", () => {
|
||||
globalIsQuitting = true;
|
||||
});
|
||||
|
||||
async function appMain() {
|
||||
const startTs = Date.now();
|
||||
const instanceLock = electronApp.requestSingleInstanceLock();
|
||||
if (!instanceLock) {
|
||||
@ -349,6 +503,7 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
|
||||
if (!fs.existsSync(waveHomeDir)) {
|
||||
fs.mkdirSync(waveHomeDir);
|
||||
}
|
||||
makeAppMenu();
|
||||
try {
|
||||
await runWaveSrv();
|
||||
} catch (e) {
|
||||
@ -358,17 +513,30 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
|
||||
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
|
||||
|
||||
console.log("get client data");
|
||||
let clientData = (await services.ClientService.GetClientData().catch((e) => console.log(e))) as Client;
|
||||
let clientData = await services.ClientService.GetClientData();
|
||||
console.log("client data ready");
|
||||
let windowData: WaveWindow = (await services.ObjectService.GetObject(
|
||||
"window:" + clientData.mainwindowid
|
||||
)) as WaveWindow;
|
||||
await electronApp.whenReady();
|
||||
createWindow(clientData, windowData);
|
||||
let wins: WaveBrowserWindow[] = [];
|
||||
for (let windowId of clientData.windowids.slice().reverse()) {
|
||||
let windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow;
|
||||
const win = createBrowserWindow(clientData, windowData);
|
||||
wins.push(win);
|
||||
}
|
||||
for (let win of wins) {
|
||||
await win.readyPromise;
|
||||
console.log("show", win.waveWindowId);
|
||||
win.show();
|
||||
}
|
||||
globalIsStarting = false;
|
||||
|
||||
electronApp.on("activate", () => {
|
||||
if (electron.BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow(clientData, windowData);
|
||||
createNewWaveWindow();
|
||||
}
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
appMain().catch((e) => {
|
||||
console.log("appMain error", e);
|
||||
electronApp.quit();
|
||||
});
|
||||
|
@ -60,6 +60,10 @@
|
||||
|
||||
&.block-preview {
|
||||
background-color: var(--main-bg-color);
|
||||
|
||||
.block-frame-tech-close {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.block-focused {
|
||||
@ -68,10 +72,6 @@
|
||||
.block-frame-tech-header {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.block-frame-tech-close {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.block-frame-tech-header {
|
||||
|
@ -21,18 +21,24 @@ export const BlockService = new BlockServiceType()
|
||||
|
||||
// clientservice.ClientService (client)
|
||||
class ClientServiceType {
|
||||
FocusWindow(arg2: string): Promise<void> {
|
||||
return WOS.callBackendService("client", "FocusWindow", Array.from(arguments))
|
||||
}
|
||||
GetClientData(): Promise<Client> {
|
||||
return WOS.callBackendService("client", "GetClientData", Array.from(arguments))
|
||||
}
|
||||
GetTab(arg1: string): Promise<Tab> {
|
||||
return WOS.callBackendService("client", "GetTab", Array.from(arguments))
|
||||
}
|
||||
GetWindow(arg1: string): Promise<Window> {
|
||||
GetWindow(arg1: string): Promise<WaveWindow> {
|
||||
return WOS.callBackendService("client", "GetWindow", Array.from(arguments))
|
||||
}
|
||||
GetWorkspace(arg1: string): Promise<Workspace> {
|
||||
return WOS.callBackendService("client", "GetWorkspace", Array.from(arguments))
|
||||
}
|
||||
MakeWindow(): Promise<WaveWindow> {
|
||||
return WOS.callBackendService("client", "MakeWindow", Array.from(arguments))
|
||||
}
|
||||
}
|
||||
|
||||
export const ClientService = new ClientServiceType()
|
||||
@ -59,11 +65,6 @@ class ObjectServiceType {
|
||||
return WOS.callBackendService("object", "AddTabToWorkspace", Array.from(arguments))
|
||||
}
|
||||
|
||||
// @returns object updates
|
||||
CloseTab(tabId: string): Promise<void> {
|
||||
return WOS.callBackendService("object", "CloseTab", Array.from(arguments))
|
||||
}
|
||||
|
||||
// @returns blockId (and object updates)
|
||||
CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<string> {
|
||||
return WOS.callBackendService("object", "CreateBlock", Array.from(arguments))
|
||||
@ -109,6 +110,14 @@ export const ObjectService = new ObjectServiceType()
|
||||
|
||||
// windowservice.WindowService (window)
|
||||
class WindowServiceType {
|
||||
// @returns object updates
|
||||
CloseTab(arg3: string): Promise<void> {
|
||||
return WOS.callBackendService("window", "CloseTab", Array.from(arguments))
|
||||
}
|
||||
CloseWindow(arg2: string): Promise<void> {
|
||||
return WOS.callBackendService("window", "CloseWindow", Array.from(arguments))
|
||||
}
|
||||
|
||||
// @returns object updates
|
||||
SetWindowPosAndSize(arg2: string, arg3: Point, arg4: WinSize): Promise<void> {
|
||||
return WOS.callBackendService("window", "SetWindowPosAndSize", Array.from(arguments))
|
||||
|
@ -313,7 +313,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
};
|
||||
|
||||
const handleCloseTab = (tabId: string) => {
|
||||
services.ObjectService.CloseTab(tabId);
|
||||
services.WindowService.CloseTab(tabId);
|
||||
deleteLayoutStateAtomForTab(tabId);
|
||||
};
|
||||
|
||||
|
27
frontend/types/gotypes.d.ts
vendored
27
frontend/types/gotypes.d.ts
vendored
@ -31,7 +31,7 @@ declare global {
|
||||
|
||||
type BlockCommand = {
|
||||
command: string;
|
||||
} & ( BlockAppendIJsonCommand | ResolveIdsCommand | BlockInputCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand );
|
||||
} & ( BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand | BlockAppendIJsonCommand | ResolveIdsCommand | BlockInputCommand | BlockSetViewCommand );
|
||||
|
||||
// wstore.BlockDef
|
||||
type BlockDef = {
|
||||
@ -84,6 +84,7 @@ declare global {
|
||||
// wstore.Client
|
||||
type Client = WaveObj & {
|
||||
mainwindowid: string;
|
||||
windowids: string[];
|
||||
meta: MetaType;
|
||||
};
|
||||
|
||||
@ -253,6 +254,18 @@ declare global {
|
||||
obj?: WaveObj;
|
||||
};
|
||||
|
||||
// wstore.Window
|
||||
type WaveWindow = WaveObj & {
|
||||
workspaceid: string;
|
||||
activetabid: string;
|
||||
activeblockid?: string;
|
||||
activeblockmap: {[key: string]: string};
|
||||
pos: Point;
|
||||
winsize: WinSize;
|
||||
lastfocusts: number;
|
||||
meta: MetaType;
|
||||
};
|
||||
|
||||
// service.WebCallType
|
||||
type WebCallType = {
|
||||
service: string;
|
||||
@ -275,18 +288,6 @@ declare global {
|
||||
height: number;
|
||||
};
|
||||
|
||||
// wstore.Window
|
||||
type WaveWindow = WaveObj & {
|
||||
workspaceid: string;
|
||||
activetabid: string;
|
||||
activeblockid?: string;
|
||||
activeblockmap: {[key: string]: string};
|
||||
pos: Point;
|
||||
winsize: WinSize;
|
||||
lastfocusts: number;
|
||||
meta: MetaType;
|
||||
};
|
||||
|
||||
// wstore.Workspace
|
||||
type Workspace = WaveObj & {
|
||||
name: string;
|
||||
|
@ -27,6 +27,8 @@ function matchViewportSize() {
|
||||
document.body.style.height = window.visualViewport.height + "px";
|
||||
}
|
||||
|
||||
document.title = `The Next Wave (${windowId.substring(0, 8)})`;
|
||||
|
||||
matchViewportSize();
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||
)
|
||||
|
||||
@ -54,3 +55,21 @@ func (cs *ClientService) GetWindow(windowId string) (*wstore.Window, error) {
|
||||
}
|
||||
return window, nil
|
||||
}
|
||||
|
||||
func (cs *ClientService) MakeWindow(ctx context.Context) (*wstore.Window, error) {
|
||||
return wstore.CreateWindow(ctx)
|
||||
}
|
||||
|
||||
// moves the window to the front of the windowId stack
|
||||
func (cs *ClientService) FocusWindow(ctx context.Context, windowId string) error {
|
||||
client, err := cs.GetClientData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
winIdx := utilfn.SliceIdx(client.WindowIds, windowId)
|
||||
if winIdx == -1 {
|
||||
return nil
|
||||
}
|
||||
client.WindowIds = utilfn.MoveSliceIdxToFront(client.WindowIds, winIdx)
|
||||
return wstore.DBUpdate(ctx, client)
|
||||
}
|
||||
|
@ -205,41 +205,6 @@ func (svc *ObjectService) CloseTab_Meta() tsgenmeta.MethodMeta {
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *ObjectService) CloseTab(uiContext wstore.UIContext, tabId string) (wstore.UpdatesRtnType, error) {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||
defer cancelFn()
|
||||
ctx = wstore.ContextWithUpdates(ctx)
|
||||
window, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting window: %w", err)
|
||||
}
|
||||
tab, err := wstore.DBMustGet[*wstore.Tab](ctx, tabId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting tab: %w", err)
|
||||
}
|
||||
for _, blockId := range tab.BlockIds {
|
||||
blockcontroller.StopBlockController(blockId)
|
||||
}
|
||||
err = wstore.CloseTab(ctx, window.WorkspaceId, tabId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error closing tab: %w", err)
|
||||
}
|
||||
if window.ActiveTabId == tabId {
|
||||
ws, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting workspace: %w", err)
|
||||
}
|
||||
var newActiveTabId string
|
||||
if len(ws.TabIds) > 0 {
|
||||
newActiveTabId = ws.TabIds[0]
|
||||
} else {
|
||||
newActiveTabId = ""
|
||||
}
|
||||
wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId)
|
||||
}
|
||||
return wstore.ContextGetUpdatesRtn(ctx), nil
|
||||
}
|
||||
|
||||
func (svc *ObjectService) UpdateObjectMeta_Meta() tsgenmeta.MethodMeta {
|
||||
return tsgenmeta.MethodMeta{
|
||||
ArgNames: []string{"uiContext", "oref", "meta"},
|
||||
|
@ -5,10 +5,15 @@ package windowservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||
)
|
||||
|
||||
const DefaultTimeout = 2 * time.Second
|
||||
|
||||
type WindowService struct{}
|
||||
|
||||
func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId string, pos *wstore.Point, size *wstore.WinSize) (wstore.UpdatesRtnType, error) {
|
||||
@ -32,3 +37,64 @@ func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId strin
|
||||
}
|
||||
return wstore.ContextGetUpdatesRtn(ctx), nil
|
||||
}
|
||||
|
||||
func (svc *WindowService) CloseTab(ctx context.Context, uiContext wstore.UIContext, tabId string) (wstore.UpdatesRtnType, error) {
|
||||
ctx = wstore.ContextWithUpdates(ctx)
|
||||
window, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting window: %w", err)
|
||||
}
|
||||
tab, err := wstore.DBMustGet[*wstore.Tab](ctx, tabId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting tab: %w", err)
|
||||
}
|
||||
for _, blockId := range tab.BlockIds {
|
||||
blockcontroller.StopBlockController(blockId)
|
||||
}
|
||||
err = wstore.DeleteTab(ctx, window.WorkspaceId, tabId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error closing tab: %w", err)
|
||||
}
|
||||
if window.ActiveTabId == tabId {
|
||||
ws, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting workspace: %w", err)
|
||||
}
|
||||
var newActiveTabId string
|
||||
if len(ws.TabIds) > 0 {
|
||||
newActiveTabId = ws.TabIds[0]
|
||||
} else {
|
||||
newActiveTabId = ""
|
||||
}
|
||||
wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId)
|
||||
}
|
||||
return wstore.ContextGetUpdatesRtn(ctx), nil
|
||||
}
|
||||
|
||||
func (svc *WindowService) CloseWindow(ctx context.Context, windowId string) error {
|
||||
ctx = wstore.ContextWithUpdates(ctx)
|
||||
window, err := wstore.DBMustGet[*wstore.Window](ctx, windowId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting window: %w", err)
|
||||
}
|
||||
workspace, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting workspace: %w", err)
|
||||
}
|
||||
for _, tabId := range workspace.TabIds {
|
||||
uiContext := wstore.UIContext{WindowId: windowId}
|
||||
_, err := svc.CloseTab(ctx, uiContext, tabId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error closing tab: %w", err)
|
||||
}
|
||||
}
|
||||
err = wstore.DBDelete(ctx, wstore.OType_Workspace, window.WorkspaceId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting workspace: %w", err)
|
||||
}
|
||||
err = wstore.DBDelete(ctx, wstore.OType_Window, windowId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting window: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -101,7 +101,11 @@ func TypeToTSType(t reflect.Type, tsTypesMap map[reflect.Type]string) (string, [
|
||||
}
|
||||
return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes
|
||||
case reflect.Struct:
|
||||
return t.Name(), []reflect.Type{t}
|
||||
name := t.Name()
|
||||
if tsRename := tsRenameMap[name]; tsRename != "" {
|
||||
name = tsRename
|
||||
}
|
||||
return name, []reflect.Type{t}
|
||||
case reflect.Ptr:
|
||||
return TypeToTSType(t.Elem(), tsTypesMap)
|
||||
case reflect.Interface:
|
||||
|
@ -740,3 +740,25 @@ func DoMapStucture(out any, input any) error {
|
||||
}
|
||||
return decoder.Decode(input)
|
||||
}
|
||||
|
||||
func SliceIdx[T comparable](arr []T, elem T) int {
|
||||
for idx, e := range arr {
|
||||
if e == elem {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func MoveSliceIdxToFront[T any](arr []T, idx int) []T {
|
||||
// create and return a new slice with idx moved to the front
|
||||
if idx == 0 || idx >= len(arr) {
|
||||
// make a copy still
|
||||
return append([]T(nil), arr...)
|
||||
}
|
||||
rtn := make([]T, 0, len(arr))
|
||||
rtn = append(rtn, arr[idx])
|
||||
rtn = append(rtn, arr[0:idx]...)
|
||||
rtn = append(rtn, arr[idx+1:]...)
|
||||
return rtn
|
||||
}
|
||||
|
@ -259,7 +259,7 @@ func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
|
||||
})
|
||||
}
|
||||
|
||||
func CloseTab(ctx context.Context, workspaceId string, tabId string) error {
|
||||
func DeleteTab(ctx context.Context, workspaceId string, tabId string) error {
|
||||
return WithTx(ctx, func(tx *TxWrap) error {
|
||||
ws, _ := DBGet[*Workspace](tx.Context(), workspaceId)
|
||||
if ws == nil {
|
||||
@ -319,29 +319,11 @@ func UpdateObjectMeta(ctx context.Context, oref waveobj.ORef, meta map[string]an
|
||||
})
|
||||
}
|
||||
|
||||
func EnsureInitialData() error {
|
||||
// does not need to run in a transaction since it is called on startup
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancelFn()
|
||||
clientCount, err := DBGetCount[*Client](ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting client count: %w", err)
|
||||
}
|
||||
if clientCount > 0 {
|
||||
return nil
|
||||
}
|
||||
func CreateWindow(ctx context.Context) (*Window, error) {
|
||||
windowId := uuid.NewString()
|
||||
workspaceId := uuid.NewString()
|
||||
tabId := uuid.NewString()
|
||||
layoutNodeId := uuid.NewString()
|
||||
client := &Client{
|
||||
OID: uuid.NewString(),
|
||||
MainWindowId: windowId,
|
||||
}
|
||||
err = DBInsert(ctx, client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error inserting client: %w", err)
|
||||
}
|
||||
window := &Window{
|
||||
OID: windowId,
|
||||
WorkspaceId: workspaceId,
|
||||
@ -356,28 +338,28 @@ func EnsureInitialData() error {
|
||||
Height: 600,
|
||||
},
|
||||
}
|
||||
err = DBInsert(ctx, window)
|
||||
err := DBInsert(ctx, window)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error inserting window: %w", err)
|
||||
return nil, fmt.Errorf("error inserting window: %w", err)
|
||||
}
|
||||
ws := &Workspace{
|
||||
OID: workspaceId,
|
||||
Name: "default",
|
||||
Name: "w" + workspaceId[0:8],
|
||||
TabIds: []string{tabId},
|
||||
}
|
||||
err = DBInsert(ctx, ws)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error inserting workspace: %w", err)
|
||||
return nil, fmt.Errorf("error inserting workspace: %w", err)
|
||||
}
|
||||
tab := &Tab{
|
||||
OID: tabId,
|
||||
Name: "Tab-1",
|
||||
Name: "T1",
|
||||
BlockIds: []string{},
|
||||
LayoutNode: layoutNodeId,
|
||||
}
|
||||
err = DBInsert(ctx, tab)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error inserting tab: %w", err)
|
||||
return nil, fmt.Errorf("error inserting tab: %w", err)
|
||||
}
|
||||
|
||||
layoutNode := &LayoutNode{
|
||||
@ -385,7 +367,62 @@ func EnsureInitialData() error {
|
||||
}
|
||||
err = DBInsert(ctx, layoutNode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error inserting layout node: %w", err)
|
||||
return nil, fmt.Errorf("error inserting layout node: %w", err)
|
||||
}
|
||||
client, err := DBGetSingleton[*Client](ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting client: %w", err)
|
||||
}
|
||||
client.WindowIds = append(client.WindowIds, windowId)
|
||||
err = DBUpdate(ctx, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error updating client: %w", err)
|
||||
}
|
||||
return DBMustGet[*Window](ctx, windowId)
|
||||
}
|
||||
|
||||
func CreateClient(ctx context.Context) (*Client, error) {
|
||||
client := &Client{
|
||||
OID: uuid.NewString(),
|
||||
WindowIds: []string{},
|
||||
}
|
||||
err := DBInsert(ctx, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error inserting client: %w", err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func EnsureInitialData() error {
|
||||
// does not need to run in a transaction since it is called on startup
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancelFn()
|
||||
client, err := DBGetSingleton[*Client](ctx)
|
||||
if err == ErrNotFound {
|
||||
client, err = CreateClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating client: %w", err)
|
||||
}
|
||||
}
|
||||
if client.MainWindowId != "" {
|
||||
// convert to windowIds
|
||||
client.WindowIds = []string{client.MainWindowId}
|
||||
client.MainWindowId = ""
|
||||
err = DBUpdate(ctx, client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating client: %w", err)
|
||||
}
|
||||
client, err = DBGetSingleton[*Client](ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting client (after main window update): %w", err)
|
||||
}
|
||||
}
|
||||
if len(client.WindowIds) > 0 {
|
||||
return nil
|
||||
}
|
||||
_, err = CreateWindow(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating window: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -67,7 +67,10 @@ func DBGetSingletonByType(ctx context.Context, otype string) (waveobj.WaveObj, e
|
||||
table := tableNameFromOType(otype)
|
||||
query := fmt.Sprintf("SELECT oid, version, data FROM %s LIMIT 1", table)
|
||||
var row idDataType
|
||||
tx.Get(&row, query)
|
||||
found := tx.Get(&row, query)
|
||||
if !found {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
rtn, err := waveobj.FromJson(row.Data)
|
||||
if err != nil {
|
||||
return rtn, err
|
||||
|
@ -115,7 +115,8 @@ func (update *WaveObjUpdate) UnmarshalJSON(data []byte) error {
|
||||
type Client struct {
|
||||
OID string `json:"oid"`
|
||||
Version int `json:"version"`
|
||||
MainWindowId string `json:"mainwindowid"`
|
||||
MainWindowId string `json:"mainwindowid"` // deprecated
|
||||
WindowIds []string `json:"windowids"`
|
||||
Meta map[string]any `json:"meta"`
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user