1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-06 09:19:22 +02:00

Desktop: Added toolbar button to switch spell checker language

This commit is contained in:
Laurent Cozic
2020-11-08 01:08:33 +00:00
parent 6ac4131003
commit bd2081c3b6
18 changed files with 206 additions and 102 deletions

View File

@@ -427,6 +427,9 @@ packages/app-desktop/gui/MainScreen/commands/showNoteProperties.js.map
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.d.ts
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js.map
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.d.ts
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js.map
packages/app-desktop/gui/MainScreen/commands/toggleEditors.d.ts
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js.map
@@ -886,6 +889,9 @@ packages/lib/services/CommandService.js.map
packages/lib/services/KeymapService.d.ts
packages/lib/services/KeymapService.js
packages/lib/services/KeymapService.js.map
packages/lib/services/KvStore.d.ts
packages/lib/services/KvStore.js
packages/lib/services/KvStore.js.map
packages/lib/services/ResourceEditWatcher/index.d.ts
packages/lib/services/ResourceEditWatcher/index.js
packages/lib/services/ResourceEditWatcher/index.js.map

6
.gitignore vendored
View File

@@ -419,6 +419,9 @@ packages/app-desktop/gui/MainScreen/commands/showNoteProperties.js.map
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.d.ts
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js.map
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.d.ts
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js.map
packages/app-desktop/gui/MainScreen/commands/toggleEditors.d.ts
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js.map
@@ -878,6 +881,9 @@ packages/lib/services/CommandService.js.map
packages/lib/services/KeymapService.d.ts
packages/lib/services/KeymapService.js
packages/lib/services/KeymapService.js.map
packages/lib/services/KvStore.d.ts
packages/lib/services/KvStore.js
packages/lib/services/KvStore.js.map
packages/lib/services/ResourceEditWatcher/index.d.ts
packages/lib/services/ResourceEditWatcher/index.js
packages/lib/services/ResourceEditWatcher/index.js.map

View File

@@ -374,6 +374,9 @@ packages/app-desktop/gui/MainScreen/commands/showNoteProperties.js.map
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.d.ts
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js.map
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.d.ts
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js.map
packages/app-desktop/gui/MainScreen/commands/toggleEditors.d.ts
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js.map
@@ -833,6 +836,9 @@ packages/lib/services/CommandService.js.map
packages/lib/services/KeymapService.d.ts
packages/lib/services/KeymapService.js
packages/lib/services/KeymapService.js.map
packages/lib/services/KvStore.d.ts
packages/lib/services/KvStore.js
packages/lib/services/KvStore.js.map
packages/lib/services/ResourceEditWatcher/index.d.ts
packages/lib/services/ResourceEditWatcher/index.js
packages/lib/services/ResourceEditWatcher/index.js.map

View File

@@ -2,7 +2,7 @@
const { asyncTest, fileContentEqual, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const KvStore = require('@joplin/lib/services/KvStore.js');
const KvStore = require('@joplin/lib/services/KvStore').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);

View File

@@ -41,7 +41,7 @@ const DecryptionWorker = require('@joplin/lib/services/DecryptionWorker.js');
const ResourceService = require('@joplin/lib/services/ResourceService.js');
const RevisionService = require('@joplin/lib/services/RevisionService.js');
const ResourceFetcher = require('@joplin/lib/services/ResourceFetcher.js');
const KvStore = require('@joplin/lib/services/KvStore.js');
const KvStore = require('@joplin/lib/services/KvStore').default;
const WebDavApi = require('@joplin/lib/WebDavApi');
const DropboxApi = require('@joplin/lib/DropboxApi');
const { OneDriveApi } = require('@joplin/lib/onedrive-api');

View File

