From bb3fb548d817c101b11ed54af1eb77f3ee7ccc6e Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 12 Nov 2020 00:29:18 +0900 Subject: [PATCH] feat: add APIs to enable/disable spell checker (#26276) * feat: add APIs to enable/disable bulitin spell checker * feat: add togglespellchecker menu item role --- docs/api/menu-item.md | 1 + docs/api/session.md | 14 ++++++++++ lib/browser/api/menu-item-roles.ts | 32 +++++++++++++++++++++-- lib/browser/api/menu-item.ts | 10 +++++-- lib/browser/api/menu.ts | 4 ++- shell/browser/api/electron_api_session.cc | 15 +++++++++++ shell/browser/api/electron_api_session.h | 2 ++ spec-main/spellchecker-spec.ts | 23 ++++++++++++++++ typings/internal-electron.d.ts | 1 + 9 files changed, 97 insertions(+), 5 deletions(-) diff --git a/docs/api/menu-item.md b/docs/api/menu-item.md index 2041d76a6a7b..74055d52cdc4 100644 --- a/docs/api/menu-item.md +++ b/docs/api/menu-item.md @@ -88,6 +88,7 @@ The `role` property can have following values: * `resetZoom` - Reset the focused page's zoom level to the original size. * `zoomIn` - Zoom in the focused page by 10%. * `zoomOut` - Zoom out the focused page by 10%. +* `toggleSpellChecker` - Enable/disable builtin spell checker. * `fileMenu` - Whole default "File" menu (Close / Quit) * `editMenu` - Whole default "Edit" menu (Undo, Copy, etc.). * `viewMenu` - Whole default "View" menu (Reload, Toggle Developer Tools, etc.) diff --git a/docs/api/session.md b/docs/api/session.md index 27873b9ce863..3afdf9f926b0 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -673,6 +673,16 @@ this session just before normal `preload` scripts run. Returns `String[]` an array of paths to preload scripts that have been registered. +#### `ses.setSpellCheckerEnabled(enable)` + +* `enable` Boolean + +Sets whether to enable the builtin spell checker. + +#### `ses.isSpellCheckerEnabled()` + +Returns `Boolean` - Whether the builtin spell checker is enabled. + #### `ses.setSpellCheckerLanguages(languages)` * `languages` String[] - An array of language codes to enable the spellchecker for. @@ -803,6 +813,10 @@ The following properties are available on instances of `Session`: A `String[]` array which consists of all the known available spell checker languages. Providing a language code to the `setSpellCheckerLanguages` API that isn't in this array will result in an error. +#### `ses.spellCheckerEnabled` + +A `Boolean` indicating whether builtin spell checker is enabled. + #### `ses.cookies` _Readonly_ A [`Cookies`](cookies.md) object for this session. diff --git a/lib/browser/api/menu-item-roles.ts b/lib/browser/api/menu-item-roles.ts index e733848e2a24..e97a95c9a5ad 100644 --- a/lib/browser/api/menu-item-roles.ts +++ b/lib/browser/api/menu-item-roles.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, WebContents, MenuItemConstructorOptions } from 'electron/main'; +import { app, BrowserWindow, session, webContents, WebContents, MenuItemConstructorOptions } from 'electron/main'; const isMac = process.platform === 'darwin'; const isWindows = process.platform === 'win32'; @@ -6,10 +6,12 @@ const isLinux = process.platform === 'linux'; type RoleId = 'about' | 'close' | 'copy' | 'cut' | 'delete' | 'forcereload' | 'front' | 'help' | 'hide' | 'hideothers' | 'minimize' | 'paste' | 'pasteandmatchstyle' | 'quit' | 'redo' | 'reload' | 'resetzoom' | 'selectall' | 'services' | 'recentdocuments' | 'clearrecentdocuments' | 'startspeaking' | 'stopspeaking' | - 'toggledevtools' | 'togglefullscreen' | 'undo' | 'unhide' | 'window' | 'zoom' | 'zoomin' | 'zoomout' | 'appmenu' | 'filemenu' | 'editmenu' | 'viewmenu' | 'windowmenu' | 'sharemenu' + 'toggledevtools' | 'togglefullscreen' | 'undo' | 'unhide' | 'window' | 'zoom' | 'zoomin' | 'zoomout' | 'togglespellchecker' | + 'appmenu' | 'filemenu' | 'editmenu' | 'viewmenu' | 'windowmenu' | 'sharemenu' interface Role { label: string; accelerator?: string; + checked?: boolean; windowMethod?: ((window: BrowserWindow) => void); webContentsMethod?: ((webContents: WebContents) => void); appMethod?: () => void; @@ -180,6 +182,19 @@ export const roleList: Record = { webContents.zoomLevel -= 0.5; } }, + togglespellchecker: { + label: 'Check Spelling While Typing', + get checked () { + const wc = webContents.getFocusedWebContents(); + const ses = wc ? wc.session : session.defaultSession; + return ses.spellCheckerEnabled; + }, + nonNativeMacOSRole: true, + webContentsMethod: (wc: WebContents) => { + const ses = wc ? wc.session : session.defaultSession; + ses.spellCheckerEnabled = !ses.spellCheckerEnabled; + } + }, // App submenu should be used for Mac only appmenu: { get label () { @@ -281,10 +296,23 @@ const canExecuteRole = (role: keyof typeof roleList) => { return roleList[role].nonNativeMacOSRole; }; +export function getDefaultType (role: RoleId) { + if (shouldOverrideCheckStatus(role)) return 'checkbox'; + return 'normal'; +} + export function getDefaultLabel (role: RoleId) { return hasRole(role) ? roleList[role].label : ''; } +export function getCheckStatus (role: RoleId) { + if (hasRole(role)) return roleList[role].checked; +} + +export function shouldOverrideCheckStatus (role: RoleId) { + return hasRole(role) && Object.prototype.hasOwnProperty.call(roleList[role], 'checked'); +} + export function getDefaultAccelerator (role: RoleId) { if (hasRole(role)) return roleList[role].accelerator; } diff --git a/lib/browser/api/menu-item.ts b/lib/browser/api/menu-item.ts index cd5562fb16e4..8952fe1019fc 100644 --- a/lib/browser/api/menu-item.ts +++ b/lib/browser/api/menu-item.ts @@ -22,7 +22,7 @@ const MenuItem = function (this: any, options: any) { throw new Error('Invalid submenu'); } - this.overrideReadOnlyProperty('type', 'normal'); + this.overrideReadOnlyProperty('type', roles.getDefaultType(this.role)); this.overrideReadOnlyProperty('role'); this.overrideReadOnlyProperty('accelerator'); this.overrideReadOnlyProperty('icon'); @@ -46,7 +46,8 @@ const MenuItem = function (this: any, options: any) { const click = options.click; this.click = (event: Event, focusedWindow: BrowserWindow, focusedWebContents: WebContents) => { // Manually flip the checked flags when clicked. - if (this.type === 'checkbox' || this.type === 'radio') { + if (!roles.shouldOverrideCheckStatus(this.role) && + (this.type === 'checkbox' || this.type === 'radio')) { this.checked = !this.checked; } @@ -66,6 +67,11 @@ MenuItem.prototype.getDefaultRoleAccelerator = function () { return roles.getDefaultAccelerator(this.role); }; +MenuItem.prototype.getCheckStatus = function () { + if (!roles.shouldOverrideCheckStatus(this.role)) return this.checked; + return roles.getCheckStatus(this.role); +}; + MenuItem.prototype.overrideProperty = function (name: string, defaultValue: any = null) { if (this[name] == null) { this[name] = defaultValue; diff --git a/lib/browser/api/menu.ts b/lib/browser/api/menu.ts index 384209c45777..9e633d0f9d1e 100644 --- a/lib/browser/api/menu.ts +++ b/lib/browser/api/menu.ts @@ -17,7 +17,9 @@ Menu.prototype._init = function () { }; Menu.prototype._isCommandIdChecked = function (id) { - return this.commandsMap[id] ? this.commandsMap[id].checked : false; + const item = this.commandsMap[id]; + if (!item) return false; + return item.getCheckStatus(); }; Menu.prototype._isCommandIdEnabled = function (id) { diff --git a/shell/browser/api/electron_api_session.cc b/shell/browser/api/electron_api_session.cc index eb981f379614..22a463ff3bf0 100644 --- a/shell/browser/api/electron_api_session.cc +++ b/shell/browser/api/electron_api_session.cc @@ -1077,6 +1077,17 @@ bool Session::RemoveWordFromSpellCheckerDictionary(const std::string& word) { #endif return service->GetCustomDictionary()->RemoveWord(word); } + +void Session::SetSpellCheckerEnabled(bool b) { + browser_context_->prefs()->SetBoolean(spellcheck::prefs::kSpellCheckEnable, + b); +} + +bool Session::IsSpellCheckerEnabled() const { + return browser_context_->prefs()->GetBoolean( + spellcheck::prefs::kSpellCheckEnable); +} + #endif // BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER) // static @@ -1179,6 +1190,10 @@ gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder( &Session::AddWordToSpellCheckerDictionary) .SetMethod("removeWordFromSpellCheckerDictionary", &Session::RemoveWordFromSpellCheckerDictionary) + .SetMethod("setSpellCheckerEnabled", &Session::SetSpellCheckerEnabled) + .SetMethod("isSpellCheckerEnabled", &Session::IsSpellCheckerEnabled) + .SetProperty("spellCheckerEnabled", &Session::IsSpellCheckerEnabled, + &Session::SetSpellCheckerEnabled) #endif .SetMethod("preconnect", &Session::Preconnect) .SetMethod("closeAllConnections", &Session::CloseAllConnections) diff --git a/shell/browser/api/electron_api_session.h b/shell/browser/api/electron_api_session.h index 64acdca2950e..8c69ccc0f2c4 100644 --- a/shell/browser/api/electron_api_session.h +++ b/shell/browser/api/electron_api_session.h @@ -131,6 +131,8 @@ class Session : public gin::Wrappable, v8::Local ListWordsInSpellCheckerDictionary(); bool AddWordToSpellCheckerDictionary(const std::string& word); bool RemoveWordFromSpellCheckerDictionary(const std::string& word); + void SetSpellCheckerEnabled(bool b); + bool IsSpellCheckerEnabled() const; #endif #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) diff --git a/spec-main/spellchecker-spec.ts b/spec-main/spellchecker-spec.ts index 41cd6ae58d38..7ecd623fae48 100644 --- a/spec-main/spellchecker-spec.ts +++ b/spec-main/spellchecker-spec.ts @@ -101,6 +101,29 @@ ifdescribe(features.isBuiltinSpellCheckerEnabled())('spellchecker', () => { expect(await callWebFrameFn('getWordSuggestions("testt")')).to.not.be.empty(); }); + describe('spellCheckerEnabled', () => { + it('is enabled by default', async () => { + expect(w.webContents.session.spellCheckerEnabled).to.be.true(); + }); + + ifit(shouldRun)('can be dynamically changed', async () => { + await w.webContents.executeJavaScript('document.body.querySelector("textarea").value = "Beautifulllll asd asd"'); + await w.webContents.executeJavaScript('document.body.querySelector("textarea").focus()'); + // Wait for spellchecker to load + await delay(500); + + const callWebFrameFn = (expr: string) => w.webContents.executeJavaScript('require("electron").webFrame.' + expr); + + w.webContents.session.spellCheckerEnabled = false; + expect(w.webContents.session.spellCheckerEnabled).to.be.false(); + expect(await callWebFrameFn('isWordMisspelled("testt")')).to.equal(false); + + w.webContents.session.spellCheckerEnabled = true; + expect(w.webContents.session.spellCheckerEnabled).to.be.true(); + expect(await callWebFrameFn('isWordMisspelled("testt")')).to.equal(true); + }); + }); + describe('custom dictionary word list API', () => { let ses: Session; diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts index d52bab2c6661..437d123029a6 100644 --- a/typings/internal-electron.d.ts +++ b/typings/internal-electron.d.ts @@ -139,6 +139,7 @@ declare namespace Electron { overrideReadOnlyProperty(property: string, value: any): void; groupId: number; getDefaultRoleAccelerator(): Accelerator | undefined; + getCheckStatus(): boolean; acceleratorWorksWhenHidden?: boolean; }