Skip to content
This repository has been archived by the owner on Jun 4, 2024. It is now read-only.

Commit

Permalink
Register studio as a url handler for foxglove:// files
Browse files Browse the repository at this point in the history
Add an "open" command with support for rosbag files from urls
  • Loading branch information
defunctzombie committed Apr 16, 2021
1 parent 401bbe7 commit 155cba2
Show file tree
Hide file tree
Showing 8 changed files with 499 additions and 332 deletions.
3 changes: 3 additions & 0 deletions app/OsContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export interface OsContext {
// Get the version string from package.json
getAppVersion: () => string;

// Get an array of deep links provided on app launch
getDeepLinks: () => string[];

// file backed key/value storage
storage: Storage;
}
27 changes: 27 additions & 0 deletions app/components/PlayerManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { connect, ConnectedProps } from "react-redux";
import { useAsync, useLocalStorage, useMountedState } from "react-use";
import { URL } from "universal-url";

import OsContextSingleton from "@foxglove-studio/app/OsContextSingleton";
import {
Expand Down Expand Up @@ -423,6 +424,32 @@ function PlayerManager({
}
}, []);

useEffect(() => {
const links = OsContextSingleton?.getDeepLinks() ?? [];
const firstLink = links[0];
if (firstLink == undefined) {
return;
}

try {
const url = new URL(firstLink);
// only support the open command
if (url.pathname !== "//open") {
return;
}

// only support rosbag urls
const type = url.searchParams.get("type");
const bagUrl = url.searchParams.get("url");
if (type !== "rosbag" || bagUrl == undefined) {
return;
}
setPlayer(async (options: BuildPlayerOptions) => buildPlayerFromBagURLs([bagUrl], options));
} catch (err) {
log.error(err);
}
}, [setPlayer]);

// The first time we load a source, we restore the previous source state (i.e. url)
// and try to automatically load the source.
// Subsequent source changes do not restore the state which typically results in a user prompt
Expand Down
280 changes: 280 additions & 0 deletions desktop/StudioWindow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import {
app,
BrowserWindow,
BrowserWindowConstructorOptions,
Menu,
MenuItemConstructorOptions,
shell,
systemPreferences,
MenuItem,
} from "electron";
import path from "path";

import colors from "@foxglove-studio/app/styles/colors.module.scss";
import Logger from "@foxglove/log";

import { getTelemetrySettings } from "./telemetry";

declare const MAIN_WINDOW_WEBPACK_ENTRY: string;

const isMac = process.platform === "darwin";
const isProduction = process.env.NODE_ENV === "production";
const rendererPath = MAIN_WINDOW_WEBPACK_ENTRY;

const log = Logger.getLogger(__filename);

function newStudioWindow(deepLinks: string[] = []): BrowserWindow {
const [allowCrashReporting, allowTelemetry] = getTelemetrySettings();

const preloadPath = path.join(app.getAppPath(), "main", "preload.js");

const windowOptions: BrowserWindowConstructorOptions = {
height: 800,
width: 1200,
autoHideMenuBar: true,
title: APP_NAME,
webPreferences: {
contextIsolation: true,
preload: preloadPath,
nodeIntegration: false,
additionalArguments: [
`--allowCrashReporting=${allowCrashReporting ? "1" : "0"}`,
`--allowTelemetry=${allowTelemetry ? "1" : "0"}`,
...deepLinks,
],
// Disable webSecurity in development so we can make XML-RPC calls, load
// remote data, etc. In production, the app is served from file:// URLs so
// the Origin header is not sent, disabling the CORS
// Access-Control-Allow-Origin check
webSecurity: isProduction,
},
backgroundColor: colors.background,
};
if (isMac) {
windowOptions.titleBarStyle = "hiddenInset";
}

const browserWindow = new BrowserWindow(windowOptions);

// Forward full screen events to the renderer
browserWindow.addListener("enter-full-screen", () =>
browserWindow.webContents.send("enter-full-screen"),
);

browserWindow.addListener("leave-full-screen", () =>
browserWindow.webContents.send("leave-full-screen"),
);

browserWindow.webContents.once("dom-ready", () => {
if (!isProduction) {
browserWindow.webContents.openDevTools();
}
});

// Open all new windows in an external browser
// Note: this API is supposed to be superseded by webContents.setWindowOpenHandler,
// but using that causes the app to freeze when a new window is opened.
browserWindow.webContents.on("new-window", (event, url) => {
event.preventDefault();
shell.openExternal(url);
});

browserWindow.webContents.on("ipc-message", (_event: unknown, channel: string) => {
if (channel === "window.toolbar-double-clicked") {
const action: string =
systemPreferences.getUserDefault?.("AppleActionOnDoubleClick", "string") || "Maximize";
if (action === "Minimize") {
browserWindow.minimize();
} else if (action === "Maximize") {
browserWindow.isMaximized() ? browserWindow.unmaximize() : browserWindow.maximize();
} else {
// "None"
}
}
});

return browserWindow;
}

