New Window Extension
This type of extension is the most complex. It creates a new window, within which there will be a brand new rendering process. This requires you to create the corresponding RPC
service to communicate with the main process, rendering process, and extension process to get the exposed API
.
Inside the new window, you can use React
, Vue
, or any other framework you like to develop the UI. You just need to use an appropriate packaging tool to build and package your code into .js
and .html
files.
To demonstrate the development of this type of extension, we provide a sample extension, which you can find on GitHub. The function of this extension is that in Windows and Linux systems, pressing the space will pop up a new window displaying the preview of the first page of the currently selected PDF.
In the following sections, we will use this extension as an example.
Development Environment
Similarly, we provide a basic development environment. You can find it on the new-window
branch on GitHub. This environment uses Vue
for UI development and vite
for packaging. Of course, you can create the environment you like.
The project structure is as follows:
├── dist // packaged files
├── ext // extension source code directory
│ ├── src/main.ts // extension entry file
│ │── vite.config.ts // vite configuration file for extension
│ ├── ...
├── view // UI source code directory
│ ├── src // UI source code directory
│ ├── index.html // UI entry file
│ ├── vite.config.ts // vite configuration file for UI
│ ├── ...
├── tsconfig.json // typescript configuration file
├── package.json // npm configuration file
When building, the code in the ext
and view
directories will be packaged into dist/main.js
and dist/view
respectively. You can see in ext/main.ts
that we use ./view/index.html
to load the UI file of the new window.
Extension Entry
New Window extensions still need to create an extension entry file, which contains the extension class. These codes are the same as other types of extensions, and they all run in the extension process. The creation of the new window is also done there.
Extension Class Structure
class PaperlibPreviewExtension extends PLExtension {
disposeCallbacks: (() => void)[];
constructor() {
super({
id: "@future-scholars/paperlib-preview-extension",
defaultPreference: {},
});
this.disposeCallbacks = [];
}
private async _createPreviewWindow() {
...
}
async initialize() {
...
try {
await this._createPreviewWindow();
} catch (error) {
PLAPI.logService.error(
"Failed to create preview window",
error as Error,
true,
"Preview",
);
}
...
}
async dispose() {
...
}
}
Create a New Window
When the extension is initializing, we call the _createPreviewWindow
method to create a new window. In this method, we use PLMainAPI.windowProcessManagementService
to create a new window. This service provides some methods to manage windows.
private async _createPreviewWindow() {
const screenSize =
await PLMainAPI.windowProcessManagementService.getScreenSize();
await PLMainAPI.windowProcessManagementService.create(
"paperlib-preview-extension-window",
{
entry: path.resolve(__dirname, "./view/index.html"),
title: "Paper Preview",
width: Math.floor(screenSize.height * 0.8 * 0.75),
height: Math.floor(screenSize.height * 0.8),
minWidth: Math.floor(screenSize.height * 0.8 * 0.75),
minHeight: Math.floor(screenSize.height * 0.8),
useContentSize: true,
center: true,
resizable: false,
skipTaskbar: true,
webPreferences: {
webSecurity: false,
nodeIntegration: true,
contextIsolation: false,
},
frame: false,
show: false,
},
);
}
The first argument of the PLMainAPI.windowProcessManagementService.create
method is the id
of the new window. This id
is used to identify the window, and it is also the ID of the process corresponding to the window. The second argument is an object that contains some configurations of the new window. These configurations are the same as the options of electron
's BrowserWindow
. You can find the detailed description of these options in the electron document.
In the above example, we get the size of the screen to limit the size of the new window. Note that here, we use ./view/index.html
to specify the UI entry file of the new window.
Listen to Window Events
A window usually emits some events, such as close, blur, etc. We can use PLMainAPI.windowProcessManagementService.on
to listen to these events.
PLMainAPI.windowProcessManagementService.on(
"paperlib-preview-extension-window" as any,
(newValues: { value: string }) => {
if (newValues.value === "blur") {
PLMainAPI.windowProcessManagementService.hide(
"paperlib-preview-extension-window",
);
}
},
)
The first argument of this method is the id
of the window. The second argument is a callback function, which will be called when the window emits an event. The argument of the callback function is an object, which contains the type of the event. For example, here, we listen to the blur
event, when the window loses focus, we hide it.
For the window events, you can find them in the documentation of PLMainAPI.windowProcessManagementService
.
Window Control
We provide a series of methods to operate the window, such as hide, minimize, close, etc. You can find these methods in the documentation of PLMainAPI.windowProcessManagementService
.
In this example, we listen to the user pressing the Preview button in the menu bar, and then we show our new window:
PLMainAPI.menuService.onClick("View-preview", async () => {
PLMainAPI.windowProcessManagementService.show(
"paperlib-preview-extension-window",
);
})
New Window UI
In this example, we use Vue
as the UI development framework. Please make sure that you are familiar with the basic Vue
development knowledge before continuing to read.
UI Entry File
In view/index.html
, we set a div
tag with an id
of app
to mount the Vue
application.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline' 'unsafe-eval';"
/>
<title>Paperlib Preview</title>
<link rel="stylesheet" href="./css/style.css" />
</head>
<body style="overflow: hidden">
<div id="app"></div>
<script type="module" src="./src/index.ts"></script>
</body>
</html>
The entry file of the UI logic code is view/src/index.ts
.
UI Logic Code
In view/src/index.ts
, we use the createApp
method of Vue
to create a Vue
application. In the createApp
method, we pass in an AppView
component, which is the root component of our UI.
import { createApp } from "vue";
import AppView from "./app.vue";
async function initialize() {
const app = createApp(AppView);
...
app.mount("#app");
}
initialize();
In the above initialization function, the most important part is to create the RPC
service of this process:
import { createApp } from "vue";
import AppView from "./app.vue";
import { RendererRPCService } from "paperlib-api/rpc";
import { PreviewService } from "./services/preview-service";
async function initialize() {
const app = createApp(AppView);
// ============================================================
// 1. Initilize the RPC service for current process
const rpcService = new RendererRPCService("paperlib-preview-extension-window");
// ============================================================
// 2. Start the port exchange process.
await rpcService.initCommunication();
// ============================================================
// 3. Wait for the main process to expose its APIs (PLMainAPI)
const mainAPIExposed = await rpcService.waitForAPI(
"mainProcess",
"PLMainAPI",
5000
);
if (!mainAPIExposed) {
throw new Error("Main process API is not exposed");
} else {
console.log("Main process API is exposed");
}
// 4. Wait for the renderer process to expose its APIs (PLRendererAPI)
const rendererAPIExposed = await rpcService.waitForAPI(
"rendererProcess",
"PLAPI",
5000
);
if (!rendererAPIExposed) {
throw new Error("Renderer process API is not exposed");
} else {
console.log("Renderer process API is exposed");
}
app.mount("#app");
}
initialize();
In the above code, we first created an instance of RendererRPCService
. This will create an RPC
service in the current process. Then, we called the rpcService.initCommunication()
method, which will notify other processes and establish the corresponding MessagePort
communication channel to expose the corresponding API
. Please refer to services/rpc-service.ts
for more.
The first argument must be consistant with the ID passed in when creating the new window.
When the rpcService.initCommunication()
method is executed, we can use the rpcService.waitForAPI
method to wait for other processes to expose their corresponding API
. Here, we wait for the main process and the rendering process to expose PLMainAPI
and PLAPI
. If you also need to access the corresponding service of the extension process, such as PLExtAPI.extensionPreferenceService
, you can also wait for the extension process to expose PLExtAPI
here:
const extAPIExposed = await rpcService.waitForAPI(
"extensionProcess", // Process ID
"PLExtAPI", // API name
5000 // Timeout
);
if (!extAPIExposed) {
throw new Error("Ext process API is not exposed");
} else {
console.log("Ext process API is exposed");
}
So far, you can access the services exposed by other processes through PLAPI, PLMainAPI, PLExtAPI
in the process of the new window.
Service in New Window
In the new window, we can implement any function we want. There are almost no restrictions here, just like developing an WebAPP.
In our demo extension, we get the selected paper, get the path of the PDF document, and then render it to the new window. You can find this part of the code in services/preview-service.ts
.