Skip to content

Commit 43ee8e9

Browse files
committed
initial work on supporting custom python environment
1 parent 3a3b1ae commit 43ee8e9

File tree

5 files changed

+259
-43
lines changed

5 files changed

+259
-43
lines changed

src/browser/app.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,14 @@ class Application extends React.Component<Application.IProps, Application.IState
231231
}
232232

233233
private _renderSplash(): JSX.Element {
234-
return (
235-
<div className='jpe-content'>
236-
<SplashScreen ref='splash' uiState={this.props.options.uiState} finished={() => {
237-
this.setState({renderSplash: this._renderEmpty}); }
238-
} />
239-
</div>
240-
);
234+
return null;
235+
// return (
236+
// <div className='jpe-content'>
237+
// <SplashScreen ref='splash' uiState={this.props.options.uiState} finished={() => {
238+
// this.setState({renderSplash: this._renderEmpty}); }
239+
// } />
240+
// </div>
241+
// );
241242
}
242243

243244
private _renderErrorScreen(): JSX.Element {

src/browser/extensions/desktop-extension/index.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ import {
1111
IMainMenu
1212
} from '@jupyterlab/mainmenu';
1313

14+
import { IStatusBar } from '@jupyterlab/statusbar';
15+
1416
import {
1517
ILabShell,
1618
JupyterFrontEndPlugin
1719
} from '@jupyterlab/application';
1820

21+
import {
22+
Widget
23+
} from '@lumino/widgets';
24+
1925
import {
2026
toArray
2127
} from '@lumino/algorithm';
@@ -41,10 +47,45 @@ async function waitForOriginUpdate(): Promise<void> {
4147
});
4248
}
4349

