(core) Support dark mode in custom widgets

Test Plan: Manual.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D4036
This commit is contained in:
George Gevoian
2023-09-19 14:44:22 -04:00
parent ffbf93b85f
commit 4c25aa7d3d
18 changed files with 444 additions and 71 deletions

View File

@@ -4,6 +4,8 @@ import {SafeBrowser} from 'app/client/lib/SafeBrowser';
import {ActiveDocAPI} from 'app/common/ActiveDocAPI';
import {LocalPlugin} from 'app/common/plugin';
import {createRpcLogger, PluginInstance} from 'app/common/PluginInstance';
import {Theme} from 'app/common/ThemePrefs';
import {Computed} from 'grainjs';
import {Rpc} from 'grain-rpc';
/**
@@ -13,15 +15,31 @@ export class DocPluginManager {
public pluginsList: PluginInstance[];
constructor(localPlugins: LocalPlugin[], private _untrustedContentOrigin: string, private _docComm: ActiveDocAPI,
private _clientScope: ClientScope) {
private _clientScope = this._options.clientScope;
private _docComm = this._options.docComm;
private _localPlugins = this._options.plugins;
private _theme = this._options.theme;
private _untrustedContentOrigin = this._options.untrustedContentOrigin;
constructor(private _options: {
plugins: LocalPlugin[],
untrustedContentOrigin: string,
docComm: ActiveDocAPI,
clientScope: ClientScope,
theme: Computed<Theme>,
}) {
this.pluginsList = [];
for (const plugin of localPlugins) {
for (const plugin of this._localPlugins) {
try {
const pluginInstance = new PluginInstance(plugin, createRpcLogger(console, `PLUGIN ${plugin.id}:`));
const components = plugin.manifest.components || {};
const safeBrowser = pluginInstance.safeBrowser = new SafeBrowser(pluginInstance,
this._clientScope, this._untrustedContentOrigin, components.safeBrowser);
const safeBrowser = pluginInstance.safeBrowser = new SafeBrowser({
pluginInstance,
clientScope: this._clientScope,
untrustedContentOrigin: this._untrustedContentOrigin,
mainPath: components.safeBrowser,
theme: this._theme,
});
if (components.safeBrowser) {
pluginInstance.rpc.registerForwarder(components.safeBrowser, safeBrowser);
}

View File

@@ -14,8 +14,8 @@ export class HomePluginManager {
public pluginsList: PluginInstance[];
constructor(localPlugins: LocalPlugin[],
_untrustedContentOrigin: string,
_clientScope: ClientScope) {
untrustedContentOrigin: string,
clientScope: ClientScope) {
this.pluginsList = [];
for (const plugin of localPlugins) {
try {
@@ -30,8 +30,12 @@ export class HomePluginManager {
continue;
}
const pluginInstance = new PluginInstance(plugin, createRpcLogger(console, `HOME PLUGIN ${plugin.id}:`));
const safeBrowser = pluginInstance.safeBrowser = new SafeBrowser(pluginInstance,
_clientScope, _untrustedContentOrigin, components.safeBrowser);
const safeBrowser = pluginInstance.safeBrowser = new SafeBrowser({
pluginInstance,
clientScope,
untrustedContentOrigin,
mainPath: components.safeBrowser,
});
if (components.safeBrowser) {
pluginInstance.rpc.registerForwarder(components.safeBrowser, safeBrowser);
}

View File

@@ -36,12 +36,15 @@ import * as Mousetrap from 'app/client/lib/Mousetrap';
import { ActionRouter } from 'app/common/ActionRouter';
import { BaseComponent, BaseLogger, createRpcLogger, PluginInstance, warnIfNotReady } from 'app/common/PluginInstance';
import { tbind } from 'app/common/tbind';
import { Theme } from 'app/common/ThemePrefs';
import { getOriginUrl } from 'app/common/urlUtils';
import { GristAPI, RPC_GRISTAPI_INTERFACE } from 'app/plugin/GristAPI';
import { RenderOptions, RenderTarget } from 'app/plugin/RenderOptions';
import { checkers } from 'app/plugin/TypeCheckers';
import { IMsgCustom, IMsgRpcCall, Rpc } from 'grain-rpc';
import { Computed, dom as grainjsDom, Observable } from 'grainjs';
import { IMsgCustom, IMsgRpcCall, IRpcLogger, MsgType, Rpc } from 'grain-rpc';
import { Disposable } from './dispose';
import isEqual from 'lodash/isEqual';
const G = getBrowserGlobals('document', 'window');
/**
@@ -54,7 +57,6 @@ const G = getBrowserGlobals('document', 'window');
// client processes and among other thing will expose both renderImpl and
// disposeImpl. ClientProcess will hold a reference to ProcessManager instead of SafeBrowser.
export class SafeBrowser extends BaseComponent {
/**
* Create a webview ClientProcess to render safe browser process in electron.
*/
@@ -71,6 +73,8 @@ export class SafeBrowser extends BaseComponent {
new IframeProcess(safeBrowser, rpc, src);
}
public theme? = this._options.theme;
// All view processes. This is not used anymore to dispose all processes on deactivation (this is
// now achieved using `this._mainProcess.autoDispose(...)`) but rather to be able to dispatch
// events to all processes (such as doc actions which will need soon).
@@ -80,17 +84,30 @@ export class SafeBrowser extends BaseComponent {
private _mainProcess: WorkerProcess|undefined;
private _viewCount: number = 0;
constructor(
private _plugin: PluginInstance,
private _clientScope: ClientScope,
private _untrustedContentOrigin: string,
private _mainPath: string = "",
private _baseLogger: BaseLogger = console,
rpcLogger = createRpcLogger(_baseLogger, `PLUGIN ${_plugin.definition.id} SafeBrowser:`),
) {
super(_plugin.definition.manifest, rpcLogger);
this._pluginId = _plugin.definition.id;
this._pluginRpc = _plugin.rpc;
private _plugin = this._options.pluginInstance;
private _clientScope = this._options.clientScope;
private _untrustedContentOrigin = this._options.untrustedContentOrigin;
private _mainPath = this._options.mainPath ?? '';
private _baseLogger = this._options.baseLogger ?? console;
constructor(private _options: {
pluginInstance: PluginInstance,
clientScope: ClientScope,
untrustedContentOrigin: string,
mainPath?: string,
baseLogger?: BaseLogger,
rpcLogger?: IRpcLogger,
theme?: Computed<Theme>,
}) {
super(
_options.pluginInstance.definition.manifest,
_options.rpcLogger ?? createRpcLogger(
_options.baseLogger ?? console,
`PLUGIN ${_options.pluginInstance.definition.id} SafeBrowser:`
)
);
this._pluginId = this._plugin.definition.id;
this._pluginRpc = this._plugin.rpc;
}
/**
@@ -274,6 +291,9 @@ class WorkerProcess extends ClientProcess {
export class ViewProcess extends ClientProcess {
public element: HTMLElement;
// Set once all of the plugin's onOptions handlers have been called.
protected _optionsInitialized: Observable<boolean>;
}
/**
@@ -282,10 +302,23 @@ export class ViewProcess extends ClientProcess {
class IframeProcess extends ViewProcess {
public create(safeBrowser: SafeBrowser, rpc: Rpc, src: string) {
super.create(safeBrowser, rpc, src);
const iframe = this.element = this.autoDispose(dom(`iframe.safe_browser_process.clipboard_focus`,
{ src }));
const listener = (event: MessageEvent) => {
this._optionsInitialized = Observable.create(this, false);
const iframe = this.element = this.autoDispose(
grainjsDom(`iframe.safe_browser_process.clipboard_focus`,
{src},
grainjsDom.style('visibility', use => use(this._optionsInitialized) ? 'visible' : 'hidden'),
) as HTMLIFrameElement
);
const listener = async (event: MessageEvent) => {
if (event.source === iframe.contentWindow) {
if (event.data.mtype === MsgType.Ready) {
await this._sendSettings({theme: safeBrowser.theme?.get()}, true);
}
if (event.data.data?.settings?.status === 'initialized') {
this._optionsInitialized.set(true);
}
this.rpc.receiveMessage(event.data);
}
};
@@ -294,8 +327,21 @@ class IframeProcess extends ViewProcess {
G.window.removeEventListener('message', listener);
});
this.rpc.setSendMessage(msg => iframe.contentWindow!.postMessage(msg, '*'));
if (safeBrowser.theme) {
this.autoDispose(
safeBrowser.theme.addListener(async (newTheme, oldTheme) => {
if (isEqual(newTheme, oldTheme)) { return; }
await this._sendSettings({theme: safeBrowser.theme?.get()});
})
);
}
}
private async _sendSettings(settings: {theme?: Theme}, fromReady = false) {
await this.rpc.postMessage({settings, fromReady});
}
}
/**