Electron-256 (Changed the implementation from modal to electron window) (#308)

1. Completed functionality
2. Refactored and added code comments
3. Fixed some style issues
4. Added some safety checks
5. Updated Window size
6. Added some safety checks
7. Fixed some keyboard interaction
8. Fixed styles for Windows OS
9. Added a functionality to place screen picker based on the event sender
10. Updated the code to open the screen picker on top of the focused window instead of finding a ref using window name
11. Added a HTML content to display error message
12. Updated window title
13. Added missing return
14. Changed the method name from `openScreenPickerWindowWindow` to `openScreenPickerWindow`
15. Fixed a typo and added code comment
16. Changes as per PR review
17. Created Enum for key code
18. Updated for loop to for..of loop
19. Updated colors from hex to rgba
20. Setting cursor property as pointer for cancel button and item-container
21. Made window draggable
22. Changed font-family to system fonts
23. Added box shadow for buttons
24. Added a new API to support backward compatibility and deprecated the existing one
25. Fixed the condition prevent a new window from being opened if there is an existing window
This commit is contained in:
Kiran Niranjan 2018-03-09 15:31:44 +05:30 committed by Vishwas Shashidhar
parent 036077ba8e
commit 08c25e4b58
9 changed files with 777 additions and 4 deletions

View File

@ -67,7 +67,7 @@
<hr> <hr>
<p>Get Media Sources:</p> <p>Get Media Sources:</p>
<button id='get-sources'>get entire desktop</button> <button id='get-sources'>Open screen picker</button>
<br> <br>
<video id='video'></video> <video id='video'></video>
@ -193,14 +193,14 @@
var getSources = document.getElementById('get-sources'); var getSources = document.getElementById('get-sources');
getSources.addEventListener('click', function() { getSources.addEventListener('click', function() {
ssf.getMediaSources({types: ['window', 'screen']}, function(error, sources) { ssf.getMediaSource({types: ['window', 'screen']}, function(error, source) {
if (error) throw error if (error) throw error
navigator.webkitGetUserMedia({ navigator.webkitGetUserMedia({
audio: false, audio: false,
video: { video: {
mandatory: { mandatory: {
chromeMediaSource: 'desktop', chromeMediaSource: 'desktop',
chromeMediaSourceId: sources[0].id, chromeMediaSourceId: source.id,
minWidth: 1280, minWidth: 1280,
maxWidth: 1280, maxWidth: 1280,
minHeight: 720, minHeight: 720,

View File

@ -0,0 +1,96 @@
'use strict';
// This code provides equivalent of desktopCapturer.getSources that works in
// a sandbox renderer. see: https://electron.atom.io/docs/api/desktop-capturer/
//
// The code here is not entirely kosher/stable as it is using private ipc
// events. The code was take directly from electron.asar file provided in
// prebuilt node module. Note: the slight difference here is the thumbnail
// returns a base64 encoded image rather than a electron nativeImage.
//
// Until electron provides access to desktopCapturer in a sandboxed
// renderer process, this will have to do. See github issue posted here to
// electron: https://github.com/electron/electron/issues/9312
const { ipcRenderer, remote } = require('electron');
const apiEnums = require('../enums/api.js');
const apiCmds = apiEnums.cmds;
const apiName = apiEnums.apiName;
const { isWindowsOS } = require('../utils/misc');
const USER_CANCELLED = 'User Cancelled';
let nextId = 0;
let includes = [].includes;
function getNextId() {
return ++nextId;
}
/**
* Checks if the options and their types are valid
* @param options |options.type| can not be empty and has to include 'window' or 'screen'.
* @returns {boolean}
*/
function isValid(options) {
return ((options !== null ? options.types : undefined) !== null) && Array.isArray(options.types);
}
/**
* Gets the sources for capturing screens / windows
* @param options
* @param callback
* @returns {*}
*/
function getSource(options, callback) {
let captureScreen, captureWindow, id;
if (!isValid(options)) {
return callback(new Error('Invalid options'));
}
captureWindow = includes.call(options.types, 'window');
captureScreen = includes.call(options.types, 'screen');
let updatedOptions = options;
if (!updatedOptions.thumbnailSize) {
updatedOptions.thumbnailSize = {
width: 150,
height: 150
};
}
if (isWindowsOS) {
/**
* Sets the captureWindow to false if Desktop composition
* is disabled otherwise true
*
* Setting captureWindow to false returns only screen sources
* @type {boolean}
*/
captureWindow = remote.systemPreferences.isAeroGlassEnabled();
}
id = getNextId();
ipcRenderer.send('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', captureWindow, captureScreen, updatedOptions.thumbnailSize, id);
return ipcRenderer.once('ELECTRON_RENDERER_DESKTOP_CAPTURER_RESULT_' + id, function(event, sources) {
ipcRenderer.send(apiName, {
cmd: apiCmds.openScreenPickerWindow,
sources: sources,
id: id
});
function successCallback(e, source) {
// Cleaning up the event listener to prevent memory leaks
if (!source) {
ipcRenderer.removeListener('start-share' + id, func);
return callback(new Error(USER_CANCELLED));
}
return callback(null, source);
}
const func = successCallback.bind(this);
ipcRenderer.once('start-share' + id, func);
});
}
module.exports = getSource;

View File

@ -87,4 +87,8 @@ function getSources(options, callback) {
}); });
} }
/**
* @deprecated instead use getSource
* @type {getSources}
*/
module.exports = getSources; module.exports = getSources;

140
js/desktopCapturer/index.js Normal file
View File

@ -0,0 +1,140 @@
'use strict';
const electron = require('electron');
const BrowserWindow = electron.BrowserWindow;
const ipc = electron.ipcMain;
const path = require('path');
const fs = require('fs');
const log = require('../log.js');
const logLevels = require('../enums/logLevels.js');
const { isMac, isWindowsOS } = require('./../utils/misc.js');
let screenPickerWindow;
let preloadWindow;
let eventId;
let windowConfig = {
width: 580,
height: isMac ? 519 : 523,
show: false,
modal: true,
frame: false,
autoHideMenuBar: true,
resizable: false,
alwaysOnTop: true,
webPreferences: {
preload: path.join(__dirname, 'renderer.js'),
sandbox: true,
nodeIntegration: false
}
};
/**
* method to get the HTML template path
* @returns {string}
*/
function getTemplatePath() {
let templatePath = path.join(__dirname, 'screen-picker.html');
try {
fs.statSync(templatePath).isFile();
} catch (err) {
log.send(logLevels.ERROR, 'screen-picker: Could not find template ("' + templatePath + '").');
}
return 'file://' + templatePath;
}
/**
* Creates the screen picker window
* @param eventSender {RTCRtpSender} - event sender window object
* @param sources {Array} - list of object which has screens and applications
* @param id {Number} - event emitter id
*/
function openScreenPickerWindow(eventSender, sources, id) {
// prevent a new window from being opened if there is an
// existing window / there is no event sender
if (!eventSender || screenPickerWindow) {
return;
}
// Screen picker will always be placed on top of the focused window
const focusedWindow = BrowserWindow.getFocusedWindow();
// As screen picker is an independent window this will make sure
// it will open screen picker window center of the focused window
if (focusedWindow) {
const { x, y, width, height } = focusedWindow.getBounds();
if (x !== undefined && y !== undefined) {
const windowWidth = Math.round(width * 0.5);
const windowHeight = Math.round(height * 0.5);
// Calculating the center of the parent window
// to place the configuration window
const centerX = x + width / 2.0;
const centerY = y + height / 2.0;
windowConfig.x = Math.round(centerX - (windowWidth / 2.0));
windowConfig.y = Math.round(centerY - (windowHeight / 2.0));
}
}
// Store the window ref to send event
preloadWindow = eventSender;
eventId = id;
screenPickerWindow = new BrowserWindow(windowConfig);
screenPickerWindow.setVisibleOnAllWorkspaces(true);
screenPickerWindow.loadURL(getTemplatePath());
screenPickerWindow.once('ready-to-show', () => {
screenPickerWindow.show();
});
screenPickerWindow.webContents.on('did-finish-load', () => {
screenPickerWindow.webContents.send('desktop-capturer-sources', sources, isWindowsOS);
});
screenPickerWindow.on('close', () => {
destroyWindow();
});
screenPickerWindow.on('closed', () => {
destroyWindow();
});
}
/**
* Destroys a window
*/
function destroyWindow() {
// sending null will clean up the event listener
startScreenShare(null);
screenPickerWindow = null;
}
/**
* Sends an event to a specific with the selected source
* @param source {Object} - User selected source
*/
function startScreenShare(source) {
if (preloadWindow && !preloadWindow.isDestroyed()) {
preloadWindow.send('start-share' + eventId, source);
}
}
// Emitted when user has selected a source and press the share button
ipc.on('share-selected-source', (event, source) => {
startScreenShare(source);
});
// Emitted when user closes the screen picker window
ipc.on('close-screen-picker', () => {
if (screenPickerWindow && !screenPickerWindow.isDestroyed()) {
screenPickerWindow.close();
}
});
module.exports = {
openScreenPickerWindow
};

View File

@ -0,0 +1,268 @@
'use strict';
const { ipcRenderer } = require('electron');
const screenRegExp = new RegExp(/^Screen \d+$/gmi);
// All the required Keyboard keyCode events
const keyCodeEnum = Object.freeze({
pageDown: 34,
rightArrow: 39,
pageUp: 33,
leftArrow: 37,
homeKey: 36,
upArrow: 38,
endKey: 35,
arrowDown: 40,
enterKey: 13,
escapeKey: 27
});
let availableSources;
let selectedSource;
let currentIndex = -1;
document.addEventListener('DOMContentLoaded', () => {
renderDom();
});
/**
* Method that renders application data
*/
function renderDom() {
const applicationTab = document.getElementById('application-tab');
const screenTab = document.getElementById('screen-tab');
const share = document.getElementById('share');
const cancel = document.getElementById('cancel');
const xButton = document.getElementById('x-button');
// Event listeners
xButton.addEventListener('click', () => {
closeScreenPickerWindow();
}, false);
share.addEventListener('click', () => {
startShare();
}, false);
cancel.addEventListener('click', () => {
closeScreenPickerWindow();
}, false);
screenTab.addEventListener('click', () => {
updateShareButtonText('Select Screen');
}, false);
applicationTab.addEventListener('click', () => {
updateShareButtonText('Select Application');
}, false);
document.addEventListener('keyup', handleKeyUpPress.bind(this), true);
}
/**
* Event emitted by main process with a list of available
* Screens and Applications
*/
ipcRenderer.on('desktop-capturer-sources', (event, sources, isWindowsOS) => {
if (!Array.isArray(sources) && typeof isWindowsOS !== 'boolean') {
return;
}
availableSources = sources;
if (isWindowsOS) {
document.body.classList.add('window-border');
}
const screenContent = document.getElementById('screen-contents');
const applicationContent = document.getElementById('application-contents');
const applicationTab = document.getElementById('applications');
const screenTab = document.getElementById('screens');
let hasScreens = false;
let hasApplications = false;
for (let source of sources) {
screenRegExp.lastIndex = 0;
if (source.name === 'Entire screen' || screenRegExp.exec(source.name)) {
source.fileName = 'fullscreen';
screenContent.appendChild(createItem(source));
hasScreens = true;
} else {
source.fileName = null;
applicationContent.appendChild(createItem(source));
hasApplications = true;
}
}
if (!hasScreens && !hasApplications) {
const errorContent = document.getElementById('error-content');
const mainContent = document.getElementById('main-content');
errorContent.style.display = 'block';
mainContent.style.display = 'none';
}
if (hasApplications) {
applicationTab.classList.remove('hidden');
}
if (hasScreens) {
screenTab.classList.remove('hidden');
}
});
function startShare() {
if (selectedSource && selectedSource.id) {
ipcRenderer.send('share-selected-source', selectedSource);
closeScreenPickerWindow();
}
}
/**
* Creates DOM elements and injects data
* @param source
* @returns {HTMLDivElement}
*/
function createItem(source) {
const itemContainer = document.createElement("div");
const sectionBox = document.createElement("div");
const imageTag = document.createElement("img");
const titleContainer = document.createElement("div");
// Added class names to the dom elements
itemContainer.classList.add('item-container');
sectionBox.classList.add('screen-section-box');
imageTag.classList.add('img-wrapper');
titleContainer.classList.add('screen-source-title');
// Inject data to the dom element
imageTag.src = source.thumbnail;
titleContainer.innerText = source.name;
sectionBox.appendChild(imageTag);
itemContainer.id = source.id;
itemContainer.appendChild(sectionBox);
itemContainer.appendChild(titleContainer);
itemContainer.addEventListener('click', updateUI.bind(this, source, itemContainer), false);
return itemContainer;
}
/**
* When ever user select a source store it and update the UI
* @param source
* @param itemContainer
*/
function updateUI(source, itemContainer) {
selectedSource = source;
let shareButton = document.getElementById('share');
shareButton.className = 'share-button';
highlightSelectedSource();
itemContainer.classList.add('selected');
shareButton.innerText = 'Share'
}
/**
* Loops through the items and removes
* the selected class property
*/
function highlightSelectedSource() {
let items = document.getElementsByClassName('item-container');
for (const item of items) {
item.classList.remove('selected');
}
}
/**
* Method that updates the share button
* text based on the content type
* @param text
*/
function updateShareButtonText(text) {
let shareButton = document.getElementById('share');
if (shareButton && shareButton.classList[0] === 'share-button-disable') {
shareButton.innerText = text;
}
}
/**
* Method handles used key up event
* @param e
*/
function handleKeyUpPress(e) {
const keyCode = e.keyCode || e.which;
switch (keyCode) {
case keyCodeEnum.pageDown:
case keyCodeEnum.rightArrow:
updateSelectedSource(1);
break;
case keyCodeEnum.pageUp:
case keyCodeEnum.leftArrow:
updateSelectedSource(-1);
break;
case keyCodeEnum.homeKey:
if (currentIndex !== 0) {
updateSelectedSource(0);
}
break;
case keyCodeEnum.upArrow:
updateSelectedSource(-2);
break;
case keyCodeEnum.endKey:
if (currentIndex !== availableSources.length - 1) {
updateSelectedSource(availableSources.length - 1);
}
break;
case keyCodeEnum.arrowDown:
updateSelectedSource(2);
break;
case keyCodeEnum.enterKey:
startShare();
break;
case keyCodeEnum.escapeKey:
closeScreenPickerWindow();
break;
default:
break;
}
}
/**
* Updates UI based on the key press
* @param index
*/
function updateSelectedSource(index) {
let selectedElement = document.getElementsByClassName('selected')[0];
if (selectedElement) {
currentIndex = availableSources.findIndex((source) => {
return source.id === selectedElement.id
});
}
// Find the next item to be selected
let nextIndex = (currentIndex + index + availableSources.length) % availableSources.length;
if (availableSources[nextIndex] && availableSources[nextIndex].id) {
let item = document.getElementById(availableSources[nextIndex] ? availableSources[nextIndex].id : "");
if (item) {
// Method that stores and update the selected source
updateUI(availableSources[nextIndex], item);
}
}
}
/**
* Closes the screen picker window
*/
function closeScreenPickerWindow() {
document.removeEventListener('keyUp', handleKeyUpPress.bind(this), true);
ipcRenderer.send('close-screen-picker');
}

View File

@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Screen Picker</title>
<style>
@font-face {
font-family: system;
font-style: normal;
src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"), local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Light"), local("Roboto-Light"), local("DroidSans"), local("Tahoma");
}
body {
margin: 0 auto;
font-family: "system";
overflow: hidden;
}
.window-border {
border: 2px solid rgba(68,68,68, 1);
}
.content {
margin: 0 auto;
background: rgba(249,249,250, 1);
text-align: center;
height: 100%;
overflow: hidden;
}
.custom-window-title {
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: space-between;
padding: 16px 16px 30px;
font-size: 1.2rem;
line-height: 1.3;
text-align: left;
-webkit-user-select: none;
-webkit-app-region: drag;
}
#x-button {
width: 20px;
color: rgba(221, 221, 221, 1);
}
span {
font-style: normal;
margin: 0 0 0 4px;
font-size: 1.2rem;
min-height: 13px;
}
input {
display: none;
}
label {
display: inline-block;
padding: 5px 25px;
font-size: 0.8em;
letter-spacing: 1px;
text-align: center;
color: rgba(187,187,187, 1);
text-transform: uppercase;
border: 1px solid transparent;
}
section {
display: none;
height: 331px;
overflow-y: scroll;
overflow-x: hidden;
padding: 20px 0 0;
border-top: 1px solid rgba(221, 221, 221, 1);
flex-flow: row wrap;
text-align: center;
justify-content: center;
}
input:checked + label {
color: rgba(61,162,253, 1);
border-bottom: 4px solid rgba(61,162,253, 1);
}
#screen-tab:checked ~ #screen-contents,
#application-tab:checked ~ #application-contents {
display: flex;
}
.item-container {
background: rgba(255,255,255, 1);
border-radius: 2px;
display: inline-block;
height: 150px;
margin: 5px;
position: relative;
width: 40%;
border: 1px solid rgba(0, 0, 0, .1);
}
.item-container:hover {
box-shadow: 0 1px 5px rgba(0, 0, 0, .1), 0 5px 5px rgba(0, 0, 0, .1);
cursor: pointer;
}
.screen-section-box {
-webkit-transition: all 1.0s ease;
width: 100%;
height: 100px;
overflow: hidden;
margin: 10px auto;
}
.img-wrapper {
max-height: 200px;
width: auto;
}
.screen-source-title {
white-space: nowrap;
width: 70%;
overflow: hidden;
font-size: 0.8rem;
color: grey;
margin: auto;
text-overflow: ellipsis;
}
.selected {
border-color: rgba(61,162,253, 1);
border-bottom-style: solid;
}
footer {
padding: 10px 16px;
border-top: 1px solid rgba(0, 0, 0, 0.10);
display: flex;
align-items: center;
justify-content: flex-end;
}
button {
box-shadow: none;
border: none;
border-radius: 20px;
font-size: 0.8rem;
text-align: center;
padding: 8px 32px;
margin: 8px 8px 8px 0;
display: inline-block;
text-decoration: none;
line-height: 12px;
}
button:focus {
box-shadow: 0 0 10px rgba(61, 162, 253, 1);
outline: none;
}
.cancel-button {
color: rgba(0, 0, 0, 0.38);
background-color: rgba(255, 255, 255, 0);
text-transform: uppercase;
padding: 8px 16px;
}
.cancel-button:hover {
background-color: rgba(255, 255, 255, 0);
color: black;
cursor: pointer;
}
.share-button {
background-color: rgba(61,162,253, 1);
color: rgba(255,255,255, 1);
cursor: pointer;
text-transform: uppercase;
box-shadow: none;
}
.share-button-disable {
background-color: rgba(221, 221, 221, 1);
color: rgba(148, 148, 148, 1);
text-transform: uppercase;
cursor: default;
box-shadow: none;
}
.hidden {
display: none;
}
#error-content {
display: none;
padding: 180px 0 180px;
border-top: 1px solid rgba(221, 221, 221, 1);
}
#error-content span {
color: rgba(0, 0, 0, 0.38);
}
</style>
</head>
<body>
<div class="content">
<div class="custom-window-title">
<span>Choose what you&#39;d like to share</span>
<div id="x-button">
<div class="content-button">
<i>
<svg viewBox="0 0 48 48" fill="grey">
<path d="M39.4,33.8L31,25.4c-0.4-0.4-0.9-0.9-1.4-1.4c0.5-0.5,1-1,1.4-1.4l8.4-8.4c0.8-0.8,0.8-2,0-2.8l-2.8-2.8 c-0.8-0.8-2-0.8-2.8,0L25.4,17c-0.4,0.4-0.9,0.9-1.4,1.4c-0.5-0.5-1-1-1.4-1.4l-8.4-8.4c-0.8-0.8-2-0.8-2.8,0l-2.8,2.8 c-0.8,0.8-0.8,2,0,2.8l8.4,8.4c0.4,0.4,0.9,0.9,1.4,1.4c-0.5,0.5-1,1-1.4,1.4l-8.4,8.4c-0.8,0.8-0.8,2,0,2.8l2.8,2.8 c0.8,0.8,2,0.8,2.8,0l8.4-8.4c0.4-0.4,0.9-0.9,1.4-1.4c0.5,0.5,1,1,1.4,1.4l8.4,8.4c0.8,0.8,2,0.8,2.8,0l2.8-2.8 C40.2,35.8,40.2,34.6,39.4,33.8z"></path>
</svg>
</i>
</div>
</div>
</div>
<div id="error-content">
<span id="error-message">No screens or applications are currently available.</span>
</div>
<div id="main-content">
<input id="screen-tab" type="radio" name="tabs" checked>
<label id="screens" for="screen-tab" class="hidden">Screens</label>
<input id="application-tab" type="radio" name="tabs">
<label id="applications" for="application-tab" class="hidden">Applications</label>
<section id="screen-contents">
</section>
<section id="application-contents">
</section>
</div>
<footer>
<button id="cancel" class="cancel-button">Cancel</button>
<button id="share" class="share-button-disable">Select Screen</button>
</footer>
</div>
</body>
</html>

View File

@ -17,7 +17,8 @@ const cmds = keyMirror({
registerActivityDetection: null, registerActivityDetection: null,
showNotificationSettings: null, showNotificationSettings: null,
sanitize: null, sanitize: null,
bringToFront: null bringToFront: null,
openScreenPickerWindow: null
}); });
module.exports = { module.exports = {

View File

@ -16,6 +16,7 @@ const configureNotification = require('./notify/settings/configure-notification-
const { bringToFront } = require('./bringToFront.js'); const { bringToFront } = require('./bringToFront.js');
const eventEmitter = require('./eventEmitter'); const eventEmitter = require('./eventEmitter');
const { isMac } = require('./utils/misc'); const { isMac } = require('./utils/misc');
const { openScreenPickerWindow } = require('./desktopCapturer');
const apiEnums = require('./enums/api.js'); const apiEnums = require('./enums/api.js');
const apiCmds = apiEnums.cmds; const apiCmds = apiEnums.cmds;
@ -135,6 +136,11 @@ electron.ipcMain.on(apiName, (event, arg) => {
bringToFront(arg.windowName, arg.reason); bringToFront(arg.windowName, arg.reason);
} }
break; break;
case apiCmds.openScreenPickerWindow:
if (Array.isArray(arg.sources) && typeof arg.id === 'number') {
openScreenPickerWindow(event.sender, arg.sources, arg.id);
}
break;
default: default:
} }

View File

@ -18,6 +18,7 @@ const apiEnums = require('../enums/api.js');
const apiCmds = apiEnums.cmds; const apiCmds = apiEnums.cmds;
const apiName = apiEnums.apiName; const apiName = apiEnums.apiName;
const getMediaSources = require('../desktopCapturer/getSources'); const getMediaSources = require('../desktopCapturer/getSources');
const getMediaSource = require('../desktopCapturer/getSource');
require('../downloadManager'); require('../downloadManager');
@ -260,9 +261,22 @@ function createAPI() {
* a sandboxed renderer process. * a sandboxed renderer process.
* see: https://electron.atom.io/docs/api/desktop-capturer/ * see: https://electron.atom.io/docs/api/desktop-capturer/
* for interface: see documentation in desktopCapturer/getSources.js * for interface: see documentation in desktopCapturer/getSources.js
*
* @deprecated instead use getMediaSource
*/ */
getMediaSources: getMediaSources, getMediaSources: getMediaSources,
/**
* Implements equivalent of desktopCapturer.getSources - that works in
* a sandboxed renderer process.
* see: https://electron.atom.io/docs/api/desktop-capturer/
* for interface: see documentation in desktopCapturer/getSource.js
*
* This opens a window and displays all the desktop sources
* and returns selected source
*/
getMediaSource: getMediaSource,
/** /**
* Opens a modal window to configure notification preference. * Opens a modal window to configure notification preference.
*/ */