From 0eb933b2789d6bd6186870b9cfbc3e26df0623eb Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Thu, 5 Oct 2023 20:42:29 -0400 Subject: [PATCH] Add hardware acceleration preference --- docs/en/src/SUMMARY.md | 3 +- .../en/src/troubleshooting/visual-glitches.md | 16 +++ package.json | 2 +- public/locales/en-US.json | 5 + src/__mocks__/electron.ts | 1 + src/__mocks__/fs-extra.ts | 1 + .../main-process/__tests__/app-prefs.test.ts | 42 +++--- .../__tests__/hardware-acceleration.test.ts | 88 ++++++++++++ .../main-process/__tests__/init-app.test.ts | 6 - .../main-process/__tests__/json-file.test.ts | 27 +++- .../main-process/__tests__/menu-bar.test.ts | 130 ++++++++++++++++++ .../__tests__/relaunch-dialog.test.ts | 62 +++++++++ src/electron/main-process/app-prefs.ts | 25 +++- .../main-process/hardware-acceleration.ts | 18 +++ src/electron/main-process/index.ts | 13 +- src/electron/main-process/init-app.ts | 2 - src/electron/main-process/json-file.ts | 9 +- src/electron/main-process/menu-bar.ts | 8 ++ src/electron/main-process/relaunch-dialog.ts | 19 +++ 19 files changed, 436 insertions(+), 41 deletions(-) create mode 100644 docs/en/src/troubleshooting/visual-glitches.md create mode 100644 src/electron/main-process/__tests__/hardware-acceleration.test.ts create mode 100644 src/electron/main-process/__tests__/relaunch-dialog.test.ts create mode 100644 src/electron/main-process/hardware-acceleration.ts create mode 100644 src/electron/main-process/relaunch-dialog.ts diff --git a/docs/en/src/SUMMARY.md b/docs/en/src/SUMMARY.md index 06ef44bcc..86a7ccbb7 100644 --- a/docs/en/src/SUMMARY.md +++ b/docs/en/src/SUMMARY.md @@ -58,4 +58,5 @@ - [If An Error Message Appears While Editing](troubleshooting/error-message.md) - [If Twine Won't Start](troubleshooting/wont-start.md) - [If Twine Lost Your Story](troubleshooting/lost-story.md) - - [If Your Story Is Damaged](troubleshooting/damaged-story.md) \ No newline at end of file + - [If Your Story Is Damaged](troubleshooting/damaged-story.md) + - [If You See Visual Glitches in Twine](troubleshooting/visual-glitches.md) \ No newline at end of file diff --git a/docs/en/src/troubleshooting/visual-glitches.md b/docs/en/src/troubleshooting/visual-glitches.md new file mode 100644 index 000000000..644abace0 --- /dev/null +++ b/docs/en/src/troubleshooting/visual-glitches.md @@ -0,0 +1,16 @@ +# If You See Visual Glitches in Twine + +This page only applies to app Twine. + +If you see visual glitches often in Twine, turning off hardware accleration may +help. You should only do this if you are seeing a problem. As the name implies, +using hardware acceleration speeds up Twine's display. + +To do this, go to _Troubleshooting_ under the _Help_ menu, then choose _Disable +Hardware Acceleration_. This item will be checked when hardware acceleration is +disabled, but changing the setting will only take effect the next time you +launch Twine. + +You can also disable hardware acceleration by launching Twine with the +command-line switch +‑‑disableHardwareAcceleration=true. \ No newline at end of file diff --git a/package.json b/package.json index 6b5a09927..98169324c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Twine", - "version": "2.7.1", + "version": "2.8.0-alpha1", "description": "a GUI for creating nonlinear stories", "author": "Chris Klimas ", "license": "GPL-3.0", diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 72ddc71fd..9b32af42f 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -262,6 +262,7 @@ }, "menuBar": { "checkForUpdates": "Check for Updates...", + "disableHardwareAcceleration": "Disable Hardware Acceleration", "edit": "Edit", "showDevTools": "Show Debug Console", "showStoryLibrary": "Show Story Library", @@ -270,6 +271,10 @@ "twineHelp": "Twine Help", "view": "View" }, + "relaunchDialog": { + "defaultPrompt": "This change will take effect the next time Twine is launched.", + "relaunchNow": "Relaunch Now" + }, "scratchDirectoryName": "Scratch", "storiesDirectoryName": "Stories", "updateCheck": { diff --git a/src/__mocks__/electron.ts b/src/__mocks__/electron.ts index 1815395ca..8abae1ec4 100644 --- a/src/__mocks__/electron.ts +++ b/src/__mocks__/electron.ts @@ -1,4 +1,5 @@ export const app = { + disableHardwareAcceleration: jest.fn(), getName() { return `mock-electron-app-name`; }, diff --git a/src/__mocks__/fs-extra.ts b/src/__mocks__/fs-extra.ts index 8c81c9413..ca9bdb31e 100644 --- a/src/__mocks__/fs-extra.ts +++ b/src/__mocks__/fs-extra.ts @@ -5,6 +5,7 @@ export const move = jest.fn(); export const readFile = jest.fn().mockResolvedValue(''); export const readdir = jest.fn().mockResolvedValue([]); export const readJson = jest.fn(); +export const readJsonSync = jest.fn(); export const remove = jest.fn(); export const rename = jest.fn(); export const stat = jest.fn(); diff --git a/src/electron/main-process/__tests__/app-prefs.test.ts b/src/electron/main-process/__tests__/app-prefs.test.ts index d50f1dca6..af165fcc1 100644 --- a/src/electron/main-process/__tests__/app-prefs.test.ts +++ b/src/electron/main-process/__tests__/app-prefs.test.ts @@ -1,6 +1,6 @@ import minimist from 'minimist'; import {getAppPref, loadAppPrefs, setAppPref} from '../app-prefs'; -import {loadJsonFile, saveJsonFile} from '../json-file'; +import {loadJsonFileSync, saveJsonFile} from '../json-file'; jest.mock('minimist'); jest.mock('../json-file'); @@ -10,14 +10,14 @@ beforeEach(() => { jest.spyOn(console, 'warn').mockReturnValue(); }); -const loadJsonFileMock = loadJsonFile as jest.Mock; +const loadJsonFileSyncMock = loadJsonFileSync as jest.Mock; const saveJsonFileMock = saveJsonFile as jest.Mock; const minimistMock = minimist as jest.Mock; function mockJsonFile(value: any) { - loadJsonFileMock.mockImplementation((name: string) => { + loadJsonFileSyncMock.mockImplementation((name: string) => { if (name === 'app-prefs.json') { - return Promise.resolve(value); + return value; } throw new Error(`Loaded incorrect file "${name}"`); @@ -30,47 +30,49 @@ describe('loadAppPrefs and getAppPrefs', () => { minimistMock.mockReturnValue({}); }); - it('loads prefs from command line arguments', async () => { + it('loads prefs from command line arguments', () => { minimistMock.mockReturnValue({ scratchFolderPath: 'mock-scratch-folder-path' }); - await loadAppPrefs(); + loadAppPrefs(); expect(getAppPref('scratchFolderPath')).toBe('mock-scratch-folder-path'); }); - it('loads prefs from the app prefs file', async () => { + it('loads prefs from the app prefs file', () => { mockJsonFile({scratchFolderPath: 'mock-scratch-folder-path'}); - await loadAppPrefs(); + loadAppPrefs(); expect(getAppPref('scratchFolderPath')).toBe('mock-scratch-folder-path'); }); - it('prefers command line arguments to values set in the app prefs file', async () => { + it('prefers command line arguments to values set in the app prefs file', () => { mockJsonFile({scratchFolderPath: 'json-path'}); minimistMock.mockReturnValue({ scratchFolderPath: 'args-path' }); - await loadAppPrefs(); + loadAppPrefs(); expect(getAppPref('scratchFolderPath')).toBe('args-path'); }); - it("ignores values in the app prefs file that aren't known prefs", async () => { + it("ignores values in the app prefs file that aren't known prefs", () => { mockJsonFile({anUnrecognizedKey: 'fail'}); - await loadAppPrefs(); + loadAppPrefs(); expect(getAppPref('anUnrecognizedKey' as any)).toBeUndefined(); }); - it("ignores values in command line arguments that aren't known prefs", async () => { + it("ignores values in command line arguments that aren't known prefs", () => { minimistMock.mockReturnValue({anUnrecognizedKey: 'fail'}); - await loadAppPrefs(); + loadAppPrefs(); expect(getAppPref('anUnrecognizedKey' as any)).toBeUndefined(); }); - it("doesn't throw an error if the app prefs file couldn't be loaded", async () => { + it("doesn't throw an error if the app prefs file couldn't be loaded", () => { minimistMock.mockReturnValue({ scratchFolderPath: 'mock-scratch-folder-path' }); - loadJsonFileMock.mockRejectedValue(new Error()); - await loadAppPrefs(); + loadJsonFileSyncMock.mockImplementation(() => { + throw new Error(); + }); + loadAppPrefs(); expect(getAppPref('scratchFolderPath')).toBe('mock-scratch-folder-path'); }); }); @@ -87,13 +89,13 @@ describe('setAppPref', () => { }); it('resolves after setting a pref', async () => { - await loadAppPrefs(); + loadAppPrefs(); await setAppPref('scratchFolderPath', 'mock-change'); expect(getAppPref('scratchFolderPath')).toBe('mock-change'); }); it('resolves after saving changes to the app prefs file', async () => { - await loadAppPrefs(); + loadAppPrefs(); expect(saveJsonFileMock).not.toBeCalled(); await setAppPref('scratchFolderPath', 'mock-change'); expect(saveJsonFileMock.mock.calls).toEqual([ @@ -103,7 +105,7 @@ describe('setAppPref', () => { it('rejects if saving changes fails', async () => { saveJsonFileMock.mockRejectedValue(new Error()); - await loadAppPrefs(); + loadAppPrefs(); await expect(() => setAppPref('scratchFolderPath', 'mock-value') ).rejects.toBeInstanceOf(Error); diff --git a/src/electron/main-process/__tests__/hardware-acceleration.test.ts b/src/electron/main-process/__tests__/hardware-acceleration.test.ts new file mode 100644 index 000000000..f957dd920 --- /dev/null +++ b/src/electron/main-process/__tests__/hardware-acceleration.test.ts @@ -0,0 +1,88 @@ +import {app} from 'electron'; +import {AppPrefName, getAppPref, setAppPref} from '../app-prefs'; +import { + initHardwareAcceleration, + toggleHardwareAcceleration +} from '../hardware-acceleration'; +import {showRelaunchDialog} from '../relaunch-dialog'; + +jest.mock('electron'); +jest.mock('../app-prefs'); +jest.mock('../relaunch-dialog'); + +describe('initHardwareAcceleration', () => { + const getAppPrefMock = getAppPref as jest.Mock; + const disableHardwareAccelerationMock = + app.disableHardwareAcceleration as jest.Mock; + + beforeEach(() => { + jest.spyOn(console, 'log').mockReturnValue(); + }); + + it('disables hardware acceleration if the app pref is true', () => { + getAppPrefMock.mockImplementation((name: AppPrefName) => { + if (name === 'disableHardwareAcceleration') { + return true; + } + + throw new Error(`Asked for a non-mocked pref: ${name}`); + }); + + initHardwareAcceleration(); + expect(disableHardwareAccelerationMock).toBeCalledTimes(1); + }); + + it("doesn't disable hardware acceleration if the app pref is falsy", () => { + getAppPrefMock.mockImplementation((name: AppPrefName) => { + if (name === 'disableHardwareAcceleration') { + return undefined; + } + + throw new Error(`Asked for a non-mocked pref: ${name}`); + }); + + initHardwareAcceleration(); + expect(disableHardwareAccelerationMock).not.toBeCalled(); + }); +}); + +describe('toggleHardwareAcceleration', () => { + const getAppPrefMock = getAppPref as jest.Mock; + const setAppPrefMock = setAppPref as jest.Mock; + const showRelaunchDialogMock = showRelaunchDialog as jest.Mock; + + it('sets the preference to true if the preference was falsy', () => { + getAppPrefMock.mockImplementation((name: AppPrefName) => { + if (name === 'disableHardwareAcceleration') { + return true; + } + + throw new Error(`Asked for a non-mocked pref: ${name}`); + }); + + toggleHardwareAcceleration(); + expect(setAppPrefMock.mock.calls).toEqual([ + ['disableHardwareAcceleration', false] + ]); + }); + + it('sets the preference to false if the preference was true', () => { + getAppPrefMock.mockImplementation((name: AppPrefName) => { + if (name === 'disableHardwareAcceleration') { + return false; + } + + throw new Error(`Asked for a non-mocked pref: ${name}`); + }); + + toggleHardwareAcceleration(); + expect(setAppPrefMock.mock.calls).toEqual([ + ['disableHardwareAcceleration', true] + ]); + }); + + it('shows the relaunch dialog', () => { + toggleHardwareAcceleration(); + expect(showRelaunchDialogMock).toBeCalledTimes(1); + }); +}); diff --git a/src/electron/main-process/__tests__/init-app.test.ts b/src/electron/main-process/__tests__/init-app.test.ts index b2d7beff7..36f0c1155 100644 --- a/src/electron/main-process/__tests__/init-app.test.ts +++ b/src/electron/main-process/__tests__/init-app.test.ts @@ -22,7 +22,6 @@ describe('initApp', () => { const backupStoryDirectoryMock = backupStoryDirectory as jest.Mock; const cleanScratchDirectoryMock = cleanScratchDirectory as jest.Mock; const createStoryDirectoryMock = createStoryDirectory as jest.Mock; - const loadAppPrefsMock = loadAppPrefs as jest.Mock; const onMock = app.on as jest.Mock; const quitMock = app.quit as jest.Mock; const showErrorBoxMock = dialog.showErrorBox as jest.Mock; @@ -67,11 +66,6 @@ describe('initApp', () => { expect(initIpcMock).toBeCalledTimes(1); }); - it('loads app prefs', async () => { - await initApp(); - expect(loadAppPrefsMock).toBeCalledTimes(1); - }); - it('initializes the menu bar', async () => { await initApp(); expect(initMenuBarMock).toBeCalledTimes(1); diff --git a/src/electron/main-process/__tests__/json-file.test.ts b/src/electron/main-process/__tests__/json-file.test.ts index ab695fc66..fb7f41d76 100644 --- a/src/electron/main-process/__tests__/json-file.test.ts +++ b/src/electron/main-process/__tests__/json-file.test.ts @@ -1,5 +1,5 @@ -import {loadJsonFile, saveJsonFile} from '../json-file'; -import {readJson, writeJson} from 'fs-extra'; +import {loadJsonFile, loadJsonFileSync, saveJsonFile} from '../json-file'; +import {readJson, readJsonSync, writeJson} from 'fs-extra'; jest.mock('fs-extra'); @@ -24,6 +24,29 @@ describe('loadJsonFile()', () => { }); }); +describe('loadJsonFileSync()', () => { + const readJsonSyncMock = readJsonSync as jest.Mock; + + it("returns the contents of a JSON file in the app's user data path", async () => { + const mockData = {test: true}; + + readJsonSyncMock.mockReturnValue(mockData); + expect(loadJsonFileSync('test.json')).toBe(mockData); + expect(readJsonSyncMock.mock.calls).toEqual([ + ['mock-electron-app-path-userData/test.json'] + ]); + }); + + it('throws an error if there is an error reading the file', async () => { + const mockError = new Error(); + + readJsonSyncMock.mockImplementation(() => { + throw mockError; + }); + expect(() => loadJsonFileSync('test.json')).toThrow(mockError); + }); +}); + describe('saveJsonFile()', () => { const writeJsonMock = writeJson as jest.Mock; diff --git a/src/electron/main-process/__tests__/menu-bar.test.ts b/src/electron/main-process/__tests__/menu-bar.test.ts index 5ba403e9f..a470d31f8 100644 --- a/src/electron/main-process/__tests__/menu-bar.test.ts +++ b/src/electron/main-process/__tests__/menu-bar.test.ts @@ -1,8 +1,12 @@ import {initMenuBar} from '../menu-bar'; import {BrowserWindow, Menu, MenuItemConstructorOptions, shell} from 'electron'; import {revealStoryDirectory} from '../story-directory'; +import {getAppPref} from '../app-prefs'; +import {toggleHardwareAcceleration} from '../hardware-acceleration'; jest.mock('electron'); +jest.mock('../app-prefs'); +jest.mock('../hardware-acceleration'); jest.mock('../story-directory'); function hasItemWithRole(menu: MenuItemConstructorOptions, roleName: string) { @@ -15,12 +19,22 @@ function hasItemWithRole(menu: MenuItemConstructorOptions, roleName: string) { } describe('initMenuBar', () => { + const getAppPrefMock = getAppPref as jest.Mock; + const toggleHardwareAccelerationMock = + toggleHardwareAcceleration as jest.Mock; let openDevToolsMock: jest.Mock; let openExternalMock = shell.openExternal as jest.Mock; let revealStoryDirectoryMock = revealStoryDirectory as jest.Mock; let setApplicationMenuSpy: jest.SpyInstance; beforeEach(() => { + getAppPrefMock.mockImplementation((name: string) => { + if (name === 'disableHardwareAcceleration') { + return undefined; + } + + throw new Error(`Asked for a unmocked app pref: ${name}`); + }); setApplicationMenuSpy = jest.spyOn(Menu, 'setApplicationMenu'); openDevToolsMock = jest.fn(); (BrowserWindow.getFocusedWindow as jest.Mock).mockReturnValue({ @@ -127,6 +141,64 @@ describe('initMenuBar', () => { item.click(); expect(openDevToolsMock).toBeCalled(); }); + + describe('its Disable Hardware Acceleration menu item', () => { + it('calls toggleHardwareAcceleration when clicked', () => { + const menu5 = setApplicationMenuSpy.mock.calls[0][0][4]; + const item = menu5.submenu + .find( + (item: any) => item.label === 'electron.menuBar.troubleshooting' + ) + .submenu.find( + (item: any) => + item.label === 'electron.menuBar.disableHardwareAcceleration' + ); + + expect(item).not.toBeUndefined(); + item.click(); + expect(toggleHardwareAccelerationMock).toBeCalledTimes(1); + }); + + it('is unchecked if the pref is falsy', () => { + const menu5 = setApplicationMenuSpy.mock.calls[0][0][4]; + const item = menu5.submenu + .find( + (item: any) => item.label === 'electron.menuBar.troubleshooting' + ) + .submenu.find( + (item: any) => + item.label === 'electron.menuBar.disableHardwareAcceleration' + ); + + expect(item).not.toBeUndefined(); + expect(item.checked).toBe(false); + }); + + it('is checked if the pref is truthy', () => { + getAppPrefMock.mockImplementation((name: string) => { + if (name === 'disableHardwareAcceleration') { + return 'true'; + } + + throw new Error(`Asked for a unmocked app pref: ${name}`); + }); + setApplicationMenuSpy.mockClear(); + initMenuBar(); + + const menu5 = setApplicationMenuSpy.mock.calls[0][0][4]; + const item = menu5.submenu + .find( + (item: any) => item.label === 'electron.menuBar.troubleshooting' + ) + .submenu.find( + (item: any) => + item.label === 'electron.menuBar.disableHardwareAcceleration' + ); + + expect(item).not.toBeUndefined(); + expect(item.checked).toBe(true); + }); + }); }); }); @@ -225,6 +297,64 @@ describe('initMenuBar', () => { item.click(); expect(openDevToolsMock).toBeCalled(); }); + + describe('its Disable Hardware Acceleration menu item', () => { + it('calls toggleHardwareAcceleration when clicked', () => { + const menu5 = setApplicationMenuSpy.mock.calls[0][0][4]; + const item = menu5.submenu + .find( + (item: any) => item.label === 'electron.menuBar.troubleshooting' + ) + .submenu.find( + (item: any) => + item.label === 'electron.menuBar.disableHardwareAcceleration' + ); + + expect(item).not.toBeUndefined(); + item.click(); + expect(toggleHardwareAccelerationMock).toBeCalledTimes(1); + }); + + it('is unchecked if the pref is falsy', () => { + const menu5 = setApplicationMenuSpy.mock.calls[0][0][4]; + const item = menu5.submenu + .find( + (item: any) => item.label === 'electron.menuBar.troubleshooting' + ) + .submenu.find( + (item: any) => + item.label === 'electron.menuBar.disableHardwareAcceleration' + ); + + expect(item).not.toBeUndefined(); + expect(item.checked).toBe(false); + }); + + it('is checked if the pref is truthy', () => { + getAppPrefMock.mockImplementation((name: string) => { + if (name === 'disableHardwareAcceleration') { + return 'true'; + } + + throw new Error(`Asked for a unmocked app pref: ${name}`); + }); + setApplicationMenuSpy.mockClear(); + initMenuBar(); + + const menu5 = setApplicationMenuSpy.mock.calls[0][0][4]; + const item = menu5.submenu + .find( + (item: any) => item.label === 'electron.menuBar.troubleshooting' + ) + .submenu.find( + (item: any) => + item.label === 'electron.menuBar.disableHardwareAcceleration' + ); + + expect(item).not.toBeUndefined(); + expect(item.checked).toBe(true); + }); + }); }); }); }); diff --git a/src/electron/main-process/__tests__/relaunch-dialog.test.ts b/src/electron/main-process/__tests__/relaunch-dialog.test.ts new file mode 100644 index 000000000..e4021fdf1 --- /dev/null +++ b/src/electron/main-process/__tests__/relaunch-dialog.test.ts @@ -0,0 +1,62 @@ +import {app, dialog} from 'electron'; +import {showRelaunchDialog} from '../relaunch-dialog'; + +jest.mock('electron'); + +describe('showRelaunchDialog', () => { + const quitMock = app.quit as jest.Mock; + const relaunchMock = app.relaunch as jest.Mock; + const showMessageBoxMock = dialog.showMessageBox as jest.Mock; + + beforeEach(() => { + showMessageBoxMock.mockResolvedValue({response: 0}); + }); + + it('uses the default prompt if not specified', async () => { + await showRelaunchDialog(); + expect(showMessageBoxMock.mock.calls).toEqual([ + [ + expect.objectContaining({ + message: 'electron.relaunchDialog.defaultPrompt' + }) + ] + ]); + }); + + it('uses a custom prompt if specified', async () => { + await showRelaunchDialog('test-message'); + expect(showMessageBoxMock.mock.calls).toEqual([ + [ + expect.objectContaining({ + message: 'test-message' + }) + ] + ]); + }); + + it('shows an OK button and Relaunch Now button, and defaults the OK button', async () => { + await showRelaunchDialog('test-message'); + expect(showMessageBoxMock.mock.calls).toEqual([ + [ + expect.objectContaining({ + buttons: ['common.ok', 'electron.relaunchDialog.relaunchNow'], + defaultId: 0 + }) + ] + ]); + }); + + it('relaunches the app if the Relaunch Now button is chosen', async () => { + showMessageBoxMock.mockResolvedValue({response: 1}); + await showRelaunchDialog('test-message'); + expect(relaunchMock).toBeCalledTimes(1); + expect(quitMock).toBeCalledTimes(1); + }); + + it('does nothing if the OK button is chosen', async () => { + showMessageBoxMock.mockResolvedValue({response: 0}); + await showRelaunchDialog('test-message'); + expect(relaunchMock).not.toBeCalled(); + expect(quitMock).not.toBeCalled(); + }); +}); diff --git a/src/electron/main-process/app-prefs.ts b/src/electron/main-process/app-prefs.ts index c2e0ad320..cae9424bc 100644 --- a/src/electron/main-process/app-prefs.ts +++ b/src/electron/main-process/app-prefs.ts @@ -1,26 +1,37 @@ import minimist from 'minimist'; -import {loadJsonFile, saveJsonFile} from './json-file'; +import {loadJsonFileSync, saveJsonFile} from './json-file'; /** * Name of an app-specific preference. These should only be used for preferences * that are related to the app build, e.g. things like folder locations. */ -export type AppPrefName = 'scratchFolderPath' | 'scratchFileCleanupAge'; +export type AppPrefName = + | 'disableHardwareAcceleration' + | 'scratchFolderPath' + | 'scratchFileCleanupAge'; -const prefNames: AppPrefName[] = ['scratchFolderPath', 'scratchFileCleanupAge']; +const prefNames: AppPrefName[] = [ + 'disableHardwareAcceleration', + 'scratchFolderPath', + 'scratchFileCleanupAge' +]; const prefs: Partial> = {}; let prefsLoaded = false; /** * Loads app-specific (e.g. not shared by the browser version) prefs. This - * *must* be called before getAppPref or setAppPref. + * *must* be called before getAppPref or setAppPref. This function is + * synchronous because we need at least one app pref before Electron is ready, + * and there is no way to delay readiness. + * + * @see https://github.com/electron/electron/issues/21370 */ -export async function loadAppPrefs() { - const argv = minimist(process.argv.slice(2)); +export function loadAppPrefs() { + const argv = minimist(process.argv.slice(1)); let appPrefFile: any = {}; try { - appPrefFile = await loadJsonFile('app-prefs.json'); + appPrefFile = loadJsonFileSync('app-prefs.json'); } catch (error) { console.warn("Couldn't read app prefs file; continuing", error); } diff --git a/src/electron/main-process/hardware-acceleration.ts b/src/electron/main-process/hardware-acceleration.ts new file mode 100644 index 000000000..dcc5327ec --- /dev/null +++ b/src/electron/main-process/hardware-acceleration.ts @@ -0,0 +1,18 @@ +import {app} from 'electron'; +import {getAppPref, setAppPref} from './app-prefs'; +import {showRelaunchDialog} from './relaunch-dialog'; + +export function initHardwareAcceleration() { + if (!!getAppPref('disableHardwareAcceleration')) { + console.log('Disabling hardware acceleration'); + app.disableHardwareAcceleration(); + } +} + +export function toggleHardwareAcceleration() { + setAppPref( + 'disableHardwareAcceleration', + !getAppPref('disableHardwareAcceleration') + ); + showRelaunchDialog(); +} diff --git a/src/electron/main-process/index.ts b/src/electron/main-process/index.ts index 528eccc4f..ceacd38e4 100644 --- a/src/electron/main-process/index.ts +++ b/src/electron/main-process/index.ts @@ -1,5 +1,16 @@ import {app} from 'electron'; import {initApp} from './init-app'; +import {loadAppPrefs} from './app-prefs'; +import {initHardwareAcceleration} from './hardware-acceleration'; -app.on('ready', initApp); +// We need to load prefs here *and block* because disabling hardware +// acceleration has to happen before the app is ready. +// @see https://github.com/electron/electron/issues/21370 + +loadAppPrefs(); +initHardwareAcceleration(); + +// Continue initialization that needs to happen after Electron is ready. + +app.whenReady().then(initApp); app.on('window-all-closed', () => app.quit()); diff --git a/src/electron/main-process/init-app.ts b/src/electron/main-process/init-app.ts index 455169b17..c1f5b1574 100644 --- a/src/electron/main-process/init-app.ts +++ b/src/electron/main-process/init-app.ts @@ -6,7 +6,6 @@ import {initMenuBar} from './menu-bar'; import {cleanScratchDirectory} from './scratch-file'; import {backupStoryDirectory, createStoryDirectory} from './story-directory'; import {getUserCss} from './user-css'; -import {loadAppPrefs} from './app-prefs'; let mainWindow: BrowserWindow | null; @@ -60,7 +59,6 @@ async function createWindow() { export async function initApp() { try { await initLocales(); - await loadAppPrefs(); await createStoryDirectory(); await backupStoryDirectory(); setInterval(backupStoryDirectory, 1000 * 60 * 20); diff --git a/src/electron/main-process/json-file.ts b/src/electron/main-process/json-file.ts index 8fbdad51b..839ceb38f 100644 --- a/src/electron/main-process/json-file.ts +++ b/src/electron/main-process/json-file.ts @@ -2,7 +2,7 @@ // listens to the `save-json` IPC event. import {app} from 'electron'; -import {readJson, writeJson} from 'fs-extra'; +import {readJson, readJsonSync, writeJson} from 'fs-extra'; import {join} from 'path'; /** @@ -14,6 +14,13 @@ export function loadJsonFile(filename: string) { return readJson(join(app.getPath('userData'), filename)); } +/** + * Reads a JSON file in the app data folder synchronously. + */ +export function loadJsonFileSync(filename: string) { + return readJsonSync(join(app.getPath('userData'), filename)); +} + /** * Saves an object to JSON in the app data folder. Returns a promise when done. */ diff --git a/src/electron/main-process/menu-bar.ts b/src/electron/main-process/menu-bar.ts index 185b8c391..c0b8e1c1d 100644 --- a/src/electron/main-process/menu-bar.ts +++ b/src/electron/main-process/menu-bar.ts @@ -8,6 +8,8 @@ import { import {revealStoryDirectory} from './story-directory'; import {i18n} from './locales'; import {checkForUpdate} from './check-for-update'; +import {toggleHardwareAcceleration} from './hardware-acceleration'; +import {getAppPref} from './app-prefs'; export function initMenuBar() { const template: MenuItemConstructorOptions[] = [ @@ -65,6 +67,12 @@ export function initMenuBar() { { label: i18n.t('electron.menuBar.troubleshooting'), submenu: [ + { + label: i18n.t('electron.menuBar.disableHardwareAcceleration'), + checked: !!getAppPref('disableHardwareAcceleration'), + click: toggleHardwareAcceleration, + type: 'checkbox' + }, { label: i18n.t('electron.menuBar.showDevTools'), click: () => diff --git a/src/electron/main-process/relaunch-dialog.ts b/src/electron/main-process/relaunch-dialog.ts new file mode 100644 index 000000000..d101556da --- /dev/null +++ b/src/electron/main-process/relaunch-dialog.ts @@ -0,0 +1,19 @@ +import {app, dialog} from 'electron'; +import {i18n} from './locales'; + +export async function showRelaunchDialog(message?: string) { + const {response} = await dialog.showMessageBox({ + message: message ?? i18n.t('electron.relaunchDialog.defaultPrompt'), + type: 'info', + buttons: [ + i18n.t('common.ok'), + i18n.t('electron.relaunchDialog.relaunchNow') + ], + defaultId: 0 + }); + + if (response === 1) { + app.relaunch(); + app.quit(); + } +}