From 560c519517b1fbea86827c01a501e468a4ec514d Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Sat, 9 Nov 2024 19:01:13 +0000 Subject: [PATCH] Improve logic --- .eslintignore | 1 + .gitignore | 1 + packages/app-desktop/gui/MainScreen.tsx | 1 + .../gui/NoteEditor/EditorWindow.tsx | 3 ++ .../CodeMirror/utils/useContextMenu.ts | 3 +- .../app-desktop/gui/NoteEditor/NoteEditor.tsx | 38 ++++++++++++- .../app-desktop/gui/NoteEditor/utils/types.ts | 1 + .../commands/index.ts | 2 + .../commands/showEditorPlugin.ts | 53 +++++++++++++++++++ packages/lib/services/plugins/Plugin.ts | 4 ++ .../lib/services/plugins/PluginService.ts | 8 +++ .../lib/services/plugins/WebviewController.ts | 41 +++++++++++++- .../services/plugins/api/JoplinViewsEditor.ts | 27 +++++++++- .../services/plugins/api/JoplinViewsPanels.ts | 4 ++ .../services/plugins/api/JoplinWorkspace.ts | 8 +-- packages/lib/services/plugins/api/types.ts | 20 +++++++ 16 files changed, 202 insertions(+), 13 deletions(-) create mode 100644 packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.ts diff --git a/.eslintignore b/.eslintignore index 5cea814d96b..2bbaeb1617d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -447,6 +447,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js diff --git a/.gitignore b/.gitignore index 4d9d582edb4..dc240afeed5 100644 --- a/.gitignore +++ b/.gitignore @@ -424,6 +424,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js diff --git a/packages/app-desktop/gui/MainScreen.tsx b/packages/app-desktop/gui/MainScreen.tsx index ed82617bb0f..e22414b6502 100644 --- a/packages/app-desktop/gui/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen.tsx @@ -637,6 +637,7 @@ class MainScreenComponent extends React.Component { ; }, diff --git a/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx b/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx index 4443b26fc0d..b52c678ab6c 100644 --- a/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx +++ b/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx @@ -21,6 +21,7 @@ interface Props { newWindow: boolean; windowId: string; activeWindowId: string; + startupPluginsLoaded: boolean; } const emptyCallback = () => {}; @@ -45,6 +46,7 @@ const SecondaryWindow: React.FC = props => { ; @@ -121,5 +123,6 @@ export default connect((state: AppState, ownProps: ConnectProps) => { codeView: windowState?.editorCodeView ?? state.settings['editor.codeView'], legacyMarkdown: state.settings['editor.legacyMarkdown'], activeWindowId: stateUtils.activeWindowId(state), + startupPluginsLoaded: state.startupPluginsLoaded, }; })(SecondaryWindow); diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts index 67f0378c427..6219b0caa0d 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts @@ -3,11 +3,10 @@ import { ContextMenuParams, Event } from 'electron'; import { useEffect, RefObject } from 'react'; import { _ } from '@joplin/lib/locale'; import { PluginStates } from '@joplin/lib/services/plugins/reducer'; -import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types'; +import { EditContextMenuFilterObject, MenuItemLocation } from '@joplin/lib/services/plugins/api/types'; import MenuUtils from '@joplin/lib/services/commands/MenuUtils'; import CommandService from '@joplin/lib/services/CommandService'; import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService'; -import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace'; import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl'; import eventManager from '@joplin/lib/eventManager'; import bridge from '../../../../../services/bridge'; diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx index 517d0f859ad..7768ecfb2b5 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx @@ -55,6 +55,10 @@ import Logger from '@joplin/utils/Logger'; import usePluginEditorView from './utils/usePluginEditorView'; import { stateUtils } from '@joplin/lib/reducer'; import { WindowIdContext } from '../NewWindowOrIFrame'; +import { EditorActivationCheckFilterObject } from '@joplin/lib/services/plugins/api/types'; +import PluginService from '@joplin/lib/services/plugins/PluginService'; +import WebviewController from '@joplin/lib/services/plugins/WebviewController'; +import AsyncActionQueue, { IntervalType } from '@joplin/lib/AsyncActionQueue'; const debounce = require('debounce'); @@ -69,6 +73,15 @@ const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance()); const onDragOver: React.DragEventHandler = event => event.preventDefault(); let editorIdCounter = 0; +const makeNoteUpdateAction = (shownEditorViewIds: string[]) => { + return async () => { + for (const viewId of shownEditorViewIds) { + const controller = PluginService.instance().viewControllerByViewId(viewId) as WebviewController; + if (controller) controller.emitUpdate(); + } + }; +}; + function NoteEditorContent(props: NoteEditorProps) { const [showRevisions, setShowRevisions] = useState(false); const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false); @@ -78,6 +91,9 @@ function NoteEditorContent(props: NoteEditorProps) { const titleInputRef = useRef(); const isMountedRef = useRef(true); const noteSearchBarRef = useRef(null); + const viewUpdateAsyncQueue_ = useRef(new AsyncActionQueue(100, IntervalType.Fixed)); + + const shownEditorViewIds = props['plugins.shownEditorViewIds']; // Should be constant and unique to this instance of the editor. const editorId = useMemo(() => { @@ -99,7 +115,27 @@ function NoteEditorContent(props: NoteEditorProps) { const effectiveNoteId = useEffectiveNoteId(props); - const { editorPlugin, editorView } = usePluginEditorView(props.plugins, props['plugins.shownEditorViewIds']); + useAsyncEffect(async (event) => { + if (!props.startupPluginsLoaded) return; + + let filterObject: EditorActivationCheckFilterObject = { + activatedEditors: [], + }; + filterObject = await eventManager.filterEmit('editorActivationCheck', filterObject); + if (event.cancelled) return; + + for (const editor of filterObject.activatedEditors) { + const controller = PluginService.instance().pluginById(editor.pluginId).viewController(editor.viewId) as WebviewController; + controller.setActive(editor.isActive); + } + }, [effectiveNoteId, props.startupPluginsLoaded]); + + useEffect(() => { + if (!props.startupPluginsLoaded) return; + viewUpdateAsyncQueue_.current.push(makeNoteUpdateAction(shownEditorViewIds)); + }, [effectiveNoteId, shownEditorViewIds, props.startupPluginsLoaded]); + + const { editorPlugin, editorView } = usePluginEditorView(props.plugins, shownEditorViewIds); const builtInEditorVisible = !editorPlugin; const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({ diff --git a/packages/app-desktop/gui/NoteEditor/utils/types.ts b/packages/app-desktop/gui/NoteEditor/utils/types.ts index b69e10911c3..6fef53ba799 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/types.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/types.ts @@ -58,6 +58,7 @@ export interface NoteEditorProps { 'plugins.shownEditorViewIds': string[]; onTitleChange?: (title: string)=> void; bodyEditor: string; + startupPluginsLoaded: boolean; } export interface NoteBodyEditorRef { diff --git a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts index 55d38fe3766..707a7ae5ec1 100644 --- a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts +++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts @@ -28,6 +28,7 @@ import * as restoreNote from './restoreNote'; import * as revealResourceFile from './revealResourceFile'; import * as search from './search'; import * as setTags from './setTags'; +import * as showEditorPlugin from './showEditorPlugin'; import * as showModalMessage from './showModalMessage'; import * as showNoteContentProperties from './showNoteContentProperties'; import * as showNoteProperties from './showNoteProperties'; @@ -77,6 +78,7 @@ const index: any[] = [ revealResourceFile, search, setTags, + showEditorPlugin, showModalMessage, showNoteContentProperties, showNoteProperties, diff --git a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.ts new file mode 100644 index 00000000000..6a12cc23186 --- /dev/null +++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.ts @@ -0,0 +1,53 @@ +import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService'; +import Setting from '@joplin/lib/models/Setting'; +import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getActivePluginEditorView'; +import Logger from '@joplin/utils/Logger'; + +const logger = Logger.create('showEditorPlugin'); + +export const declaration: CommandDeclaration = { + name: 'showEditorPlugin', + label: () => 'Show editor plugin', + iconName: 'fas fa-eye', +}; + +export const runtime = (): CommandRuntime => { + return { + execute: async (context: CommandContext, editorViewId = '', show = true) => { + logger.info('View:', editorViewId, 'Show:', show); + + const shownEditorViewIds = Setting.value('plugins.shownEditorViewIds'); + + if (!editorViewId) { + const { editorPlugin, editorView } = getActivePluginEditorView(context.state.pluginService.plugins); + + if (!editorPlugin) { + logger.warn('No editor plugin to toggle to'); + return; + } + + editorViewId = editorView.id; + } + + const idx = shownEditorViewIds.indexOf(editorViewId); + + if (show) { + if (idx >= 0) { + logger.info(`Editor is already visible: ${editorViewId}`); + return; + } + + shownEditorViewIds.push(editorViewId); + } else { + if (idx < 0) { + logger.info(`Editor is already hidden: ${editorViewId}`); + return; + } + + shownEditorViewIds.splice(idx, 1); + } + + Setting.setValue('plugins.shownEditorViewIds', shownEditorViewIds); + }, + }; +}; diff --git a/packages/lib/services/plugins/Plugin.ts b/packages/lib/services/plugins/Plugin.ts index 12988f8eaac..1d6c09bec86 100644 --- a/packages/lib/services/plugins/Plugin.ts +++ b/packages/lib/services/plugins/Plugin.ts @@ -180,6 +180,10 @@ export default class Plugin { this.viewControllers_[v.handle] = v; } + public hasViewController(handle: ViewHandle) { + return !!this.viewControllers_[handle]; + } + public viewController(handle: ViewHandle): ViewController { if (!this.viewControllers_[handle]) throw new Error(`View not found: ${handle}`); return this.viewControllers_[handle]; diff --git a/packages/lib/services/plugins/PluginService.ts b/packages/lib/services/plugins/PluginService.ts index 083c61dff27..a2a103e32af 100644 --- a/packages/lib/services/plugins/PluginService.ts +++ b/packages/lib/services/plugins/PluginService.ts @@ -14,6 +14,7 @@ import isCompatible from './utils/isCompatible'; import { AppType } from './api/types'; import minVersionForPlatform from './utils/isCompatible/minVersionForPlatform'; import { _ } from '../../locale'; +import ViewController from './ViewController'; const uslug = require('@joplin/fork-uslug'); const logger = Logger.create('PluginService'); @@ -202,6 +203,13 @@ export default class PluginService extends BaseService { return this.plugins_[id]; } + public viewControllerByViewId(id: string): ViewController|null { + for (const [, plugin] of Object.entries(this.plugins_)) { + if (plugin.hasViewController(id)) return plugin.viewController(id); + } + return null; + } + public unserializePluginSettings(settings: SerializedPluginSettings): PluginSettings { const output = { ...settings }; diff --git a/packages/lib/services/plugins/WebviewController.ts b/packages/lib/services/plugins/WebviewController.ts index 7eed3428591..36ccf98221f 100644 --- a/packages/lib/services/plugins/WebviewController.ts +++ b/packages/lib/services/plugins/WebviewController.ts @@ -5,6 +5,10 @@ const { toSystemSlashes } = require('../../path-utils'); import PostMessageService, { MessageParticipant } from '../PostMessageService'; import { PluginViewState } from './reducer'; import { defaultWindowId } from '../../reducer'; +import Logger from '@joplin/utils/Logger'; +import CommandService from '../CommandService'; + +const logger = Logger.create('WebviewController'); export enum ContainerType { Panel = 'panel', @@ -50,12 +54,15 @@ export default class WebviewController extends ViewController { private baseDir_: string; // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied private messageListener_: Function = null; + private updateListener_: ()=> void = null; private closeResponse_: CloseResponse = null; + private containerType_: ContainerType = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public constructor(handle: ViewHandle, pluginId: string, store: any, baseDir: string, containerType: ContainerType) { super(handle, pluginId, store); this.baseDir_ = toSystemSlashes(baseDir, 'linux'); + this.containerType_ = containerType; const view: PluginViewState = { id: this.handle, @@ -138,16 +145,38 @@ export default class WebviewController extends ViewController { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public async emitMessage(event: EmitMessageEvent): Promise { - if (!this.messageListener_) return; + + if (this.containerType_ === ContainerType.Editor && !this.isActive()) { + logger.info('emitMessage: Not emitting message because editor is disabled:', this.pluginId, this.handle); + return; + } + return this.messageListener_(event.message); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + public emitUpdate() { + if (!this.updateListener_) return; + + if (this.containerType_ === ContainerType.Editor && (!this.isActive() || !this.isVisible())) { + logger.info('emitMessage: Not emitting update because editor is disabled or hidden:', this.pluginId, this.handle, this.isActive(), this.isVisible()); + return; + } + + this.updateListener_(); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public onMessage(callback: any) { this.messageListener_ = callback; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + public onUpdate(callback: any) { + this.updateListener_ = callback; + } + // --------------------------------------------- // Specific to panels // --------------------------------------------- @@ -244,14 +273,22 @@ export default class WebviewController extends ViewController { // Specific to editors // --------------------------------------------- - public async setActive(active: boolean): Promise { + public setActive(active: boolean) { this.setStoreProp('opened', active); } + public isActive(): boolean { + return this.storeView.opened; + } + public async isVisible(): Promise { if (!this.storeView.opened) return false; const shownEditorViewIds: string[] = this.store.getState().settings['plugins.shownEditorViewIds']; return shownEditorViewIds.includes(this.handle); } + public async setVisible(visible: boolean) { + await CommandService.instance().execute('showEditorPlugin', this.handle, visible); + } + } diff --git a/packages/lib/services/plugins/api/JoplinViewsEditor.ts b/packages/lib/services/plugins/api/JoplinViewsEditor.ts index b55882a54cb..0cb28bf19b3 100644 --- a/packages/lib/services/plugins/api/JoplinViewsEditor.ts +++ b/packages/lib/services/plugins/api/JoplinViewsEditor.ts @@ -1,9 +1,10 @@ /* eslint-disable multiline-comment-style */ +import eventManager from '../../../eventManager'; import Plugin from '../Plugin'; import createViewHandle from '../utils/createViewHandle'; import WebviewController, { ContainerType } from '../WebviewController'; -import { ViewHandle } from './types'; +import { ActivationCheckCallback, EditorActivationCheckFilterObject, FilterHandler, ViewHandle, UpdateCallback } from './types'; /** * Allows creating alternative note editors. When `setActive` is called, this view is going to @@ -27,6 +28,7 @@ export default class JoplinViewsEditors { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied private store: any; private plugin: Plugin; + private activationCheckHandlers_: Record> = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public constructor(plugin: Plugin, store: any) { @@ -70,6 +72,29 @@ export default class JoplinViewsEditors { return this.controller(handle).onMessage(callback); } + public async onActivationCheck(handle: ViewHandle, callback: ActivationCheckCallback): Promise { + const handler: FilterHandler = async (object) => { + const isActive = await callback(); + object.activatedEditors.push({ + pluginId: this.plugin.id, + viewId: handle, + isActive: isActive, + }); + return object; + }; + + this.activationCheckHandlers_[handle] = handler; + + eventManager.filterOn('editorActivationCheck', this.activationCheckHandlers_[handle]); + this.plugin.addOnUnloadListener(() => { + eventManager.filterOff('editorActivationCheck', this.activationCheckHandlers_[handle]); + }); + } + + public async onUpdate(handle: ViewHandle, callback: UpdateCallback): Promise { + this.controller(handle).onUpdate(callback); + } + /** * See [[JoplinViewPanels]] */ diff --git a/packages/lib/services/plugins/api/JoplinViewsPanels.ts b/packages/lib/services/plugins/api/JoplinViewsPanels.ts index 839d9ebaf35..8a8bcf8444d 100644 --- a/packages/lib/services/plugins/api/JoplinViewsPanels.ts +++ b/packages/lib/services/plugins/api/JoplinViewsPanels.ts @@ -130,4 +130,8 @@ export default class JoplinViewsPanels { return this.controller(handle).visible; } + public async isActive(handle: ViewHandle): Promise { + return this.controller(handle).isActive(); + } + } diff --git a/packages/lib/services/plugins/api/JoplinWorkspace.ts b/packages/lib/services/plugins/api/JoplinWorkspace.ts index 07fce360cca..3b10a746adc 100644 --- a/packages/lib/services/plugins/api/JoplinWorkspace.ts +++ b/packages/lib/services/plugins/api/JoplinWorkspace.ts @@ -6,7 +6,7 @@ import eventManager, { EventName } from '../../../eventManager'; import Setting from '../../../models/Setting'; import { FolderEntity } from '../../database/types'; import makeListener from '../utils/makeListener'; -import { Disposable, MenuItem } from './types'; +import { Disposable, EditContextMenuFilterObject, FilterHandler } from './types'; /** * @ignore @@ -18,12 +18,6 @@ import Note from '../../../models/Note'; */ import Folder from '../../../models/Folder'; -export interface EditContextMenuFilterObject { - items: MenuItem[]; -} - -type FilterHandler = (object: T)=> Promise; - enum ItemChangeEventType { Create = 1, Update = 2, diff --git a/packages/lib/services/plugins/api/types.ts b/packages/lib/services/plugins/api/types.ts index a51b7fad1f6..30ec0022f01 100644 --- a/packages/lib/services/plugins/api/types.ts +++ b/packages/lib/services/plugins/api/types.ts @@ -384,6 +384,26 @@ export interface Rectangle { height?: number; } +export type ActivationCheckCallback = ()=> Promise; + +export type UpdateCallback = ()=> Promise; + +export type VisibleHandler = ()=> Promise; + +export interface EditContextMenuFilterObject { + items: MenuItem[]; +} + +export interface EditorActivationCheckFilterObject { + activatedEditors: { + pluginId: string; + viewId: string; + isActive: boolean; + }[]; +} + +export type FilterHandler = (object: T)=> Promise; + // ================================================================= // Settings types // =================================================================