function buildMenu(browserWindow: BrowserWindow): Menu {
const menuTemplate: MenuItemConstructorOptions[] = [];

if (isMac) {
menuTemplate.push({
role: "appMenu",
label: app.name,
submenu: [
{ role: "about" },
{ type: "separator" },
{
label: "Preferences…",
accelerator: "CommandOrControl+,",
click: () => browserWindow.webContents.send("open-preferences"),
},
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "unhide" },
{ type: "separator" },
{ role: "quit" },
],
});
}

menuTemplate.push({
role: "fileMenu",
label: "File",
id: "fileMenu",
submenu: [isMac ? { role: "close" } : { role: "quit" }],
});

menuTemplate.push({
role: "editMenu",
label: "Edit",
submenu: [
{
label: "Undo",
accelerator: "CommandOrControl+Z",
click: () => browserWindow.webContents.send("undo"),
},
{
label: "Redo",
accelerator: "CommandOrControl+Shift+Z",
click: () => browserWindow.webContents.send("redo"),
},
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
...(isMac
? [
{ role: "pasteAndMatchStyle" } as const,
{ role: "delete" } as const,
{ role: "selectAll" } as const,
]
: [
{ role: "delete" } as const,
{ type: "separator" } as const,
{ role: "selectAll" } as const,
]),
],
});

const showSharedWorkersMenu = () => {
// Electron doesn't let us update dynamic menus when they are being opened, so just open a popup
// context menu. This is ugly, but only for development anyway.
// https://github.com/electron/electron/issues/528
const workers = browserWindow.webContents.getAllSharedWorkers();
Menu.buildFromTemplate(
workers.length === 0
? [{ label: "No Shared Workers", enabled: false }]
: workers.map(
(worker) =>
new MenuItem({
label: worker.url,
click() {
browserWindow.webContents.closeDevTools();
browserWindow.webContents.inspectSharedWorkerById(worker.id);
},
}),
),
).popup();
};

menuTemplate.push({
role: "viewMenu",
label: "View",
submenu: [
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ type: "separator" },
{ role: "togglefullscreen" },
{ type: "separator" },
{
label: "Advanced",
submenu: [
{ role: "reload" },
{ role: "forceReload" },
{ role: "toggleDevTools" },
{
label: "Inspect Shared Worker…",
click() {
showSharedWorkersMenu();
},
},
],
},
],
});

menuTemplate.push({
role: "help",
submenu: [
{
label: "Welcome",
click: () => browserWindow.webContents.send("open-welcome-layout"),
},
{
label: "Message Path Syntax",
click: () => browserWindow.webContents.send("open-message-path-syntax-help"),
},
{
label: "Keyboard Shortcuts",
accelerator: "CommandOrControl+/",
click: () => browserWindow.webContents.send("open-keyboard-shortcuts"),
},
{
label: "Learn More",
click: async () => shell.openExternal("https://foxglove.dev"),
},
],
});

return Menu.buildFromTemplate(menuTemplate);
}

class StudioWindow {
private static windowsById = new Map<number, StudioWindow>();

private _window: BrowserWindow;
private _menu: Menu;

constructor(deepLinks: string[] = []) {
const browserWindow = newStudioWindow(deepLinks);
this._window = browserWindow;
this._menu = buildMenu(browserWindow);

const id = browserWindow.webContents.id;

log.info(`New studio window ${id}`);
StudioWindow.windowsById.set(id, this);
browserWindow.once("closed", () => {
StudioWindow.windowsById.delete(id);
});

// load after setting windowsById so any ipc handlers with id lookup work
log.info(`window.loadURL(${rendererPath})`);
browserWindow.loadURL(rendererPath).then(() => log.info("window URL loaded"));
}

getBrowserWindow(): BrowserWindow {
return this._window;
}

getMenu(): Menu {
return this._menu;
}

static fromId(id: number): StudioWindow | undefined {
return StudioWindow.windowsById.get(id);
}
}

export default StudioWindow;

0 comments on commit 155cba2

Please sign in to comment.