mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
initial implementation of move block to window (#77)
This commit is contained in:
parent
182c5f6e3d
commit
7b93354657
@ -127,7 +127,8 @@ func main() {
|
||||
if pidStr != "" {
|
||||
_, err := strconv.Atoi(pidStr)
|
||||
if err == nil {
|
||||
log.Printf("WAVESRV-ESTART\n")
|
||||
// use fmt instead of log here to make sure it goes directly to stderr
|
||||
fmt.Fprintf(os.Stderr, "WAVESRV-ESTART\n")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
@ -139,11 +139,37 @@ function runWaveSrv(): Promise<boolean> {
|
||||
waveSrvReadyResolve(true);
|
||||
return;
|
||||
}
|
||||
if (line.startsWith("WAVESRV-EVENT:")) {
|
||||
const evtJson = line.slice("WAVESRV-EVENT:".length);
|
||||
try {
|
||||
const evtMsg: WSEventType = JSON.parse(evtJson);
|
||||
handleWSEvent(evtMsg);
|
||||
} catch (e) {
|
||||
console.log("error handling WAVESRV-EVENT", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.log(line);
|
||||
});
|
||||
return rtnPromise;
|
||||
}
|
||||
|
||||
async function handleWSEvent(evtMsg: WSEventType) {
|
||||
if (evtMsg.eventtype == "electron:newwindow") {
|
||||
let windowId: string = evtMsg.data;
|
||||
let windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow;
|
||||
if (windowData == null) {
|
||||
return;
|
||||
}
|
||||
let clientData = await services.ClientService.GetClientData();
|
||||
const newWin = createBrowserWindow(clientData.oid, windowData);
|
||||
await newWin.readyPromise;
|
||||
newWin.show();
|
||||
} else {
|
||||
console.log("unhandled electron ws eventtype", evtMsg.eventtype);
|
||||
}
|
||||
}
|
||||
|
||||
async function mainResizeHandler(_: any, windowId: string, win: WaveBrowserWindow) {
|
||||
if (win == null || win.isDestroyed() || win.fullScreen) {
|
||||
return;
|
||||
@ -201,7 +227,9 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
|
||||
console.log("frame navigation canceled");
|
||||
}
|
||||
|
||||
function createBrowserWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow {
|
||||
// note, this does not *show* the window.
|
||||
// to show, await win.readyPromise and then win.show()
|
||||
function createBrowserWindow(clientId: string, waveWindow: WaveWindow): WaveBrowserWindow {
|
||||
let winBounds = {
|
||||
x: waveWindow.pos.x,
|
||||
y: waveWindow.pos.y,
|
||||
@ -236,7 +264,7 @@ function createBrowserWindow(client: Client, waveWindow: WaveWindow): WaveBrowse
|
||||
const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
|
||||
// const indexHtml = isDev ? "index-dev.html" : "index.html";
|
||||
let usp = new URLSearchParams();
|
||||
usp.set("clientid", client.oid);
|
||||
usp.set("clientid", clientId);
|
||||
usp.set("windowid", waveWindow.oid);
|
||||
const indexHtml = "index.html";
|
||||
if (isDevServer) {
|
||||
@ -406,7 +434,7 @@ electron.ipcMain.on("getCursorPoint", (event) => {
|
||||
async function createNewWaveWindow() {
|
||||
let clientData = await services.ClientService.GetClientData();
|
||||
const newWindow = await services.ClientService.MakeWindow();
|
||||
createBrowserWindow(clientData, newWindow);
|
||||
createBrowserWindow(clientData.oid, newWindow);
|
||||
}
|
||||
|
||||
electron.ipcMain.on("openNewWindow", createNewWaveWindow);
|
||||
@ -512,7 +540,7 @@ async function appMain() {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const win = createBrowserWindow(clientData, windowData);
|
||||
const win = createBrowserWindow(clientData.oid, windowData);
|
||||
wins.push(win);
|
||||
}
|
||||
for (let win of wins) {
|
||||
|
@ -3,11 +3,11 @@
|
||||
|
||||
import { Workspace } from "@/app/workspace/workspace";
|
||||
import { getLayoutStateAtomForTab, globalLayoutTransformsMap } from "@/faraday/lib/layoutAtom";
|
||||
import type { LayoutTreeState } from "@/faraday/lib/model";
|
||||
import { ContextMenuModel } from "@/store/contextmenu";
|
||||
import { WOS, atoms, globalStore, setBlockFocus } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import * as layoututil from "@/util/layoututil";
|
||||
import * as util from "@/util/util";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
@ -95,18 +95,6 @@ function switchTab(offset: number) {
|
||||
services.ObjectService.SetActiveTab(newActiveTabId);
|
||||
}
|
||||
|
||||
function findLeafIdFromBlockId(layoutTree: LayoutTreeState<TabLayoutData>, blockId: string): string {
|
||||
if (layoutTree?.leafs == null) {
|
||||
return null;
|
||||
}
|
||||
for (let leaf of layoutTree.leafs) {
|
||||
if (leaf.data.blockId == blockId) {
|
||||
return leaf.id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var transformRegexp = /translate\(\s*([0-9.]+)px\s*,\s*([0-9.]+)px\)/;
|
||||
|
||||
function parseFloatFromCSS(s: string | number): number {
|
||||
@ -174,7 +162,7 @@ function switchBlock(tabId: string, offsetX: number, offsetY: number) {
|
||||
}
|
||||
const layoutTreeState = globalStore.get(getLayoutStateAtomForTab(tabId, tabAtom));
|
||||
const curBlockId = globalStore.get(atoms.waveWindow).activeblockid;
|
||||
const curBlockLeafId = findLeafIdFromBlockId(layoutTreeState, curBlockId);
|
||||
const curBlockLeafId = layoututil.findLeafIdFromBlockId(layoutTreeState, curBlockId);
|
||||
if (curBlockLeafId == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -8,7 +8,8 @@ import { TerminalView } from "@/app/view/term/term";
|
||||
import { ErrorBoundary } from "@/element/errorboundary";
|
||||
import { CenteredDiv } from "@/element/quickelems";
|
||||
import { ContextMenuModel } from "@/store/contextmenu";
|
||||
import { atoms, setBlockFocus, useBlockAtom } from "@/store/global";
|
||||
import { atoms, globalStore, setBlockFocus, useBlockAtom } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import * as util from "@/util/util";
|
||||
import clsx from "clsx";
|
||||
@ -144,7 +145,12 @@ function handleHeaderContextMenu(e: React.MouseEvent<HTMLDivElement>, blockData:
|
||||
menu.push({
|
||||
label: "Move to New Window",
|
||||
click: () => {
|
||||
alert("Not Implemented");
|
||||
let currentTabId = globalStore.get(atoms.activeTabId);
|
||||
try {
|
||||
services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
|
||||
} catch (e) {
|
||||
console.error("error moving block to new window", e);
|
||||
}
|
||||
},
|
||||
});
|
||||
menu.push({ type: "separator" });
|
||||
@ -173,7 +179,12 @@ const FramelessBlockHeader = ({ blockId, onClose, dragHandleRef }: FramelessBloc
|
||||
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
|
||||
|
||||
return (
|
||||
<div key="header" className="block-header" ref={dragHandleRef}>
|
||||
<div
|
||||
key="header"
|
||||
className="block-header"
|
||||
ref={dragHandleRef}
|
||||
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
|
||||
>
|
||||
<div className="block-header-text text-fixed">{getBlockHeaderText(null, blockData, settingsConfig)}</div>
|
||||
{onClose && (
|
||||
<div className="close-button" onClick={onClose}>
|
||||
|
@ -5,6 +5,7 @@ import { LayoutTreeAction, LayoutTreeActionType, LayoutTreeInsertNodeAction, new
|
||||
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
|
||||
import { layoutTreeStateReducer } from "@/faraday/lib/layoutState";
|
||||
|
||||
import * as layoututil from "@/util/layoututil";
|
||||
import { produce } from "immer";
|
||||
import * as jotai from "jotai";
|
||||
import * as rxjs from "rxjs";
|
||||
@ -217,7 +218,8 @@ function handleWSEventMessage(msg: WSEventType) {
|
||||
return;
|
||||
}
|
||||
if (msg.eventtype == "layoutaction") {
|
||||
const layoutAction: WSLayoutAction = msg.data;
|
||||
console.log("got wslayoutaction", msg);
|
||||
const layoutAction: WSLayoutActionData = msg.data;
|
||||
if (layoutAction.actiontype == LayoutTreeActionType.InsertNode) {
|
||||
const insertNodeAction: LayoutTreeInsertNodeAction<TabLayoutData> = {
|
||||
type: LayoutTreeActionType.InsertNode,
|
||||
@ -226,6 +228,18 @@ function handleWSEventMessage(msg: WSEventType) {
|
||||
}),
|
||||
};
|
||||
runLayoutAction(layoutAction.tabid, insertNodeAction);
|
||||
} else if (layoutAction.actiontype == LayoutTreeActionType.DeleteNode) {
|
||||
const layoutStateAtom = getLayoutStateAtomForTab(
|
||||
layoutAction.tabid,
|
||||
WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", layoutAction.tabid))
|
||||
);
|
||||
const curState = globalStore.get(layoutStateAtom);
|
||||
const leafId = layoututil.findLeafIdFromBlockId(curState, layoutAction.blockid);
|
||||
const deleteNodeAction = {
|
||||
type: LayoutTreeActionType.DeleteNode,
|
||||
nodeId: leafId,
|
||||
};
|
||||
runLayoutAction(layoutAction.tabid, deleteNodeAction);
|
||||
} else {
|
||||
console.log("unsupported layout action", layoutAction);
|
||||
}
|
||||
|
@ -138,6 +138,12 @@ class WindowServiceType {
|
||||
return WOS.callBackendService("window", "CloseWindow", Array.from(arguments))
|
||||
}
|
||||
|
||||
// move block to new window
|
||||
// @returns object updates
|
||||
MoveBlockToNewWindow(currentTabId: string, blockId: string): Promise<void> {
|
||||
return WOS.callBackendService("window", "MoveBlockToNewWindow", Array.from(arguments))
|
||||
}
|
||||
|
||||
// @returns object updates
|
||||
SetWindowPosAndSize(arg2: string, arg3: Point, arg4: WinSize): Promise<void> {
|
||||
return WOS.callBackendService("window", "SetWindowPosAndSize", Array.from(arguments))
|
||||
|
18
frontend/util/layoututil.ts
Normal file
18
frontend/util/layoututil.ts
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { LayoutTreeState } from "@/faraday/index";
|
||||
|
||||
function findLeafIdFromBlockId(layoutTree: LayoutTreeState<TabLayoutData>, blockId: string): string {
|
||||
if (layoutTree?.leafs == null) {
|
||||
return null;
|
||||
}
|
||||
for (let leaf of layoutTree.leafs) {
|
||||
if (leaf.data.blockId == blockId) {
|
||||
return leaf.id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export { findLeafIdFromBlockId };
|
@ -20,7 +20,6 @@ console.log("clientid", clientId, "windowid", windowId);
|
||||
keyutil.setKeyUtilPlatform(getApi().getPlatform());
|
||||
|
||||
loadFonts();
|
||||
initWS();
|
||||
(window as any).globalWS = globalWS;
|
||||
(window as any).WOS = WOS;
|
||||
(window as any).globalStore = globalStore;
|
||||
@ -33,6 +32,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const client = await WOS.loadAndPinWaveObject<Client>(WOS.makeORef("client", clientId));
|
||||
const waveWindow = await WOS.loadAndPinWaveObject<WaveWindow>(WOS.makeORef("window", windowId));
|
||||
await WOS.loadAndPinWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));
|
||||
const initialTab = await WOS.loadAndPinWaveObject<Tab>(WOS.makeORef("tab", waveWindow.activetabid));
|
||||
WOS.loadAndPinWaveObject<LayoutNode>(WOS.makeORef("layout", initialTab.layoutNode));
|
||||
initWS();
|
||||
globalStore.set(atoms.settingsConfigAtom, await services.FileService.GetSettingsConfig());
|
||||
services.ObjectService.SetActiveTab(waveWindow.activetabid); // no need to wait
|
||||
const reactElem = React.createElement(App, null, null);
|
||||
|
@ -4,7 +4,12 @@
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
)
|
||||
@ -15,6 +20,7 @@ const (
|
||||
WSEvent_Config = "config"
|
||||
WSEvent_BlockControllerStatus = "blockcontroller:status"
|
||||
WSEvent_LayoutAction = "layoutaction"
|
||||
WSEvent_ElectronNewWindow = "electron:newwindow"
|
||||
)
|
||||
|
||||
type WSEventType struct {
|
||||
@ -41,6 +47,11 @@ type WindowWatchData struct {
|
||||
WatchedORefs map[waveobj.ORef]bool
|
||||
}
|
||||
|
||||
const (
|
||||
WSLayoutActionType_Insert = "insert"
|
||||
WSLayoutActionType_Remove = "delete"
|
||||
)
|
||||
|
||||
type WSLayoutActionData struct {
|
||||
TabId string `json:"tabid"`
|
||||
ActionType string `json:"actiontype"`
|
||||
@ -78,6 +89,21 @@ func getWindowWatchesForWindowId(windowId string) []*WindowWatchData {
|
||||
return watches
|
||||
}
|
||||
|
||||
// TODO fix busy wait -- but we need to wait until a new window connects back with a websocket
|
||||
// returns true if the window is connected
|
||||
func BusyWaitForWindowId(windowId string, timeout time.Duration) bool {
|
||||
endTime := time.Now().Add(timeout)
|
||||
for {
|
||||
if len(getWindowWatchesForWindowId(windowId)) > 0 {
|
||||
return true
|
||||
}
|
||||
if time.Now().After(endTime) {
|
||||
return false
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func getAllWatches() []*WindowWatchData {
|
||||
globalLock.Lock()
|
||||
defer globalLock.Unlock()
|
||||
@ -101,3 +127,14 @@ func SendEvent(event WSEventType) {
|
||||
wdata.WindowWSCh <- event
|
||||
}
|
||||
}
|
||||
|
||||
func SendEventToElectron(event WSEventType) {
|
||||
barr, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
log.Printf("cannot marshal electron message: %v\n", err)
|
||||
return
|
||||
}
|
||||
// send to electron
|
||||
log.Printf("sending event to electron: %q\n", event.EventType)
|
||||
fmt.Fprintf(os.Stderr, "\nWAVESRV-EVENT:%s\n", string(barr))
|
||||
}
|
||||
|
@ -6,9 +6,12 @@ package windowservice
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
||||
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
||||
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
||||
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||
)
|
||||
@ -72,6 +75,70 @@ func (svc *WindowService) CloseTab(ctx context.Context, uiContext wstore.UIConte
|
||||
return wstore.ContextGetUpdatesRtn(ctx), nil
|
||||
}
|
||||
|
||||
func (svc *WindowService) MoveBlockToNewWindow_Meta() tsgenmeta.MethodMeta {
|
||||
return tsgenmeta.MethodMeta{
|
||||
Desc: "move block to new window",
|
||||
ArgNames: []string{"ctx", "currentTabId", "blockId"},
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *WindowService) MoveBlockToNewWindow(ctx context.Context, currentTabId string, blockId string) (wstore.UpdatesRtnType, error) {
|
||||
log.Printf("MoveBlockToNewWindow(%s, %s)", currentTabId, blockId)
|
||||
ctx = wstore.ContextWithUpdates(ctx)
|
||||
curWindowId, err := wstore.DBFindWindowForTabId(ctx, currentTabId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error finding window for current-tab: %w", err)
|
||||
}
|
||||
tab, err := wstore.DBMustGet[*wstore.Tab](ctx, currentTabId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting tab: %w", err)
|
||||
}
|
||||
log.Printf("tab.BlockIds[%s]: %v", tab.OID, tab.BlockIds)
|
||||
var foundBlock bool
|
||||
for _, tabBlockId := range tab.BlockIds {
|
||||
if tabBlockId == blockId {
|
||||
foundBlock = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundBlock {
|
||||
return nil, fmt.Errorf("block not found in current tab")
|
||||
}
|
||||
newWindow, err := wstore.CreateWindow(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating window: %w", err)
|
||||
}
|
||||
err = wstore.MoveBlockToTab(ctx, currentTabId, newWindow.ActiveTabId, blockId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error moving block to tab: %w", err)
|
||||
}
|
||||
eventbus.SendEventToElectron(eventbus.WSEventType{
|
||||
EventType: eventbus.WSEvent_ElectronNewWindow,
|
||||
Data: newWindow.OID,
|
||||
})
|
||||
windowCreated := eventbus.BusyWaitForWindowId(newWindow.OID, 2*time.Second)
|
||||
if !windowCreated {
|
||||
return nil, fmt.Errorf("new window not created")
|
||||
}
|
||||
eventbus.SendEventToWindow(curWindowId, eventbus.WSEventType{
|
||||
EventType: eventbus.WSEvent_LayoutAction,
|
||||
Data: eventbus.WSLayoutActionData{
|
||||
ActionType: eventbus.WSLayoutActionType_Remove,
|
||||
TabId: currentTabId,
|
||||
BlockId: blockId,
|
||||
},
|
||||
})
|
||||
eventbus.SendEventToWindow(newWindow.OID, eventbus.WSEventType{
|
||||
EventType: eventbus.WSEvent_LayoutAction,
|
||||
Data: eventbus.WSLayoutActionData{
|
||||
ActionType: eventbus.WSLayoutActionType_Insert,
|
||||
TabId: newWindow.ActiveTabId,
|
||||
BlockId: blockId,
|
||||
},
|
||||
})
|
||||
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)
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
)
|
||||
|
||||
@ -395,6 +396,28 @@ func CreateWindow(ctx context.Context) (*Window, error) {
|
||||
return DBMustGet[*Window](ctx, windowId)
|
||||
}
|
||||
|
||||
func MoveBlockToTab(ctx context.Context, currentTabId string, newTabId string, blockId string) error {
|
||||
return WithTx(ctx, func(tx *TxWrap) error {
|
||||
currentTab, _ := DBGet[*Tab](tx.Context(), currentTabId)
|
||||
if currentTab == nil {
|
||||
return fmt.Errorf("current tab not found: %q", currentTabId)
|
||||
}
|
||||
newTab, _ := DBGet[*Tab](tx.Context(), newTabId)
|
||||
if newTab == nil {
|
||||
return fmt.Errorf("new tab not found: %q", newTabId)
|
||||
}
|
||||
blockIdx := findStringInSlice(currentTab.BlockIds, blockId)
|
||||
if blockIdx == -1 {
|
||||
return fmt.Errorf("block not found in current tab: %q", blockId)
|
||||
}
|
||||
currentTab.BlockIds = utilfn.RemoveElemFromSlice(currentTab.BlockIds, blockId)
|
||||
newTab.BlockIds = append(newTab.BlockIds, blockId)
|
||||
DBUpdate(tx.Context(), currentTab)
|
||||
DBUpdate(tx.Context(), newTab)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CreateClient(ctx context.Context) (*Client, error) {
|
||||
client := &Client{
|
||||
OID: uuid.NewString(),
|
||||
|
Loading…
Reference in New Issue
Block a user