@@ -59,6 +59,7 @@ const commands = [
require('./gui/MainScreen/commands/showNoteContentProperties'),
require('./gui/MainScreen/commands/showNoteProperties'),
require('./gui/MainScreen/commands/showShareNoteDialog'),
require('./gui/MainScreen/commands/showSpellCheckerMenu'),
require('./gui/MainScreen/commands/toggleNoteList'),
require('./gui/MainScreen/commands/toggleSideBar'),
require('./gui/MainScreen/commands/toggleVisiblePanes'),
@@ -453,6 +454,8 @@ class Application extends BaseApplication {
}
setupContextMenu() {
const MenuItem = bridge().MenuItem;
// The context menu must be setup in renderer process because that's where
// the spell checker service lives.
require('electron-context-menu')({
@@ -463,7 +466,7 @@ class Application extends BaseApplication {
},
menu: (actions:any, props:any) => {
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(props.misspelledWord, props.dictionarySuggestions);
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(props.misspelledWord, props.dictionarySuggestions).map((item:any) => new MenuItem(item));
const output = [
actions.cut(),

View File

@@ -60,6 +60,7 @@ const commands = [
require('./commands/showNoteContentProperties'),
require('./commands/showNoteProperties'),
require('./commands/showShareNoteDialog'),
require('./commands/showSpellCheckerMenu'),
require('./commands/toggleEditors'),
require('./commands/toggleNoteList'),
require('./commands/toggleSideBar'),

View File

@@ -0,0 +1,34 @@
import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import bridge from '../../../services/bridge';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import { AppState } from '../../../app';
const Menu = bridge().Menu;
export const declaration:CommandDeclaration = {
name: 'showSpellCheckerMenu',
label: () => _('Spell checker'),
iconName: 'fas fa-globe',
};
export const runtime = ():CommandRuntime => {
return {
execute: async (context:CommandContext, selectedLanguage:string = null, useSpellChecker:boolean = null) => {
selectedLanguage = selectedLanguage === null ? context.state.settings['spellChecker.language'] : selectedLanguage;
useSpellChecker = useSpellChecker === null ? context.state.settings['spellChecker.enabled'] : useSpellChecker;
const menuItems = SpellCheckerService.instance().spellCheckerConfigMenuItems(selectedLanguage, useSpellChecker);
const menu = Menu.buildFromTemplate(menuItems);
menu.popup(bridge().window());
},
mapStateToTitle(state:AppState):string {
if (!state.settings['spellChecker.enabled']) return null;
const language = state.settings['spellChecker.language'];
if (!language) return null;
const s = language.split('-');
return s[0];
},
};
};

View File

@@ -35,9 +35,6 @@ export default function(editor:any) {
const element = contextMenuElement(editor, params.x, params.y);
if (!element) return;
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
let itemType:ContextMenuItemType = ContextMenuItemType.None;
let resourceId = '';
let linkToCopy = null;
@@ -65,27 +62,28 @@ export default function(editor:any) {
isReadOnly: false,
};
const menu = new Menu();
const template = [];
for (const itemName in contextMenuItems) {
const item = contextMenuItems[itemName];
if (!item.isActive(itemType, contextMenuActionOptions.current)) continue;
menu.append(new MenuItem({
template.push({
label: item.label,
click: () => {
item.onAction(contextMenuActionOptions.current);
},
}));
});
}
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
for (const item of spellCheckerMenuItems) {
menu.append(item);
template.push(item);
}
menu.popup();
const menu = bridge().Menu.buildFromTemplate(template);
menu.popup(bridge().window());
});
}

View File

@@ -37,6 +37,7 @@ const mapStateToProps = (state:any) => {
return {
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
'showSpellCheckerMenu',
'editAlarm',
'toggleVisiblePanes',
'showNoteProperties',

View File

@@ -21,6 +21,8 @@ export const StyledRoot = styled.a<RootProps>`
box-sizing: border-box;
color: ${(props:RootProps) => props.theme.color3};
font-size: ${(props:RootProps) => props.theme.toolbarIconSize * 0.8}px;
padding-left: 5px;
padding-right: 5px;
&:hover {
background-color: ${(props:RootProps) => props.disabled ? 'none' : props.theme.backgroundColorHover3};

View File

@@ -25,11 +25,6 @@ export default class SpellCheckerServiceDriverNative extends SpellCheckerService
return languages.length ? languages[0] : '';
}
public makeMenuItem(item:any):any {
const MenuItem = bridge().MenuItem;
return new MenuItem(item);
}
public addWordToSpellCheckerDictionary(_language:string, word:string) {
// Actually on Electron all languages share the same dictionary, or
// perhaps it's added to the currently active language.

View File

@@ -2,7 +2,7 @@ import shim from '@joplin/lib/shim';
const { dirname } = require('@joplin/lib/path-utils');
const Setting = require('@joplin/lib/models/Setting').default;
const pluginAssets = require('./pluginAssets/index');
const KvStore = require('@joplin/lib/services/KvStore.js');
const KvStore = require('@joplin/lib/services/KvStore').default;
export default class PluginAssetsLoader {

View File

@@ -33,7 +33,7 @@ const BaseModel = require('@joplin/lib/BaseModel').default;
const BaseService = require('@joplin/lib/services/BaseService').default;
const ResourceService = require('@joplin/lib/services/ResourceService');
const RevisionService = require('@joplin/lib/services/RevisionService');
const KvStore = require('@joplin/lib/services/KvStore');
const KvStore = require('@joplin/lib/services/KvStore').default;
const { JoplinDatabase } = require('@joplin/lib/joplin-database.js');
const { Database } = require('@joplin/lib/database.js');
const { NotesScreen } = require('./components/screens/notes.js');

View File

@@ -5,6 +5,7 @@ import BaseService from './services/BaseService';
import reducer from './reducer';
import KeychainServiceDriver from './services/keychain/KeychainServiceDriver.node';
import { _, setLocale } from './locale';
import KvStore from './services/KvStore';
const { createStore, applyMiddleware } = require('redux');
const { defaultState, stateUtils } = require('./reducer');
@@ -41,7 +42,6 @@ const RevisionService = require('./services/RevisionService');
const ResourceService = require('./services/RevisionService');
const DecryptionWorker = require('./services/DecryptionWorker');
const { loadKeychainServiceAndSettings } = require('./services/SettingUtils');
const KvStore = require('./services/KvStore');
const MigrationService = require('./services/MigrationService');
const { toSystemSlashes } = require('./path-utils');
const { setAutoFreeze } = require('immer');
@@ -89,7 +89,7 @@ export default class BaseApplication {
await reg.cancelTimers();
this.eventEmitter_.removeAllListeners();
KvStore.instance_ = null;
KvStore.destroyInstance();
BaseModel.setDb(null);
reg.setDb(null);

View File

@@ -1,34 +1,49 @@
const BaseService = require('./BaseService').default;
import BaseService from './BaseService';
const Mutex = require('async-mutex').Mutex;
class KvStore extends BaseService {
static instance() {
enum ValueType {
Int = 1,
Text = 2,
}
export default class KvStore extends BaseService {
private incMutex_:any = null
private db_:any = null;
private static instance_:KvStore = null;
public static instance() {
if (this.instance_) return this.instance_;
this.instance_ = new KvStore();
return this.instance_;
}
constructor() {
public static destroyInstance() {
this.instance_ = null;
}
private constructor() {
super();
this.incMutex_ = new Mutex();
}
setDb(v) {
public setDb(v:any) {
this.db_ = v;
}
db() {
private db() {
if (!this.db_) throw new Error('Accessing DB before it has been set!');
return this.db_;
}
typeFromValue_(value) {
if (typeof value === 'string') return KvStore.TYPE_TEXT;
if (typeof value === 'number') return KvStore.TYPE_INT;
private typeFromValue_(value:any) {
if (typeof value === 'string') return ValueType.Text;
if (typeof value === 'number') return ValueType.Int;
throw new Error(`Unsupported value type: ${typeof value}`);
}
formatValues_(kvs) {
private formatValues_(kvs:any[]) {
const output = [];
for (const kv of kvs) {
kv.value = this.formatValue_(kv.value, kv.type);
@@ -37,47 +52,48 @@ class KvStore extends BaseService {
return output;
}
formatValue_(value, type) {
if (type === KvStore.TYPE_INT) return Number(value);
if (type === KvStore.TYPE_TEXT) return `${value}`;
private formatValue_(value:any, type:ValueType):string | number {
if (type === ValueType.Int) return Number(value);
if (type === ValueType.Text) return `${value}`;
throw new Error(`Unknown type: ${type}`);
}
async value(key) {
public async value<T>(key:string):Promise<T> {
const r = await this.db().selectOne('SELECT `value`, `type` FROM key_values WHERE `key` = ?', [key]);
if (!r) return null;
return this.formatValue_(r.value, r.type);
return this.formatValue_(r.value, r.type) as any;
}
async setValue(key, value) {
public async setValue(key:string, value:any) {
const t = Date.now();
await this.db().exec('INSERT OR REPLACE INTO key_values (`key`, `value`, `type`, `updated_time`) VALUES (?, ?, ?, ?)', [key, value, this.typeFromValue_(value), t]);
}
async deleteValue(key) {
public async deleteValue(key:string) {
await this.db().exec('DELETE FROM key_values WHERE `key` = ?', [key]);
}
async deleteByPrefix(prefix) {
public async deleteByPrefix(prefix:string) {
await this.db().exec('DELETE FROM key_values WHERE `key` LIKE ?', [`${prefix}%`]);
}
async clear() {
public async clear() {
await this.db().exec('DELETE FROM key_values');
}
async all() {
public async all() {
return this.formatValues_(await this.db().selectAll('SELECT * FROM key_values'));
}
// Note: atomicity is done at application level so two difference instances
// accessing the db at the same time could mess up the increment.
async incValue(key, inc = 1) {
public async incValue(key:string, inc:number = 1) {
const release = await this.incMutex_.acquire();
try {
const result = await this.db().selectOne('SELECT `value`, `type` FROM key_values WHERE `key` = ?', [key]);
const newValue = result ? this.formatValue_(result.value, result.type) + inc : inc;
const value = this.formatValue_(result.value, result.type) as ValueType.Int;
const newValue = result ? value + inc : inc;
await this.setValue(key, newValue);
release();
return newValue;
@@ -87,18 +103,13 @@ class KvStore extends BaseService {
}
}
async searchByPrefix(prefix) {
public async searchByPrefix(prefix:string) {
const results = await this.db().selectAll('SELECT `key`, `value`, `type` FROM key_values WHERE `key` LIKE ?', [`${prefix}%`]);
return this.formatValues_(results);
}
async countKeys() {
public async countKeys() {
const r = await this.db().selectOne('SELECT count(*) as total FROM key_values');
return r.total ? r.total : 0;
}
}
KvStore.TYPE_INT = 1;
KvStore.TYPE_TEXT = 2;
module.exports = KvStore;

View File

@@ -2,10 +2,12 @@ import Setting from '../../models/Setting';
import CommandService from '../CommandService';
import SpellCheckerServiceDriverBase from './SpellCheckerServiceDriverBase';
import { _, countryDisplayName } from '../../locale';
import KvStore from '../KvStore';
export default class SpellCheckerService {
private driver_:SpellCheckerServiceDriverBase;
private latestSelectedLanguages_:string[] = [];
private static instance_:SpellCheckerService;
@@ -17,6 +19,7 @@ export default class SpellCheckerService {
public async initialize(driver:SpellCheckerServiceDriverBase) {
this.driver_ = driver;
this.latestSelectedLanguages_ = await this.loadLatestSelectedLanguages();
this.setupDefaultLanguage();
this.applyStateToDriver();
}
@@ -25,6 +28,26 @@ export default class SpellCheckerService {
return 'en-US';
}
private async loadLatestSelectedLanguages():Promise<string[]> {
const result = await KvStore.instance().value<string>('spellCheckerService.latestSelectedLanguages');
if (!result) return [];
return JSON.parse(result);
}
private async addLatestSelectedLanguage(language:string) {
const languages = this.latestSelectedLanguages_.slice();
if (languages.length > 5) languages.splice(0, 1);
if (languages.includes(language)) {
languages.splice(languages.indexOf(language), 1);
}
languages.splice(0, 0, language);
this.latestSelectedLanguages_ = languages;
await KvStore.instance().setValue('spellCheckerService.latestSelectedLanguages', JSON.stringify(this.latestSelectedLanguages_));
}
public setupDefaultLanguage() {
if (!Setting.value('spellChecker.language')) {
const l = this.driver_.language;
@@ -43,6 +66,7 @@ export default class SpellCheckerService {
public setLanguage(language:string) {
Setting.setValue('spellChecker.language', language);
this.applyStateToDriver();
this.addLatestSelectedLanguage(language);
}
public get language():string {
@@ -58,100 +82,121 @@ export default class SpellCheckerService {
this.applyStateToDriver();
}
private makeMenuItem(item:any):any {
return this.driver_.makeMenuItem(item);
}
private async addToDictionary(language:string, word:string) {
this.driver_.addWordToSpellCheckerDictionary(language, word);
}
public contextMenuItems<T>(misspelledWord:string, dictionarySuggestions:string[]):T[] {
public contextMenuItems(misspelledWord:string, dictionarySuggestions:string[]):any[] {
if (!misspelledWord) return [];
const output = [];
output.push(this.makeMenuItem({ type: 'separator' }));
output.push({ type: 'separator' });
if (dictionarySuggestions.length) {
for (const suggestion of dictionarySuggestions) {
output.push(this.makeMenuItem({
output.push({
label: suggestion,
click: () => {
CommandService.instance().execute('replaceSelection', suggestion);
},
}));
});
}
} else {
output.push(this.makeMenuItem({
output.push({
label: `(${_('No suggestions')})`,
enabled: false,
click: () => {},
}));
});
}
output.push(this.makeMenuItem({ type: 'separator' }));
output.push({ type: 'separator' });
output.push(this.makeMenuItem({
output.push({
label: _('Add to dictionary'),
click: () => {
this.addToDictionary(this.language, misspelledWord);
},
}));
});
return output;
}
private changeLanguageMenuItem(language:string, enabled:boolean, checked:boolean) {
return {
label: countryDisplayName(language),
type: 'radio',
checked: checked,
enabled: enabled,
click: () => {
this.setLanguage(language);
},
};
}
private changeLanguageMenuItems(selectedLanguage:string, enabled:boolean) {
const languageMenuItems = [];
for (const locale of this.driver_.availableLanguages) {
languageMenuItems.push({
label: countryDisplayName(locale),
type: 'radio',
checked: locale === selectedLanguage,
enabled: enabled,
click: () => {
this.setLanguage(locale);
},
});
languageMenuItems.push(this.changeLanguageMenuItem(locale, enabled, locale === selectedLanguage));
}
languageMenuItems.sort((a:any, b:any) => {
return a.label < b.label ? -1 : +1;
});
return languageMenuItems.map((item:any) => this.makeMenuItem(item));
return languageMenuItems;
}
public spellCheckerConfigMenuItems(selectedLanguage:string, useSpellChecker:boolean) {
const latestLanguageItems = this.latestSelectedLanguages_.map((language:string) => {
return this.changeLanguageMenuItem(language, true, language === selectedLanguage);
});
if (latestLanguageItems.length) latestLanguageItems.splice(0, 0, { type: 'separator' } as any);
latestLanguageItems.sort((a:any, b:any) => {
return a.label < b.label ? -1 : +1;
});
return [
{
label: _('Use spell checker'),
type: 'checkbox',
checked: useSpellChecker,
click: () => {
this.toggleEnabled();
},
},
...latestLanguageItems,
{
type: 'separator',
},
// Can be removed once it does work
{
label: '⚠ Spell checker doesn\'t work in Markdown editor ⚠',
enabled: false,
},
{
type: 'separator',
},
{
label: _('Change language'),
submenu: this.changeLanguageMenuItems(selectedLanguage, useSpellChecker),
},
];
}
public spellCheckerConfigMenuItem(selectedLanguage:string, useSpellChecker:boolean) {
return this.makeMenuItem({
return {
label: _('Spell checker'),
submenu: [
this.makeMenuItem({
label: _('Use spell checker'),
type: 'checkbox',
checked: useSpellChecker,
click: () => {
this.toggleEnabled();
},
}),
this.makeMenuItem({
type: 'separator',
}),
// Can be removed once it does work
this.makeMenuItem({
label: '⚠ Spell checker doesn\'t work in Markdown editor ⚠',
enabled: false,
}),
this.makeMenuItem({
type: 'separator',
}),
...this.changeLanguageMenuItems(selectedLanguage, useSpellChecker),
],
});
submenu: this.spellCheckerConfigMenuItems(selectedLanguage, useSpellChecker),
};
}
}

View File

@@ -12,10 +12,6 @@ export default class SpellCheckerServiceDriverBase {
throw new Error('Not implemented');
}
public makeMenuItem(_item:any):any {
throw new Error('Not implemented');
}
public addWordToSpellCheckerDictionary(_language:string, _word:string) {
throw new Error('Not implemented');
}