windows.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. const { app, BrowserWindow, ipcMain: ipc } = require('electron');
  2. const Settings = require('./settings');
  3. const log = require('./utils/logger').create('Windows');
  4. const EventEmitter = require('events').EventEmitter;
  5. import {
  6. closeWindow,
  7. openWindow,
  8. resetGenericWindow,
  9. reuseGenericWindow
  10. } from './core/ui/actions';
  11. class GenericWindow extends EventEmitter {
  12. constructor(mgr) {
  13. super();
  14. this._mgr = mgr;
  15. this._log = log.create('generic');
  16. this.isPrimary = false;
  17. this.type = 'generic';
  18. this.isPopup = true;
  19. this.ownerId = null;
  20. this.isAvailable = true;
  21. this.actingType = null;
  22. this._log.debug('Creating generic window');
  23. let electronOptions = this._mgr.getDefaultOptionsForType('generic');
  24. this.window = new BrowserWindow(electronOptions);
  25. // set Accept_Language header
  26. this.session = this.window.webContents.session;
  27. this.session.setUserAgent(this.session.getUserAgent(), Settings.language);
  28. this.webContents = this.window.webContents;
  29. this.webContents.once('did-finish-load', () => {
  30. this._log.debug(`Content loaded, id: ${this.id}`);
  31. this.emit('ready');
  32. });
  33. // prevent dropping files
  34. this.webContents.on('will-navigate', e => e.preventDefault());
  35. this.window.once('closed', () => {
  36. this._log.debug('Closed');
  37. this.emit('closed');
  38. });
  39. this.window.on('close', e => {
  40. // Preserve window unless quitting Mist
  41. if (store.getState().ui.appQuit) {
  42. return this.emit('close', e);
  43. }
  44. e.preventDefault();
  45. this.hide();
  46. });
  47. this.window.on('show', e => this.emit('show', e));
  48. this.window.on('hide', e => this.emit('hide', e));
  49. this.load(`${global.interfacePopupsUrl}#generic`);
  50. }
  51. load(url) {
  52. this._log.debug(`Load URL: ${url}`);
  53. this.window.loadURL(url);
  54. }
  55. send() {
  56. this._log.trace('Sending data', arguments);
  57. this.webContents.send.apply(this.webContents, arguments);
  58. }
  59. hide() {
  60. this._log.debug('Hide');
  61. this.window.hide();
  62. this.send('uiAction_switchTemplate', 'generic');
  63. this.actingType = null;
  64. this.isAvailable = true;
  65. this.emit('hidden');
  66. store.dispatch(resetGenericWindow());
  67. }
  68. show() {
  69. this._log.debug('Show');
  70. this.window.show();
  71. }
  72. close() {
  73. this._log.debug('Avoiding close of generic window');
  74. this.hide();
  75. }
  76. reuse(type, options, callback) {
  77. this.isAvailable = false;
  78. this.actingType = type;
  79. if (callback) {
  80. this.callback = callback;
  81. }
  82. if (options.ownerId) {
  83. this.ownerId = options.ownerId;
  84. }
  85. if (options.sendData) {
  86. if (_.isString(options.sendData)) {
  87. this.send(options.sendData);
  88. } else if (_.isObject(options.sendData)) {
  89. for (const key in options.sendData) {
  90. if ({}.hasOwnProperty.call(options.sendData, key)) {
  91. this.send(key, options.sendData[key]);
  92. }
  93. }
  94. }
  95. }
  96. this.window.setSize(
  97. options.electronOptions.width,
  98. options.electronOptions.height
  99. );
  100. this.window.setAlwaysOnTop(true, 'floating', 1);
  101. this.send('uiAction_switchTemplate', type);
  102. this.show();
  103. store.dispatch(reuseGenericWindow(type));
  104. }
  105. }
  106. class Window extends EventEmitter {
  107. constructor(mgr, type, opts) {
  108. super();
  109. opts = opts || {};
  110. this._mgr = mgr;
  111. this._log = log.create(type);
  112. this.isPrimary = !!opts.primary;
  113. this.type = type;
  114. this.isPopup = !!opts.isPopup;
  115. this.ownerId = opts.ownerId; // the window which creates this new window
  116. let electronOptions = {
  117. title: Settings.appName,
  118. show: false,
  119. width: 1100,
  120. height: 720,
  121. icon: global.icon,
  122. titleBarStyle: 'hidden-inset', // hidden-inset: more space
  123. backgroundColor: '#F6F6F6',
  124. acceptFirstMouse: true,
  125. darkTheme: true,
  126. webPreferences: {
  127. nodeIntegration: false,
  128. webaudio: true,
  129. webgl: false,
  130. webSecurity: false, // necessary to make routing work on file:// protocol for assets in windows and popups. Not webviews!
  131. textAreasAreResizable: true
  132. }
  133. };
  134. electronOptions = _.deepExtend(electronOptions, opts.electronOptions);
  135. this._log.debug('Creating browser window');
  136. this.window = new BrowserWindow(electronOptions);
  137. // set Accept_Language header
  138. this.session = this.window.webContents.session;
  139. this.session.setUserAgent(this.session.getUserAgent(), Settings.language);
  140. this.webContents = this.window.webContents;
  141. this.webContents.once('did-finish-load', () => {
  142. this.isContentReady = true;
  143. this._log.debug(`Content loaded, id: ${this.id}`);
  144. if (opts.sendData) {
  145. if (_.isString(opts.sendData)) {
  146. this.send(opts.sendData);
  147. } else if (_.isObject(opts.sendData)) {
  148. for (const key in opts.sendData) {
  149. if ({}.hasOwnProperty.call(opts.sendData, key)) {
  150. this.send(key, opts.sendData[key]);
  151. }
  152. }
  153. }
  154. }
  155. if (opts.show) {
  156. this.show();
  157. }
  158. this.emit('ready');
  159. });
  160. // prevent droping files
  161. this.webContents.on('will-navigate', e => {
  162. e.preventDefault();
  163. });
  164. this.window.once('closed', () => {
  165. this._log.debug('Closed');
  166. this.isShown = false;
  167. this.isClosed = true;
  168. this.isContentReady = false;
  169. this.emit('closed');
  170. store.dispatch(closeWindow(this.type));
  171. });
  172. this.window.once('close', e => {
  173. this.emit('close', e);
  174. });
  175. this.window.on('show', e => {
  176. this.emit('show', e);
  177. });
  178. this.window.on('hide', e => {
  179. this.emit('hide', e);
  180. });
  181. if (opts.url) {
  182. this.load(opts.url);
  183. }
  184. }
  185. load(url) {
  186. if (this.isClosed) {
  187. return;
  188. }
  189. this._log.debug(`Load URL: ${url}`);
  190. this.window.loadURL(url);
  191. }
  192. send() {
  193. if (this.isClosed || !this.isContentReady) {
  194. return;
  195. }
  196. this._log.trace('Sending data', arguments);
  197. this.webContents.send.apply(this.webContents, arguments);
  198. }
  199. hide() {
  200. if (this.isClosed) {
  201. return;
  202. }
  203. this._log.debug('Hide');
  204. this.window.hide();
  205. this.isShown = false;
  206. }
  207. show() {
  208. if (this.isClosed) {
  209. return;
  210. }
  211. this._log.debug('Show');
  212. this.window.show();
  213. this.isShown = true;
  214. store.dispatch(openWindow(this.type));
  215. }
  216. close() {
  217. if (this.isClosed) {
  218. return;
  219. }
  220. this._log.debug('Close');
  221. this.window.close();
  222. }
  223. }
  224. class Windows {
  225. constructor() {
  226. this._windows = {};
  227. }
  228. init() {
  229. log.info('Creating commonly-used windows');
  230. this.loading = this.create('loading');
  231. this.generic = this.createGenericWindow();
  232. this.loading.on('show', () => {
  233. this.loading.window.center();
  234. store.dispatch(openWindow('loading'));
  235. });
  236. this.loading.on('hide', () => {
  237. store.dispatch(closeWindow('loading'));
  238. });
  239. // when a window gets initalized it will send us its id
  240. ipc.on('backendAction_setWindowId', event => {
  241. const id = event.sender.id;
  242. log.debug('Set window id', id);
  243. const bwnd = BrowserWindow.fromWebContents(event.sender);
  244. const wnd = _.find(this._windows, w => {
  245. return w.window === bwnd;
  246. });
  247. if (wnd) {
  248. log.trace(`Set window id=${id}, type=${wnd.type}`);
  249. wnd.id = id;
  250. }
  251. });
  252. store.dispatch({ type: '[MAIN]:WINDOWS:INIT_FINISH' });
  253. }
  254. createGenericWindow() {
  255. const wnd = (this._windows.generic = new GenericWindow(this));
  256. return wnd;
  257. }
  258. create(type, opts, callback) {
  259. store.dispatch({
  260. type: '[MAIN]:WINDOW:CREATE_START',
  261. payload: { type }
  262. });
  263. const options = _.deepExtend(
  264. this.getDefaultOptionsForType(type),
  265. opts || {}
  266. );
  267. const existing = this.getByType(type);
  268. if (existing && existing.ownerId === options.ownerId) {
  269. log.debug(
  270. `Window ${type} with owner ${options.ownerId} already existing.`
  271. );
  272. return existing;
  273. }
  274. const category = options.primary ? 'primary' : 'secondary';
  275. log.info(
  276. `Create ${category} window: ${type}, owner: ${options.ownerId ||
  277. 'notset'}`
  278. );
  279. const wnd = (this._windows[type] = new Window(this, type, options));
  280. wnd.on('closed', this._onWindowClosed.bind(this, wnd));
  281. if (callback) {
  282. wnd.callback = callback;
  283. }
  284. store.dispatch({
  285. type: '[MAIN]:WINDOW:CREATE_FINISH',
  286. payload: { type }
  287. });
  288. return wnd;
  289. }
  290. getDefaultOptionsForType(type) {
  291. const mainWebPreferences = {
  292. mist: {
  293. nodeIntegration: true /* necessary for webviews;
  294. require will be removed through preloader */,
  295. preload: `${__dirname}/preloader/mistUI.js`,
  296. 'overlay-fullscreen-video': true,
  297. 'overlay-scrollbars': true,
  298. experimentalFeatures: true
  299. },
  300. wallet: {
  301. preload: `${__dirname}/preloader/walletMain.js`,
  302. 'overlay-fullscreen-video': true,
  303. 'overlay-scrollbars': true
  304. }
  305. };
  306. switch (type) {
  307. case 'main':
  308. return {
  309. primary: true,
  310. electronOptions: {
  311. width: Math.max(global.defaultWindow.width, 500),
  312. height: Math.max(global.defaultWindow.height, 440),
  313. x: global.defaultWindow.x,
  314. y: global.defaultWindow.y,
  315. webPreferences: mainWebPreferences[global.mode]
  316. }
  317. };
  318. case 'loading':
  319. return {
  320. show: false,
  321. url: `${global.interfacePopupsUrl}#loadingWindow`,
  322. electronOptions: {
  323. title: '',
  324. alwaysOnTop: true,
  325. resizable: false,
  326. width: 100,
  327. height: 80,
  328. center: true,
  329. frame: false,
  330. useContentSize: true,
  331. titleBarStyle: '', // hidden-inset: more space
  332. skipTaskbar: true,
  333. webPreferences: {
  334. preload: `${__dirname}/preloader/popupWindowsNoWeb3.js`
  335. }
  336. }
  337. };
  338. case 'about':
  339. return {
  340. url: `${global.interfacePopupsUrl}#about`,
  341. electronOptions: {
  342. width: 420,
  343. height: 230,
  344. alwaysOnTop: true
  345. }
  346. };
  347. case 'remix':
  348. return {
  349. url: 'https://remix.ethereum.org',
  350. electronOptions: {
  351. width: 1024,
  352. height: 720,
  353. center: true,
  354. frame: true,
  355. resizable: true,
  356. titleBarStyle: 'default'
  357. }
  358. };
  359. case 'importAccount':
  360. return {
  361. electronOptions: {
  362. width: 600,
  363. height: 370,
  364. alwaysOnTop: true
  365. }
  366. };
  367. case 'requestAccount':
  368. return {
  369. electronOptions: {
  370. width: 420,
  371. height: 230,
  372. alwaysOnTop: true
  373. }
  374. };
  375. case 'connectAccount':
  376. return {
  377. electronOptions: {
  378. width: 460,
  379. height: 520,
  380. maximizable: false,
  381. minimizable: false,
  382. alwaysOnTop: true
  383. }
  384. };
  385. case 'sendTransactionConfirmation':
  386. return {
  387. electronOptions: {
  388. width: 580,
  389. height: 550,
  390. alwaysOnTop: true,
  391. enableLargerThanScreen: false,
  392. resizable: true
  393. }
  394. };
  395. case 'updateAvailable':
  396. return {
  397. useWeb3: false,
  398. electronOptions: {
  399. width: 580,
  400. height: 250,
  401. alwaysOnTop: true,
  402. resizable: false,
  403. maximizable: false
  404. }
  405. };
  406. case 'clientUpdateAvailable':
  407. return {
  408. useWeb3: false,
  409. electronOptions: {
  410. width: 600,
  411. height: 340,
  412. alwaysOnTop: false,
  413. resizable: false,
  414. maximizable: false
  415. }
  416. };
  417. case 'generic':
  418. return {
  419. title: Settings.appName,
  420. show: false,
  421. icon: global.icon,
  422. titleBarStyle: 'hidden-inset', // hidden-inset: more space
  423. backgroundColor: '#F6F6F6',
  424. acceptFirstMouse: true,
  425. darkTheme: true,
  426. webPreferences: {
  427. preload: `${__dirname}/preloader/popupWindows.js`,
  428. nodeIntegration: false,
  429. webaudio: true,
  430. webgl: false,
  431. webSecurity: false, // necessary to make routing work on file:// protocol for assets in windows and popups. Not webviews!
  432. textAreasAreResizable: true
  433. }
  434. };
  435. }
  436. }
  437. createPopup(type, options, callback) {
  438. const defaultPopupOpts = {
  439. url: `${global.interfacePopupsUrl}#${type}`,
  440. show: true,
  441. ownerId: null,
  442. useWeb3: true,
  443. electronOptions: {
  444. title: '',
  445. width: 400,
  446. height: 400,
  447. resizable: false,
  448. center: true,
  449. useContentSize: true,
  450. titleBarStyle: 'hidden', // hidden-inset: more space
  451. autoHideMenuBar: true, // TODO: test on windows
  452. webPreferences: {
  453. textAreasAreResizable: false
  454. }
  455. }
  456. };
  457. let opts = _.deepExtend(
  458. defaultPopupOpts,
  459. this.getDefaultOptionsForType(type),
  460. options || {}
  461. );
  462. // always show on top of main window
  463. const parent = _.find(this._windows, w => {
  464. return w.type === 'main';
  465. });
  466. if (parent) {
  467. opts.electronOptions.parent = parent.window;
  468. }
  469. // mark it as a pop-up window
  470. opts.isPopup = true;
  471. if (opts.useWeb3) {
  472. opts.electronOptions.webPreferences.preload = `${__dirname}/preloader/popupWindows.js`;
  473. } else {
  474. opts.electronOptions.webPreferences.preload = `${__dirname}/preloader/popupWindowsNoWeb3.js`;
  475. }
  476. // If generic window is available, recycle it (unless on blacklist)
  477. const genericWindow = this.getByType('generic');
  478. const genericWindowBlacklist = [
  479. 'remix',
  480. 'updateAvailable',
  481. 'clientUpdateAvailable',
  482. 'connectAccount'
  483. ];
  484. if (
  485. !genericWindowBlacklist.includes(type) &&
  486. genericWindow &&
  487. genericWindow.isAvailable
  488. ) {
  489. genericWindow.reuse(type, opts, callback);
  490. return genericWindow;
  491. } else if (genericWindow) {
  492. // If a generic window exists of the same actingType, focus that window
  493. if (genericWindow.actingType === type) {
  494. genericWindow.webContents.focus();
  495. return genericWindow;
  496. }
  497. }
  498. this.loading.show();
  499. log.info(`Create popup window: ${type}`);
  500. const wnd = this.create(type, opts, callback);
  501. wnd.once('ready', () => {
  502. this.loading.hide();
  503. });
  504. return wnd;
  505. }
  506. getByType(type) {
  507. log.trace('Get by type', type);
  508. return _.find(this._windows, w => {
  509. return w.type === type;
  510. });
  511. }
  512. getById(id) {
  513. log.trace('Get by id', id);
  514. return _.find(this._windows, w => {
  515. return w.id === id;
  516. });
  517. }
  518. broadcast() {
  519. const data = arguments;
  520. log.trace('Broadcast', data);
  521. _.each(this._windows, wnd => {
  522. wnd.send(...data);
  523. });
  524. }
  525. /**
  526. * Handle a window being closed.
  527. *
  528. * This will remove the window from the internal list.
  529. *
  530. * This also checks to see if any primary windows are still visible
  531. * (even if hidden). If none found then it quits the app.
  532. *
  533. * @param {Window} wnd
  534. */
  535. _onWindowClosed(wnd) {
  536. log.debug(`Removing window from list: ${wnd.type}`);
  537. for (const t in this._windows) {
  538. if (this._windows[t] === wnd) {
  539. delete this._windows[t];
  540. break;
  541. }
  542. }
  543. const anyOpen = _.find(this._windows, wnd => {
  544. return wnd.isPrimary && !wnd.isClosed && wnd.isShown;
  545. });
  546. if (!anyOpen) {
  547. log.info('All primary windows closed/invisible, so quitting app...');
  548. app.quit();
  549. }
  550. }
  551. }
  552. module.exports = new Windows();