initial implementation of move block to window (#77)

This commit is contained in:
Mike Sawka 2024-06-25 14:56:37 -07:00 committed by GitHub
parent 182c5f6e3d
commit 7b93354657
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 219 additions and 24 deletions

View File

@ -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")
}
}
}()

View File

@ -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) {

View File

@ -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;
}

View File

@ -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}>

View File

@ -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);
}

View File

@ -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))

View 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 };

View File

@ -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);

View File

@ -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))
}

View File

@ -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)

View File

@ -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(),