Browse Source

feat: add APIs to enable/disable spell checker (#26276)

* feat: add APIs to enable/disable bulitin spell checker

* feat: add togglespellchecker menu item role
Cheng Zhao 4 years ago
parent
commit
bb3fb548d8

+ 1 - 0
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.)

+ 14 - 0
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.

+ 30 - 2
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<RoleId, Role> = {
       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;
 }

+ 8 - 2
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;

+ 3 - 1
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) {

+ 15 - 0
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)

+ 2 - 0
shell/browser/api/electron_api_session.h

@@ -131,6 +131,8 @@ class Session : public gin::Wrappable<Session>,
   v8::Local<v8::Promise> 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)

+ 23 - 0
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;
 

+ 1 - 0
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;
   }