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();
+ }
+}