50+
class StatusBarItem extends Widget {
51+
static createNode(): HTMLElement {
52+
let node = document.createElement('div');
53+
let content = document.createElement('div');
54+
let button = document.createElement('button');
55+
button.textContent = 'Python Environment';
56+
button.onclick = () => {
57+
asyncRemoteRenderer.runRemoteMethod(IAppRemoteInterface.showPythonPathSelector, void(0));
58+
};
59+
content.appendChild(button);
60+
node.appendChild(content);
61+
return node;
62+
}
63+
64+
constructor(name: string) {
65+
super({ node: StatusBarItem.createNode() });
66+
this.setFlag(Widget.Flag.DisallowLayout);
67+
this.addClass('content');
68+
this.addClass(name.toLowerCase());
69+
this.title.label = name;
70+
this.title.closable = true;
71+
this.title.caption = `Long description for: ${name}`;
72+
}
73+
74+
get button(): HTMLButtonElement {
75+
return this.node.getElementsByTagName('button')[0] as HTMLButtonElement;
76+
}
77+
78+
protected onActivateRequest(msg: any): void {
79+
if (this.isAttached) {
80+
this.button.focus();
81+
}
82+
}
83+
}
84+
4485
const desktopExtension: JupyterFrontEndPlugin<void> = {
4586
id: 'jupyterlab-desktop.extensions.desktop',
46-
requires: [ICommandPalette, IMainMenu, ILabShell],
47-
activate: (app: ElectronJupyterLab, palette: ICommandPalette, menu: IMainMenu, labShell: ILabShell) => {
87+
requires: [ICommandPalette, IMainMenu, ILabShell, IStatusBar],
88+
activate: (app: ElectronJupyterLab, palette: ICommandPalette, menu: IMainMenu, labShell: ILabShell, statusBar: IStatusBar) => {
4889
app.commands.addCommand('check-for-updates', {
4990
label: 'Check for Updates…',
5091
execute: () => {
@@ -64,6 +105,21 @@ const desktopExtension: JupyterFrontEndPlugin<void> = {
64105
{ command: 'check-for-updates' }
65106
], 20);
66107

108+
const statusItem = new StatusBarItem('Python');
109+
110+
statusBar.registerStatusItem('jupyterlab-desktop-environment', {
111+
item: statusItem,
112+
align: 'left'
113+
});
114+
115+
asyncRemoteRenderer.runRemoteMethod(IAppRemoteInterface.getCurrentPythonPath, void(0)).then((path) => {
116+
statusItem.button.textContent = path === '' ? 'Python' : path;
117+
});
118+
119+
asyncRemoteRenderer.onRemoteEvent(IAppRemoteInterface.pythonPathChangedEvent, (newPath) => {
120+
statusItem.button.textContent = newPath;
121+
});
122+
67123
const recreateLaunchers = () => {
68124
const mainWidgets = toArray(labShell.widgets('main'));
69125
const launchers = mainWidgets.filter(

src/main/app.ts

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Distributed under the terms of the Modified BSD License.
33

44
import {
5-
app, BrowserWindow, ipcMain, shell
5+
app, BrowserWindow, dialog, ipcMain, shell
66
} from 'electron';
77

88
import {
@@ -40,6 +40,8 @@ interface IApplication {
4040
* Force the application service to write data to the disk.
4141
*/
4242
saveState: (service: IStatefulService, data: JSONValue) => Promise<void>;
43+
44+
getPythonPath(): Promise<string>;
4345
}
4446

4547
/**
@@ -97,6 +99,18 @@ namespace IAppRemoteInterface {
9799
let openDevTools: AsyncRemote.IMethod<void, void> = {
98100
id: 'JupyterLabDesktop-open-dev-tools'
99101
};
102+
export
103+
let getCurrentPythonPath: AsyncRemote.IMethod<void, string> = {
104+
id: 'JupyterLabDesktop-get-python-path'
105+
};
106+
export
107+
let showPythonPathSelector: AsyncRemote.IMethod<void, void> = {
108+
id: 'JupyterLabDesktop-select-python-path'
109+
};
110+
export
111+
let pythonPathChangedEvent: AsyncRemote.IEvent<string> = {
112+
id: 'JupyterLabDesktop-python-path-changed'
113+
};
100114
}
101115

102116
export
@@ -122,13 +136,17 @@ class JupyterApplication implements IApplication, IStatefulService {
122136
});
123137

124138
this._applicationState = {
125-
checkForUpdatesAutomatically: true
139+
checkForUpdatesAutomatically: true,
140+
pythonPath: '',
126141
};
127142

128143
this.registerStatefulService(this)
129144
.then((state: JupyterApplication.IState) => {
130145
if (state) {
131146
this._applicationState = state;
147+
if (this._applicationState.pythonPath === undefined) {
148+
this._applicationState.pythonPath = '';
149+
}
132150
}
133151

134152
if (this._applicationState.checkForUpdatesAutomatically) {
@@ -139,6 +157,14 @@ class JupyterApplication implements IApplication, IStatefulService {
139157
});
140158
}
141159

160+
getPythonPath(): Promise<string> {
161+
return new Promise<string>((resolve, _reject) => {
162+
this._appState.then((state: JSONObject) => {
163+
resolve(this._applicationState.pythonPath);
164+
});
165+
});
166+
}
167+
142168
registerStatefulService(service: IStatefulService): Promise<JSONValue> {
143169
this._services.push(service);
144170

@@ -266,6 +292,24 @@ class JupyterApplication implements IApplication, IStatefulService {
266292
shell.openExternal('https://github.com/jupyterlab/jupyterlab-desktop/releases');
267293
});
268294

295+
ipcMain.on('select-python-path', (event) => {
296+
dialog.showOpenDialog({
297+
properties: ['openFile', 'showHiddenFiles', 'noResolveAliases'],
298+
buttonLabel: 'Use Path'
299+
}).then(({filePaths}) => {
300+
if (filePaths) {
301+
event.sender.send('custom-python-path-selected', filePaths[0]);
302+
}
303+
});
304+
});
305+
306+
ipcMain.on('set-python-path', (event, path) => {
307+
this._applicationState.pythonPath = path;
308+
app.relaunch();
309+
app.quit();
310+
});
311+
312+
269313
asyncRemoteMain.registerRemoteMethod(IAppRemoteInterface.checkForUpdates,
270314
(): Promise<void> => {
271315
this._checkForUpdates('always');
@@ -277,6 +321,18 @@ class JupyterApplication implements IApplication, IStatefulService {
277321
this._window.webContents.openDevTools();
278322
return Promise.resolve();
279323
});
324+
325+
asyncRemoteMain.registerRemoteMethod(IAppRemoteInterface.getCurrentPythonPath,
326+
(): Promise<string> => {
327+
return this.getPythonPath();
328+
});
329+
330+
asyncRemoteMain.registerRemoteMethod(IAppRemoteInterface.showPythonPathSelector,
331+
(): Promise<void> => {
332+
// asyncRemoteMain.emitRemoteEvent(IAppRemoteInterface.pythonPathChangedEvent, 'new-path', this._window.webContents);
333+
this._showPythonSelectorDialog();
334+
return Promise.resolve();
335+
});
280336
}
281337

282338
private _showUpdateDialog(type: 'updates-available' | 'error' | 'no-updates') {
@@ -326,6 +382,100 @@ class JupyterApplication implements IApplication, IStatefulService {
326382
dialog.loadURL(`data:text/html;charset=utf-8,${pageSource}`);
327383
}
328384

385+
private _showPythonSelectorDialog() {
386+
const dialog = new BrowserWindow({
387+
title: 'Set Python Environment',
388+
width: 600,
389+
height: 200,
390+
resizable: false,
391+
parent: this._window,
392+
modal: true,
393+
webPreferences: {
394+
nodeIntegration: true,
395+
enableRemoteModule: true,
396+
contextIsolation: false
397+
}
398+
});
399+
dialog.setMenuBarVisibility(false);
400+
401+
const bundledPythonPath = '';
402+
const pythonPath = this._applicationState.pythonPath;
403+
let useBundledPythonPath = false;
404+
if (pythonPath === '' || pythonPath === bundledPythonPath) {
405+
useBundledPythonPath = true;
406+
}
407+
408+
const pageSource = `
409+
<body style="background: rgba(238,238,238,1); font-size: 13px; font-family: Helvetica, Arial, sans-serif; padding: 20px;">
410+
<style>.row {display: flex; margin-bottom: 10px; }</style>
411+
<div style="height: 100%; display: flex;flex-direction: column; justify-content: space-between;">
412+
<div class="row">
413+
<b>Set Python Environment</b>
414+
</div>
415+
<div>
416+
<div class="row">
417+
<input type="radio" id="bundled" name="env_type" value="bundled" ${useBundledPythonPath ? 'checked' : ''} onchange="handleEnvTypeChange(this);">
418+
<label for="bundled">Use bundled Python environment</label>
419+
</div>
420+
<div class="row">
421+
<input type="radio" id="custom" name="env_type" value="custom" ${!useBundledPythonPath ? 'checked' : ''} onchange="handleEnvTypeChange(this);">
422+
<label for="custom">Use custom Python environment</label>
423+
</div>
424+
425+
<div class="row">
426+
<div style="flex-grow: 1;">
427+
<input type="text" id="python-path" value="${pythonPath}" readonly style="width: 100%;"></input>
428+
</div>
429+
<div>
430+
<button id='select-python-path' onclick='handleSelectPythonPath(this);'>Select Python path</button>
431+
</div>
432+
</div>
433+
<div class="row" style="justify-content: flex-end;">
434+
<button onclick='handleSave(this);' style='margin-right: 5px;'>Save and restart</button>
435+
<button onclick='handleCancel(this);'>Cancel</button>
436+
</div>
437+
</div>
438+
</div>
439+
440+
<script>
441+
const ipcRenderer = require('electron').ipcRenderer;
442+
let pythonPath = '';
443+
const bundledRadio = document.getElementById('bundled');
444+
const pythonPathInput = document.getElementById('python-path');
445+
const selectPythonPathButton = document.getElementById('select-python-path');
446+
447+
function handleSelectPythonPath(el) {
448+
ipcRenderer.send('select-python-path');
449+
}
450+
451+
function handleReleasesLink(el) {
452+
ipcRenderer.send('launch-installer-download-page');
453+
}
454+
455+
function handleEnvTypeChange() {
456+
pythonPathInput.disabled = bundledRadio.checked;
457+
selectPythonPathButton.disabled = bundledRadio.checked;
458+
}
459+
460+
function handleSave(el) {
461+
ipcRenderer.send('set-python-path', bundledRadio.checked ? '' : pythonPathInput.value);
462+
}
463+
464+
function handleCancel(el) {
465+
window.close();
466+
}
467+
468+
ipcRenderer.on('custom-python-path-selected', (event, path) => {
469+
pythonPathInput.value = path;
470+
});
471+
472+
handleEnvTypeChange();
473+
</script>
474+
</body>
475+
`;
476+
dialog.loadURL(`data:text/html;charset=utf-8,${pageSource}`);
477+
}
478+
329479
private _checkForUpdates(showDialog: 'on-new-version' | 'always') {
330480
fetch('https://github.com/jupyterlab/jupyterlab-desktop/releases/latest/download/latest.yml').then(async (response) => {
331481
try {
@@ -389,6 +539,7 @@ namespace JupyterApplication {
389539
export
390540
interface IState extends JSONObject {
391541
checkForUpdatesAutomatically?: boolean;
542+
pythonPath?: string;
392543
}
393544
}
394545

0 commit comments

Comments
 (0)