You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-03 23:50:33 +02:00
Desktop: Simplified and improve command service, and added command palette
- Commands "enabled" state is now expressed using a "when-clause" like in VSCode - A command palette has been added to the Tools menu
This commit is contained in:
@ -590,7 +590,24 @@ class BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
BaseModel.typeEnum_ = [['TYPE_NOTE', 1], ['TYPE_FOLDER', 2], ['TYPE_SETTING', 3], ['TYPE_RESOURCE', 4], ['TYPE_TAG', 5], ['TYPE_NOTE_TAG', 6], ['TYPE_SEARCH', 7], ['TYPE_ALARM', 8], ['TYPE_MASTER_KEY', 9], ['TYPE_ITEM_CHANGE', 10], ['TYPE_NOTE_RESOURCE', 11], ['TYPE_RESOURCE_LOCAL_STATE', 12], ['TYPE_REVISION', 13], ['TYPE_MIGRATION', 14], ['TYPE_SMART_FILTER', 15]];
|
||||
BaseModel.typeEnum_ = [
|
||||
['TYPE_NOTE', 1],
|
||||
['TYPE_FOLDER', 2],
|
||||
['TYPE_SETTING', 3],
|
||||
['TYPE_RESOURCE', 4],
|
||||
['TYPE_TAG', 5],
|
||||
['TYPE_NOTE_TAG', 6],
|
||||
['TYPE_SEARCH', 7],
|
||||
['TYPE_ALARM', 8],
|
||||
['TYPE_MASTER_KEY', 9],
|
||||
['TYPE_ITEM_CHANGE', 10],
|
||||
['TYPE_NOTE_RESOURCE', 11],
|
||||
['TYPE_RESOURCE_LOCAL_STATE', 12],
|
||||
['TYPE_REVISION', 13],
|
||||
['TYPE_MIGRATION', 14],
|
||||
['TYPE_SMART_FILTER', 15],
|
||||
['TYPE_COMMAND', 16],
|
||||
];
|
||||
|
||||
for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
|
||||
const e = BaseModel.typeEnum_[i];
|
||||
|
@ -4,27 +4,16 @@ import { _ } from 'lib/locale';
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'historyBackward',
|
||||
label: () => _('Back'),
|
||||
// iconName: 'fa-arrow-left',
|
||||
iconName: 'icon-back',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
hasBackwardNotes: boolean,
|
||||
}
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async (props:Props) => {
|
||||
if (!props.hasBackwardNotes) return;
|
||||
execute: async () => {
|
||||
utils.store.dispatch({
|
||||
type: 'HISTORY_BACKWARD',
|
||||
});
|
||||
},
|
||||
isEnabled: (props:Props) => {
|
||||
return props.hasBackwardNotes;
|
||||
},
|
||||
mapStateToProps: (state:any) => {
|
||||
return { hasBackwardNotes: state.backwardHistoryNotes.length > 0 };
|
||||
},
|
||||
enabledCondition: 'historyhasBackwardNotes',
|
||||
};
|
||||
};
|
||||
|
@ -7,23 +7,13 @@ export const declaration:CommandDeclaration = {
|
||||
iconName: 'icon-forward',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
hasForwardNotes: boolean,
|
||||
}
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async (props:Props) => {
|
||||
if (!props.hasForwardNotes) return;
|
||||
execute: async () => {
|
||||
utils.store.dispatch({
|
||||
type: 'HISTORY_FORWARD',
|
||||
});
|
||||
},
|
||||
isEnabled: (props:Props) => {
|
||||
return props.hasForwardNotes;
|
||||
},
|
||||
mapStateToProps: (state:any) => {
|
||||
return { hasForwardNotes: state.forwardHistoryNotes.length > 0 };
|
||||
},
|
||||
enabledCondition: 'historyhasForwardNotes',
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { utils, CommandRuntime, CommandDeclaration } from '../services/CommandService';
|
||||
import { utils, CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
const { reg } = require('lib/registry.js');
|
||||
|
||||
@ -8,9 +8,11 @@ export const declaration:CommandDeclaration = {
|
||||
iconName: 'fa-sync-alt',
|
||||
};
|
||||
|
||||
// Note that this command actually acts as a toggle - it starts or cancels
|
||||
// synchronisation depending on the "syncStarted" parameter
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ syncStarted }:any) => {
|
||||
execute: async (_context:CommandContext, syncStarted:boolean = false) => {
|
||||
const action = syncStarted ? 'cancel' : 'start';
|
||||
|
||||
if (!(await reg.syncTarget().isAuthenticated())) {
|
||||
@ -43,13 +45,5 @@ export const runtime = ():CommandRuntime => {
|
||||
return 'sync';
|
||||
}
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return !props.syncStarted;
|
||||
},
|
||||
mapStateToProps: (state:any):any => {
|
||||
return {
|
||||
syncStarted: state.syncStarted,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import produce, { Draft } from 'immer';
|
||||
import pluginServiceReducer, { stateRootKey as pluginServiceStateRootKey, defaultState as pluginServiceDefaultState, State as PluginServiceState } from 'lib/services/plugins/reducer';
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const ArrayUtils = require('lib/ArrayUtils.js');
|
||||
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');
|
||||
const { createSelectorCreator, defaultMemoize } = require('reselect');
|
||||
@ -160,8 +161,6 @@ for (const additionalReducer of additionalReducers) {
|
||||
|
||||
export const MAX_HISTORY = 200;
|
||||
|
||||
export const stateUtils:any = {};
|
||||
|
||||
const derivedStateCache_:any = {};
|
||||
|
||||
// Allows, for a given state, to return the same derived
|
||||
@ -185,9 +184,7 @@ const createShallowArrayEqualSelector = createSelectorCreator(
|
||||
}
|
||||
);
|
||||
|
||||
// Given an input array, this selector ensures that the same array is returned
|
||||
// if its content hasn't changed.
|
||||
stateUtils.selectArrayShallow = createCachedSelector(
|
||||
const selectArrayShallow = createCachedSelector(
|
||||
(state:any) => state.array,
|
||||
(array:any[]) => array
|
||||
)({
|
||||
@ -197,85 +194,85 @@ stateUtils.selectArrayShallow = createCachedSelector(
|
||||
selectorCreator: createShallowArrayEqualSelector,
|
||||
});
|
||||
|
||||
stateUtils.hasOneSelectedNote = function(state:State):boolean {
|
||||
return state.selectedNoteIds.length === 1;
|
||||
};
|
||||
class StateUtils {
|
||||
|
||||
stateUtils.notesOrder = function(stateSettings:any) {
|
||||
if (stateSettings['notes.sortOrder.field'] === 'order') {
|
||||
return cacheEnabledOutput('notesOrder', [
|
||||
{
|
||||
by: 'order',
|
||||
dir: 'DESC',
|
||||
},
|
||||
{
|
||||
by: 'user_created_time',
|
||||
dir: 'DESC',
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
return cacheEnabledOutput('notesOrder', [
|
||||
{
|
||||
by: stateSettings['notes.sortOrder.field'],
|
||||
dir: stateSettings['notes.sortOrder.reverse'] ? 'DESC' : 'ASC',
|
||||
},
|
||||
]);
|
||||
// Given an input array, this selector ensures that the same array is returned
|
||||
// if its content hasn't changed.
|
||||
public selectArrayShallow(props:any, cacheKey:any) {
|
||||
return selectArrayShallow(props, cacheKey);
|
||||
}
|
||||
};
|
||||
|
||||
stateUtils.foldersOrder = function(stateSettings:any) {
|
||||
return cacheEnabledOutput('foldersOrder', [
|
||||
{
|
||||
by: stateSettings['folders.sortOrder.field'],
|
||||
dir: stateSettings['folders.sortOrder.reverse'] ? 'DESC' : 'ASC',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
stateUtils.hasNotesBeingSaved = function(state:State):boolean {
|
||||
for (const id in state.editorNoteStatuses) {
|
||||
if (state.editorNoteStatuses[id] === 'saving') return true;
|
||||
public oneNoteSelected(state:State):boolean {
|
||||
return state.selectedNoteIds.length === 1;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
stateUtils.parentItem = function(state:State) {
|
||||
const t = state.notesParentType;
|
||||
let id = null;
|
||||
if (t === 'Folder') id = state.selectedFolderId;
|
||||
if (t === 'Tag') id = state.selectedTagId;
|
||||
if (t === 'Search') id = state.selectedSearchId;
|
||||
if (!t || !id) return null;
|
||||
return { type: t, id: id };
|
||||
};
|
||||
|
||||
stateUtils.lastSelectedNoteIds = function(state:State):string[] {
|
||||
const parent = stateUtils.parentItem(state);
|
||||
if (!parent) return [];
|
||||
const output = (state.lastSelectedNotesIds as any)[parent.type][parent.id];
|
||||
return output ? output : [];
|
||||
};
|
||||
|
||||
stateUtils.getCurrentNote = function(state:State) {
|
||||
const selectedNoteIds = state.selectedNoteIds;
|
||||
const notes = state.notes;
|
||||
if (selectedNoteIds != null && selectedNoteIds.length > 0) {
|
||||
const currNote = notes.find(note => note.id === selectedNoteIds[0]);
|
||||
if (currNote != null) {
|
||||
return {
|
||||
id: currNote.id,
|
||||
parent_id: currNote.parent_id,
|
||||
notesParentType: state.notesParentType,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
selectedTagId: state.selectedTagId,
|
||||
selectedSearchId: state.selectedSearchId,
|
||||
searches: state.searches,
|
||||
selectedSmartFilterId: state.selectedSmartFilterId,
|
||||
};
|
||||
public notesOrder(stateSettings:any) {
|
||||
if (stateSettings['notes.sortOrder.field'] === 'order') {
|
||||
return cacheEnabledOutput('notesOrder', [
|
||||
{
|
||||
by: 'order',
|
||||
dir: 'DESC',
|
||||
},
|
||||
{
|
||||
by: 'user_created_time',
|
||||
dir: 'DESC',
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
return cacheEnabledOutput('notesOrder', [
|
||||
{
|
||||
by: stateSettings['notes.sortOrder.field'],
|
||||
dir: stateSettings['notes.sortOrder.reverse'] ? 'DESC' : 'ASC',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
public foldersOrder(stateSettings:any) {
|
||||
return cacheEnabledOutput('foldersOrder', [
|
||||
{
|
||||
by: stateSettings['folders.sortOrder.field'],
|
||||
dir: stateSettings['folders.sortOrder.reverse'] ? 'DESC' : 'ASC',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
public hasNotesBeingSaved(state:State):boolean {
|
||||
for (const id in state.editorNoteStatuses) {
|
||||
if (state.editorNoteStatuses[id] === 'saving') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public parentItem(state:State) {
|
||||
const t = state.notesParentType;
|
||||
let id = null;
|
||||
if (t === 'Folder') id = state.selectedFolderId;
|
||||
if (t === 'Tag') id = state.selectedTagId;
|
||||
if (t === 'Search') id = state.selectedSearchId;
|
||||
if (!t || !id) return null;
|
||||
return { type: t, id: id };
|
||||
}
|
||||
|
||||
public lastSelectedNoteIds(state:State):string[] {
|
||||
const parent = this.parentItem(state);
|
||||
if (!parent) return [];
|
||||
const output = (state.lastSelectedNotesIds as any)[parent.type][parent.id];
|
||||
return output ? output : [];
|
||||
}
|
||||
|
||||
public selectedNote(state:State):any {
|
||||
const noteId = this.selectedNoteId(state);
|
||||
return noteId ? BaseModel.byId(state.notes, noteId) : null;
|
||||
}
|
||||
|
||||
public selectedNoteId(state:State):any {
|
||||
return state.selectedNoteIds.length ? state.selectedNoteIds[0] : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const stateUtils:StateUtils = new StateUtils();
|
||||
|
||||
function arrayHasEncryptedItems(array:any[]) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
@ -526,8 +523,29 @@ const getContextFromHistory = (ctx:any) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
function getNoteHistoryInfo(state:State) {
|
||||
const selectedNoteIds = state.selectedNoteIds;
|
||||
const notes = state.notes;
|
||||
if (selectedNoteIds != null && selectedNoteIds.length > 0) {
|
||||
const currNote = notes.find(note => note.id === selectedNoteIds[0]);
|
||||
if (currNote != null) {
|
||||
return {
|
||||
id: currNote.id,
|
||||
parent_id: currNote.parent_id,
|
||||
notesParentType: state.notesParentType,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
selectedTagId: state.selectedTagId,
|
||||
selectedSearchId: state.selectedSearchId,
|
||||
searches: state.searches,
|
||||
selectedSmartFilterId: state.selectedSmartFilterId,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleHistory(draft:Draft<State>, action:any) {
|
||||
const currentNote = stateUtils.getCurrentNote(draft);
|
||||
const currentNote = getNoteHistoryInfo(draft);
|
||||
switch (action.type) {
|
||||
case 'HISTORY_BACKWARD': {
|
||||
const note = draft.backwardHistoryNotes[draft.backwardHistoryNotes.length - 1];
|
||||
@ -1086,6 +1104,7 @@ const reducer = produce((draft: Draft<State> = defaultState, action:any) => {
|
||||
const newPluginsLegacy = Object.assign({}, draft.pluginsLegacy);
|
||||
const newPlugin = draft.pluginsLegacy[action.pluginName] ? Object.assign({}, draft.pluginsLegacy[action.pluginName]) : {};
|
||||
if ('open' in action) newPlugin.dialogOpen = action.open;
|
||||
if ('userData' in action) newPlugin.userData = action.userData;
|
||||
newPluginsLegacy[action.pluginName] = newPlugin;
|
||||
draft.pluginsLegacy = newPluginsLegacy;
|
||||
}
|
||||
|
@ -1,30 +1,23 @@
|
||||
import { State } from 'lib/reducer';
|
||||
import eventManager from 'lib/eventManager';
|
||||
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from 'lib/markdownUtils';
|
||||
import BaseService from 'lib/services/BaseService';
|
||||
import shim from 'lib/shim';
|
||||
import WhenClause from './WhenClause';
|
||||
import stateToWhenClauseContext from './commands/stateToWhenClauseContext';
|
||||
|
||||
type LabelFunction = () => string;
|
||||
type EnabledCondition = string;
|
||||
|
||||
export interface CommandContext {
|
||||
// The state may also be of type "AppState" (used by the desktop app), which inherits from "State" (used by all apps)
|
||||
state: State,
|
||||
}
|
||||
|
||||
export interface CommandRuntime {
|
||||
execute(props:any):Promise<any>
|
||||
isEnabled?(props:any):boolean
|
||||
|
||||
// "state" type is "AppState" but in order not to introduce a
|
||||
// dependency to the desktop app (so that the service can
|
||||
// potentially be used by the mobile app too), we keep it as "any".
|
||||
// Individual commands can define it as state:AppState when relevant.
|
||||
//
|
||||
// In general this method should reduce the provided state to only
|
||||
// what's absolutely necessary. For example, if the property of a
|
||||
// note is needed, return only that particular property and not the
|
||||
// whole note object. This will ensure that components that depends
|
||||
// on this command are not uncessarily re-rendered. A note object for
|
||||
// example might change frequently but its markdown_language property
|
||||
// will almost never change.
|
||||
mapStateToProps?(state:any):any
|
||||
|
||||
execute(context:CommandContext, ...args:any[]):Promise<any>
|
||||
enabledCondition?: EnabledCondition;
|
||||
// Used for the (optional) toolbar button title
|
||||
title?(props:any):string,
|
||||
mapStateToTitle?(state:any):string,
|
||||
}
|
||||
|
||||
export interface CommandDeclaration {
|
||||
@ -33,6 +26,9 @@ export interface CommandDeclaration {
|
||||
// Used for the menu item label, and toolbar button tooltip
|
||||
label?: LabelFunction | string,
|
||||
|
||||
// Command description - if none is provided, the label will be used as description
|
||||
description?: string,
|
||||
|
||||
// This is a bit of a hack because some labels don't make much sense in isolation. For example,
|
||||
// the commmand to focus the note list is called just "Note list". This makes sense within the menu
|
||||
// but not so much within the keymap config screen, where the parent item is not displayed. Because
|
||||
@ -88,30 +84,29 @@ interface CommandByNameOptions {
|
||||
runtimeMustBeRegistered?:boolean,
|
||||
}
|
||||
|
||||
interface CommandState {
|
||||
export interface SearchResult {
|
||||
commandName: string,
|
||||
title: string,
|
||||
enabled: boolean,
|
||||
}
|
||||
|
||||
interface CommandStates {
|
||||
[key:string]: CommandState
|
||||
}
|
||||
|
||||
export default class CommandService extends BaseService {
|
||||
|
||||
private static instance_:CommandService;
|
||||
|
||||
static instance():CommandService {
|
||||
public static instance():CommandService {
|
||||
if (this.instance_) return this.instance_;
|
||||
this.instance_ = new CommandService();
|
||||
return this.instance_;
|
||||
}
|
||||
|
||||
private commands_:Commands = {};
|
||||
private commandPreviousStates_:CommandStates = {};
|
||||
private store_:any;
|
||||
private devMode_:boolean;
|
||||
|
||||
initialize(store:any) {
|
||||
public initialize(store:any, devMode:boolean) {
|
||||
utils.store = store;
|
||||
this.store_ = store;
|
||||
this.devMode_ = devMode;
|
||||
}
|
||||
|
||||
public on(eventName:string, callback:Function) {
|
||||
@ -122,6 +117,36 @@ export default class CommandService extends BaseService {
|
||||
eventManager.off(eventName, callback);
|
||||
}
|
||||
|
||||
public searchCommands(query:string, returnAllWhenEmpty:boolean, excludeWithoutLabel:boolean = true):SearchResult[] {
|
||||
query = query.toLowerCase();
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const commandName of this.commandNames()) {
|
||||
const label = this.label(commandName, true);
|
||||
if (!label && excludeWithoutLabel) continue;
|
||||
|
||||
const title = label ? `${label} (${commandName})` : commandName;
|
||||
|
||||
if ((returnAllWhenEmpty && !query) || title.toLowerCase().includes(query)) {
|
||||
output.push({
|
||||
commandName: commandName,
|
||||
title: title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
output.sort((a:SearchResult, b:SearchResult) => {
|
||||
return a.title.toLowerCase() < b.title.toLowerCase() ? -1 : +1;
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public commandNames() {
|
||||
return Object.keys(this.commands_);
|
||||
}
|
||||
|
||||
public commandByName(name:string, options:CommandByNameOptions = null):Command {
|
||||
options = {
|
||||
mustExist: true,
|
||||
@ -140,7 +165,7 @@ export default class CommandService extends BaseService {
|
||||
return command;
|
||||
}
|
||||
|
||||
registerDeclaration(declaration:CommandDeclaration) {
|
||||
public registerDeclaration(declaration:CommandDeclaration) {
|
||||
declaration = { ...declaration };
|
||||
if (!declaration.label) declaration.label = '';
|
||||
if (!declaration.iconName) declaration.iconName = '';
|
||||
@ -148,83 +173,89 @@ export default class CommandService extends BaseService {
|
||||
this.commands_[declaration.name] = {
|
||||
declaration: declaration,
|
||||
};
|
||||
|
||||
delete this.commandPreviousStates_[declaration.name];
|
||||
}
|
||||
|
||||
registerRuntime(commandName:string, runtime:CommandRuntime) {
|
||||
public registerRuntime(commandName:string, runtime:CommandRuntime) {
|
||||
if (typeof commandName !== 'string') throw new Error(`Command name must be a string. Got: ${JSON.stringify(commandName)}`);
|
||||
|
||||
const command = this.commandByName(commandName);
|
||||
|
||||
runtime = Object.assign({}, runtime);
|
||||
if (!runtime.isEnabled) runtime.isEnabled = () => true;
|
||||
if (!runtime.title) runtime.title = () => null;
|
||||
if (!runtime.enabledCondition) runtime.enabledCondition = 'true';
|
||||
command.runtime = runtime;
|
||||
|
||||
delete this.commandPreviousStates_[commandName];
|
||||
}
|
||||
|
||||
componentRegisterCommands(component:any, commands:any[]) {
|
||||
public componentRegisterCommands(component:any, commands:any[]) {
|
||||
for (const command of commands) {
|
||||
CommandService.instance().registerRuntime(command.declaration.name, command.runtime(component));
|
||||
}
|
||||
}
|
||||
|
||||
componentUnregisterCommands(commands:any[]) {
|
||||
public componentUnregisterCommands(commands:any[]) {
|
||||
for (const command of commands) {
|
||||
CommandService.instance().unregisterRuntime(command.declaration.name);
|
||||
}
|
||||
}
|
||||
|
||||
unregisterRuntime(commandName:string) {
|
||||
public unregisterRuntime(commandName:string) {
|
||||
const command = this.commandByName(commandName, { mustExist: false });
|
||||
if (!command || !command.runtime) return;
|
||||
delete command.runtime;
|
||||
|
||||
delete this.commandPreviousStates_[commandName];
|
||||
}
|
||||
|
||||
async execute(commandName:string, props:any = null):Promise<any> {
|
||||
public async execute(commandName:string, ...args:any[]):Promise<any> {
|
||||
const command = this.commandByName(commandName);
|
||||
this.logger().info('CommandService::execute:', commandName, props);
|
||||
return command.runtime.execute(props ? props : {});
|
||||
this.logger().info('CommandService::execute:', commandName, args);
|
||||
return command.runtime.execute({ state: this.store_.getState() }, ...args);
|
||||
}
|
||||
|
||||
scheduleExecute(commandName:string, args:any) {
|
||||
public scheduleExecute(commandName:string, args:any) {
|
||||
shim.setTimeout(() => {
|
||||
this.execute(commandName, args);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
isEnabled(commandName:string, props:any):boolean {
|
||||
public currentWhenClauseContext() {
|
||||
return stateToWhenClauseContext(this.store_.getState());
|
||||
}
|
||||
|
||||
// When looping on commands and checking their enabled state, the whenClauseContext
|
||||
// should be specified (created using currentWhenClauseContext) to avoid having
|
||||
// to re-create it on each call.
|
||||
public isEnabled(commandName:string, whenClauseContext:any = null):boolean {
|
||||
const command = this.commandByName(commandName);
|
||||
if (!command || !command.runtime) return false;
|
||||
// if (!command.runtime.props) return false;
|
||||
return command.runtime.isEnabled(props);
|
||||
|
||||
if (!whenClauseContext) whenClauseContext = this.currentWhenClauseContext();
|
||||
|
||||
const exp = new WhenClause(command.runtime.enabledCondition, this.devMode_);
|
||||
return exp.evaluate(whenClauseContext);
|
||||
}
|
||||
|
||||
commandMapStateToProps(commandName:string, state:any):any {
|
||||
const command = this.commandByName(commandName);
|
||||
if (!command.runtime) return null;
|
||||
if (!command.runtime.mapStateToProps) return {};
|
||||
return command.runtime.mapStateToProps(state);
|
||||
}
|
||||
|
||||
title(commandName:string, props:any):string {
|
||||
// The title is dynamic and derived from the state, which is why the state is passed
|
||||
// as an argument. Title can be used for example to display the alarm date on the
|
||||
// "set alarm" toolbar button.
|
||||
public title(commandName:string, state:any = null):string {
|
||||
const command = this.commandByName(commandName);
|
||||
if (!command || !command.runtime) return null;
|
||||
return command.runtime.title(props);
|
||||
|
||||
state = state || this.store_.getState();
|
||||
|
||||
if (command.runtime.mapStateToTitle) {
|
||||
return command.runtime.mapStateToTitle(state);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
iconName(commandName:string, variant:string = null):string {
|
||||
public iconName(commandName:string, variant:string = null):string {
|
||||
const command = this.commandByName(commandName);
|
||||
if (!command) throw new Error(`No such command: ${commandName}`);
|
||||
if (variant === 'tinymce') return command.declaration.tinymceIconName ? command.declaration.tinymceIconName : 'preferences';
|
||||
return command.declaration.iconName;
|
||||
}
|
||||
|
||||
label(commandName:string, fullLabel:boolean = false):string {
|
||||
public label(commandName:string, fullLabel:boolean = false):string {
|
||||
const command = this.commandByName(commandName);
|
||||
if (!command) throw new Error(`Command: ${commandName} is not declared`);
|
||||
const output = [];
|
||||
@ -240,42 +271,15 @@ export default class CommandService extends BaseService {
|
||||
return output.join(': ');
|
||||
}
|
||||
|
||||
exists(commandName:string):boolean {
|
||||
public description(commandName:string):string {
|
||||
const command = this.commandByName(commandName);
|
||||
if (command.declaration.description) return command.declaration.description;
|
||||
return this.label(commandName, true);
|
||||
}
|
||||
|
||||
public exists(commandName:string):boolean {
|
||||
const command = this.commandByName(commandName, { mustExist: false });
|
||||
return !!command;
|
||||
}
|
||||
|
||||
public commandsToMarkdownTable(state:any):string {
|
||||
const headers:MarkdownTableHeader[] = [
|
||||
{
|
||||
name: 'commandName',
|
||||
label: 'Name',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
},
|
||||
{
|
||||
name: 'props',
|
||||
label: 'Props',
|
||||
},
|
||||
];
|
||||
|
||||
const rows:MarkdownTableRow[] = [];
|
||||
|
||||
for (const commandName in this.commands_) {
|
||||
const props = this.commandMapStateToProps(commandName, state);
|
||||
|
||||
const row:MarkdownTableRow = {
|
||||
commandName: commandName,
|
||||
description: this.label(commandName),
|
||||
props: JSON.stringify(props),
|
||||
};
|
||||
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
return markdownUtils.createMarkdownTable(headers, rows);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ const defaultKeymapItems = {
|
||||
{ accelerator: 'Cmd+N', command: 'newNote' },
|
||||
{ accelerator: 'Cmd+T', command: 'newTodo' },
|
||||
{ accelerator: 'Cmd+S', command: 'synchronize' },
|
||||
{ accelerator: 'Cmd+P', command: 'print' },
|
||||
{ accelerator: '', command: 'print' },
|
||||
{ accelerator: 'Cmd+H', command: 'hideApp' },
|
||||
{ accelerator: 'Cmd+Q', command: 'quit' },
|
||||
{ accelerator: 'Cmd+,', command: 'config' },
|
||||
@ -37,20 +37,21 @@ const defaultKeymapItems = {
|
||||
{ accelerator: 'Shift+Cmd+L', command: 'focusElementNoteList' },
|
||||
{ accelerator: 'Shift+Cmd+N', command: 'focusElementNoteTitle' },
|
||||
{ accelerator: 'Shift+Cmd+B', command: 'focusElementNoteBody' },
|
||||
{ accelerator: 'Option+Cmd+S', command: 'toggleSidebar' },
|
||||
{ accelerator: 'Option+Cmd+S', command: 'toggleSideBar' },
|
||||
{ accelerator: 'Option+Cmd+L', command: 'toggleNoteList' },
|
||||
{ accelerator: 'Cmd+L', command: 'toggleVisiblePanes' },
|
||||
{ accelerator: 'Cmd+0', command: 'zoomActualSize' },
|
||||
{ accelerator: 'Cmd+E', command: 'toggleExternalEditing' },
|
||||
{ accelerator: 'Option+Cmd+T', command: 'setTags' },
|
||||
{ accelerator: 'Cmd+G', command: 'gotoAnything' },
|
||||
{ accelerator: 'Cmd+P', command: 'gotoAnything' },
|
||||
{ accelerator: 'Shift+Cmd+P', command: 'commandPalette' },
|
||||
{ accelerator: 'F1', command: 'help' },
|
||||
],
|
||||
default: [
|
||||
{ accelerator: 'Ctrl+N', command: 'newNote' },
|
||||
{ accelerator: 'Ctrl+T', command: 'newTodo' },
|
||||
{ accelerator: 'Ctrl+S', command: 'synchronize' },
|
||||
{ accelerator: 'Ctrl+P', command: 'print' },
|
||||
{ accelerator: '', command: 'print' },
|
||||
{ accelerator: 'Ctrl+Q', command: 'quit' },
|
||||
{ accelerator: 'Ctrl+Alt+I', command: 'insertTemplate' },
|
||||
{ accelerator: 'Ctrl+C', command: 'textCopy' },
|
||||
@ -68,14 +69,15 @@ const defaultKeymapItems = {
|
||||
{ accelerator: 'Ctrl+Shift+L', command: 'focusElementNoteList' },
|
||||
{ accelerator: 'Ctrl+Shift+N', command: 'focusElementNoteTitle' },
|
||||
{ accelerator: 'Ctrl+Shift+B', command: 'focusElementNoteBody' },
|
||||
{ accelerator: 'F10', command: 'toggleSidebar' },
|
||||
{ accelerator: 'F10', command: 'toggleSideBar' },
|
||||
{ accelerator: 'F11', command: 'toggleNoteList' },
|
||||
{ accelerator: 'Ctrl+L', command: 'toggleVisiblePanes' },
|
||||
{ accelerator: 'Ctrl+0', command: 'zoomActualSize' },
|
||||
{ accelerator: 'Ctrl+E', command: 'toggleExternalEditing' },
|
||||
{ accelerator: 'Ctrl+Alt+T', command: 'setTags' },
|
||||
{ accelerator: 'Ctrl+,', command: 'config' },
|
||||
{ accelerator: 'Ctrl+G', command: 'gotoAnything' },
|
||||
{ accelerator: 'Ctrl+P', command: 'gotoAnything' },
|
||||
{ accelerator: 'Ctrl+Shift+P', command: 'commandPalette' },
|
||||
{ accelerator: 'F1', command: 'help' },
|
||||
],
|
||||
};
|
||||
@ -90,13 +92,14 @@ interface Keymap {
|
||||
}
|
||||
|
||||
export default class KeymapService extends BaseService {
|
||||
|
||||
private keymap: Keymap;
|
||||
private platform: string;
|
||||
private customKeymapPath: string;
|
||||
private defaultKeymapItems: KeymapItem[];
|
||||
private lastSaveTime_:number;
|
||||
|
||||
constructor() {
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this.lastSaveTime_ = Date.now();
|
||||
@ -106,11 +109,11 @@ export default class KeymapService extends BaseService {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
get lastSaveTime():number {
|
||||
public get lastSaveTime():number {
|
||||
return this.lastSaveTime_;
|
||||
}
|
||||
|
||||
initialize(platform: string = shim.platformName()) {
|
||||
public initialize(platform: string = shim.platformName()) {
|
||||
this.platform = platform;
|
||||
|
||||
switch (platform) {
|
||||
@ -131,7 +134,7 @@ export default class KeymapService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
async loadCustomKeymap(customKeymapPath: string) {
|
||||
public async loadCustomKeymap(customKeymapPath: string) {
|
||||
this.customKeymapPath = customKeymapPath; // Useful for saving the changes later
|
||||
|
||||
if (await shim.fsDriver().exists(customKeymapPath)) {
|
||||
@ -143,7 +146,7 @@ export default class KeymapService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
async saveCustomKeymap(customKeymapPath: string = this.customKeymapPath) {
|
||||
public async saveCustomKeymap(customKeymapPath: string = this.customKeymapPath) {
|
||||
this.logger().info(`KeymapService: Saving keymap to file: ${customKeymapPath}`);
|
||||
|
||||
try {
|
||||
@ -161,7 +164,7 @@ export default class KeymapService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
acceleratorExists(command: string) {
|
||||
public acceleratorExists(command: string) {
|
||||
return !!this.keymap[command];
|
||||
}
|
||||
|
||||
@ -189,33 +192,33 @@ export default class KeymapService extends BaseService {
|
||||
};
|
||||
}
|
||||
|
||||
setAccelerator(command: string, accelerator: string) {
|
||||
public setAccelerator(command: string, accelerator: string) {
|
||||
this.keymap[command].accelerator = accelerator;
|
||||
}
|
||||
|
||||
getAccelerator(command: string) {
|
||||
public getAccelerator(command: string) {
|
||||
const item = this.keymap[command];
|
||||
if (!item) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
||||
|
||||
return item.accelerator;
|
||||
}
|
||||
|
||||
getDefaultAccelerator(command: string) {
|
||||
public getDefaultAccelerator(command: string) {
|
||||
const defaultItem = this.defaultKeymapItems.find((item => item.command === command));
|
||||
if (!defaultItem) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
||||
|
||||
return defaultItem.accelerator;
|
||||
}
|
||||
|
||||
getCommandNames() {
|
||||
public getCommandNames() {
|
||||
return Object.keys(this.keymap);
|
||||
}
|
||||
|
||||
getKeymapItems() {
|
||||
public getKeymapItems() {
|
||||
return Object.values(this.keymap);
|
||||
}
|
||||
|
||||
getCustomKeymapItems() {
|
||||
public getCustomKeymapItems() {
|
||||
const customkeymapItems: KeymapItem[] = [];
|
||||
this.defaultKeymapItems.forEach(({ command, accelerator }) => {
|
||||
const currentAccelerator = this.getAccelerator(command);
|
||||
@ -236,11 +239,11 @@ export default class KeymapService extends BaseService {
|
||||
return customkeymapItems;
|
||||
}
|
||||
|
||||
getDefaultKeymapItems() {
|
||||
public getDefaultKeymapItems() {
|
||||
return [...this.defaultKeymapItems];
|
||||
}
|
||||
|
||||
overrideKeymap(customKeymapItems: KeymapItem[]) {
|
||||
public overrideKeymap(customKeymapItems: KeymapItem[]) {
|
||||
try {
|
||||
for (let i = 0; i < customKeymapItems.length; i++) {
|
||||
const item = customKeymapItems[i];
|
||||
@ -284,7 +287,7 @@ export default class KeymapService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
validateKeymap(proposedKeymapItem: KeymapItem = null) {
|
||||
public validateKeymap(proposedKeymapItem: KeymapItem = null) {
|
||||
const usedAccelerators = new Set();
|
||||
|
||||
// Validate as if the proposed change is already present in the current keymap
|
||||
@ -312,7 +315,7 @@ export default class KeymapService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
validateAccelerator(accelerator: string) {
|
||||
public validateAccelerator(accelerator: string) {
|
||||
let keyFound = false;
|
||||
|
||||
const parts = accelerator.split('+');
|
||||
@ -334,7 +337,7 @@ export default class KeymapService extends BaseService {
|
||||
if (!isValid) throw new Error(_('Accelerator "%s" is not valid.', accelerator));
|
||||
}
|
||||
|
||||
domToElectronAccelerator(event: KeyboardEvent<HTMLDivElement>) {
|
||||
public domToElectronAccelerator(event: KeyboardEvent<HTMLDivElement>) {
|
||||
const parts = [];
|
||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = event;
|
||||
|
||||
@ -358,7 +361,7 @@ export default class KeymapService extends BaseService {
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
static domToElectronKey(domKey: string) {
|
||||
private static domToElectronKey(domKey: string) {
|
||||
let electronKey;
|
||||
|
||||
if (/^([a-z])$/.test(domKey)) {
|
||||
@ -398,7 +401,7 @@ export default class KeymapService extends BaseService {
|
||||
|
||||
private static instance_:KeymapService = null;
|
||||
|
||||
static instance():KeymapService {
|
||||
public static instance():KeymapService {
|
||||
if (this.instance_) return this.instance_;
|
||||
|
||||
this.instance_ = new KeymapService();
|
||||
|
@ -1,5 +1,4 @@
|
||||
const Logger = require('lib/Logger').default;
|
||||
const KeymapService = require('lib/services/KeymapService').default;
|
||||
|
||||
class PluginManager {
|
||||
constructor() {
|
||||
@ -52,6 +51,7 @@ class PluginManager {
|
||||
const p = this.pluginInstance_(event.pluginName);
|
||||
p.onTrigger({
|
||||
itemName: event.itemName,
|
||||
userData: event.userData,
|
||||
});
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ class PluginManager {
|
||||
|
||||
return {
|
||||
Dialog: Class.Dialog,
|
||||
props: this.dialogProps_(name),
|
||||
props: Object.assign({}, this.dialogProps_(name), { userData: p.userData }),
|
||||
};
|
||||
}
|
||||
|
||||
@ -81,20 +81,24 @@ class PluginManager {
|
||||
|
||||
menuItems() {
|
||||
let output = [];
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
for (const name in this.plugins_) {
|
||||
const menuItems = this.plugins_[name].Class.manifest.menuItems;
|
||||
const menuItems = this.plugins_[name].Class.manifest.menuItems.slice();
|
||||
if (!menuItems) continue;
|
||||
|
||||
for (const item of menuItems) {
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
const item = Object.assign({}, menuItems[i]);
|
||||
|
||||
item.click = () => {
|
||||
this.onPluginMenuItemTrigger_({
|
||||
pluginName: name,
|
||||
itemName: item.name,
|
||||
userData: item.userData,
|
||||
});
|
||||
};
|
||||
item.accelerator = keymapService.getAccelerator(name);
|
||||
|
||||
item.accelerator = menuItems[i].accelerator();
|
||||
|
||||
menuItems[i] = item;
|
||||
}
|
||||
|
||||
output = output.concat(menuItems);
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { ContextKeyExpr, ContextKeyExpression } from './contextkey/contextkey';
|
||||
|
||||
export default class BooleanExpression {
|
||||
export default class WhenClause {
|
||||
|
||||
private expression_:string;
|
||||
private validate_:boolean;
|
||||
private rules_:ContextKeyExpression = null;
|
||||
|
||||
constructor(expression:string) {
|
||||
constructor(expression:string, validate:boolean) {
|
||||
this.expression_ = expression;
|
||||
this.validate_ = validate;
|
||||
}
|
||||
|
||||
private createContext(ctx: any) {
|
||||
@ -21,11 +23,20 @@ export default class BooleanExpression {
|
||||
if (!this.rules_) {
|
||||
this.rules_ = ContextKeyExpr.deserialize(this.expression_);
|
||||
}
|
||||
|
||||
return this.rules_;
|
||||
}
|
||||
|
||||
public evaluate(context:any):boolean {
|
||||
if (this.validate_) this.validate(context);
|
||||
return this.rules.evaluate(this.createContext(context));
|
||||
}
|
||||
|
||||
public validate(context:any) {
|
||||
const keys = this.rules.keys();
|
||||
for (const key of keys) {
|
||||
if (!(key in context)) throw new Error(`No such key: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -87,9 +87,9 @@ export default class MenuUtils {
|
||||
return item;
|
||||
}
|
||||
|
||||
public commandToStatefulMenuItem(commandName:string, props:any = null):MenuItem {
|
||||
public commandToStatefulMenuItem(commandName:string, ...args:any[]):MenuItem {
|
||||
return this.commandToMenuItem(commandName, () => {
|
||||
return this.service.execute(commandName, props ? props : {});
|
||||
return this.service.execute(commandName, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
@ -108,11 +108,14 @@ export default class MenuUtils {
|
||||
return output;
|
||||
}
|
||||
|
||||
public commandsToMenuItemProps(state:any, commandNames:string[]):MenuItemProps {
|
||||
public commandsToMenuItemProps(commandNames:string[], whenClauseContext:any):MenuItemProps {
|
||||
const output:MenuItemProps = {};
|
||||
|
||||
for (const commandName of commandNames) {
|
||||
const newProps = this.service.commandMapStateToProps(commandName, state);
|
||||
const newProps = {
|
||||
enabled: this.service.isEnabled(commandName, whenClauseContext),
|
||||
};
|
||||
|
||||
if (newProps === null || propsHaveChanged(this.menuItemPropsCache_[commandName], newProps)) {
|
||||
output[commandName] = newProps;
|
||||
this.menuItemPropsCache_[commandName] = newProps;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import CommandService from '../CommandService';
|
||||
import propsHaveChanged from './propsHaveChanged';
|
||||
import CommandService from 'lib/services/CommandService';
|
||||
import { stateUtils } from 'lib/reducer';
|
||||
|
||||
const separatorItem = { type: 'separator' };
|
||||
@ -14,7 +13,6 @@ export interface ToolbarButtonInfo {
|
||||
}
|
||||
|
||||
interface ToolbarButtonCacheItem {
|
||||
props: any,
|
||||
info: ToolbarButtonInfo,
|
||||
}
|
||||
|
||||
@ -35,8 +33,15 @@ export default class ToolbarButtonUtils {
|
||||
return this.service_;
|
||||
}
|
||||
|
||||
private commandToToolbarButton(commandName:string, props:any):ToolbarButtonInfo {
|
||||
if (this.toolbarButtonCache_[commandName] && !propsHaveChanged(this.toolbarButtonCache_[commandName].props, props)) {
|
||||
private commandToToolbarButton(commandName:string, whenClauseContext:any):ToolbarButtonInfo {
|
||||
const newEnabled = this.service.isEnabled(commandName, whenClauseContext);
|
||||
const newTitle = this.service.title(commandName);
|
||||
|
||||
if (
|
||||
this.toolbarButtonCache_[commandName] &&
|
||||
this.toolbarButtonCache_[commandName].info.enabled === newEnabled &&
|
||||
this.toolbarButtonCache_[commandName].info.title === newTitle
|
||||
) {
|
||||
return this.toolbarButtonCache_[commandName].info;
|
||||
}
|
||||
|
||||
@ -46,15 +51,14 @@ export default class ToolbarButtonUtils {
|
||||
name: commandName,
|
||||
tooltip: this.service.label(commandName),
|
||||
iconName: command.declaration.iconName,
|
||||
enabled: this.service.isEnabled(commandName, props),
|
||||
enabled: newEnabled,
|
||||
onClick: async () => {
|
||||
this.service.execute(commandName, props);
|
||||
this.service.execute(commandName);
|
||||
},
|
||||
title: this.service.title(commandName, props),
|
||||
title: newTitle,
|
||||
};
|
||||
|
||||
this.toolbarButtonCache_[commandName] = {
|
||||
props: props,
|
||||
info: output,
|
||||
};
|
||||
|
||||
@ -64,7 +68,7 @@ export default class ToolbarButtonUtils {
|
||||
// This method ensures that if the provided commandNames and state hasn't changed
|
||||
// the output also won't change. Invididual toolbarButtonInfo also won't changed
|
||||
// if the state they use hasn't changed. This is to avoid useless renders of the toolbars.
|
||||
public commandsToToolbarButtons(state:any, commandNames:string[]):ToolbarButtonInfo[] {
|
||||
public commandsToToolbarButtons(commandNames:string[], whenClauseContext:any):ToolbarButtonInfo[] {
|
||||
const output:ToolbarButtonInfo[] = [];
|
||||
|
||||
for (const commandName of commandNames) {
|
||||
@ -73,8 +77,7 @@ export default class ToolbarButtonUtils {
|
||||
continue;
|
||||
}
|
||||
|
||||
const props = this.service.commandMapStateToProps(commandName, state);
|
||||
output.push(this.commandToToolbarButton(commandName, props));
|
||||
output.push(this.commandToToolbarButton(commandName, whenClauseContext));
|
||||
}
|
||||
|
||||
return stateUtils.selectArrayShallow({ array: output }, commandNames.join('_'));
|
||||
|
@ -0,0 +1,32 @@
|
||||
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from 'lib/markdownUtils';
|
||||
|
||||
export default function commandsToMarkdownTable():string {
|
||||
const headers:MarkdownTableHeader[] = [
|
||||
{
|
||||
name: 'commandName',
|
||||
label: 'Name',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
},
|
||||
{
|
||||
name: 'props',
|
||||
label: 'Props',
|
||||
},
|
||||
];
|
||||
|
||||
const rows:MarkdownTableRow[] = [];
|
||||
|
||||
for (const commandName in this.commands_) {
|
||||
|
||||
const row:MarkdownTableRow = {
|
||||
commandName: commandName,
|
||||
description: this.label(commandName),
|
||||
};
|
||||
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
return markdownUtils.createMarkdownTable(headers, rows);
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
export default function propsHaveChanged(previous:any, next:any):boolean {
|
||||
if (!previous && next) return true;
|
||||
if (previous && !next) return true;
|
||||
if (!previous && !next) return false;
|
||||
|
||||
if (Object.keys(previous).length !== Object.keys(next).length) return true;
|
||||
|
||||
|
@ -0,0 +1,47 @@
|
||||
import { stateUtils } from 'lib/reducer';
|
||||
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const MarkupToHtml = require('lib/joplin-renderer/MarkupToHtml');
|
||||
|
||||
export default function stateToWhenClauseContext(state:any) {
|
||||
const noteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null;
|
||||
const note = noteId ? BaseModel.byId(state.notes, noteId) : null;
|
||||
|
||||
return {
|
||||
// UI elements
|
||||
markdownEditorVisible: !!state.settings['editor.codeView'],
|
||||
richTextEditorVisible: !state.settings['editor.codeView'],
|
||||
markdownEditorPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('editor'),
|
||||
markdownViewerPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('viewer'),
|
||||
modalDialogVisible: !!Object.keys(state.visibleDialogs).length,
|
||||
sideBarVisible: !!state.sidebarVisibility,
|
||||
noteListHasNotes: !!state.notes.length,
|
||||
|
||||
// Application state
|
||||
notesAreBeingSaved: stateUtils.hasNotesBeingSaved(state),
|
||||
syncStarted: state.syncStarted,
|
||||
|
||||
// Current location
|
||||
inConflictFolder: state.selectedFolderId === Folder.conflictFolderId(),
|
||||
|
||||
// Note selection
|
||||
oneNoteSelected: !!note,
|
||||
someNotesSelected: state.selectedNoteIds.length > 0,
|
||||
multipleNotesSelected: state.selectedNoteIds.length > 1,
|
||||
noNotesSelected: !state.selectedNoteIds.length,
|
||||
|
||||
// Note history
|
||||
historyhasBackwardNotes: state.backwardHistoryNotes.length > 0,
|
||||
historyhasForwardNotes: state.forwardHistoryNotes.length > 0,
|
||||
|
||||
// Folder selection
|
||||
oneFolderSelected: !!state.selectedFolderId,
|
||||
|
||||
// Current note properties
|
||||
noteIsTodo: note ? !!note.is_todo : false,
|
||||
noteTodoCompleted: note ? !!note.todo_completed : false,
|
||||
noteIsMarkdown: note ? note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN : false,
|
||||
noteIsHtml: note ? note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML : false,
|
||||
};
|
||||
}
|
@ -30,11 +30,11 @@ export default class JoplinCommands {
|
||||
*
|
||||
* // Create a new sub-notebook under the provided notebook
|
||||
* // Note: internally, notebooks are called "folders".
|
||||
* await joplin.commands.execute('newFolder', { parent_id: "SOME_FOLDER_ID" });
|
||||
* await joplin.commands.execute('newFolder', "SOME_FOLDER_ID");
|
||||
* ```
|
||||
*/
|
||||
async execute(commandName: string, props: any = null):Promise<any> {
|
||||
return CommandService.instance().execute(commandName, props);
|
||||
async execute(commandName: string, ...args:any[]):Promise<any> {
|
||||
return CommandService.instance().execute(commandName, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,8 +65,7 @@ export default class JoplinCommands {
|
||||
execute: command.execute,
|
||||
};
|
||||
|
||||
if ('isEnabled' in command) runtime.isEnabled = command.isEnabled;
|
||||
if ('mapStateToProps' in command) runtime.mapStateToProps = command.mapStateToProps;
|
||||
if ('enabledCondition' in command) runtime.enabledCondition = command.enabledCondition;
|
||||
|
||||
CommandService.instance().registerDeclaration(declaration);
|
||||
CommandService.instance().registerRuntime(declaration.name, runtime);
|
||||
|
@ -3,12 +3,47 @@
|
||||
// =================================================================
|
||||
|
||||
export interface Command {
|
||||
/**
|
||||
* Name of command - must be globally unique
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Label to be displayed on menu items or keyboard shortcut editor for example
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* Icon to be used on toolbar buttons for example
|
||||
*/
|
||||
iconName?: string,
|
||||
|
||||
/**
|
||||
* Code to be ran when the command is executed. It maybe return a result.
|
||||
*/
|
||||
execute(props:any):Promise<any>
|
||||
isEnabled?(props:any):boolean
|
||||
mapStateToProps?(state:any):any
|
||||
|
||||
/**
|
||||
* Defines whether the command should be enabled or disabled, which in turns affects
|
||||
* the enabled state of any associated button or menu item.
|
||||
*
|
||||
* The condition should be expressed as a "when-clause" (as in Visual Studio Code). It's a simple boolean expression that evaluates to
|
||||
* `true` or `false`. It supports the following operators:
|
||||
*
|
||||
* Operator | Symbol | Example
|
||||
* -- | -- | --
|
||||
* Equality | == | "editorType == markdown"
|
||||
* Inequality | != | "currentScreen != config"
|
||||
* Or | \|\| | "noteIsTodo \|\| noteTodoCompleted"
|
||||
* And | && | "oneNoteSelected && !inConflictFolder"
|
||||
*
|
||||
* Currently the supported context variables aren't documented, but you can find the list there:
|
||||
*
|
||||
* https://github.com/laurent22/joplin/blob/dev/ReactNativeClient/lib/services/commands/stateToWhenClauseContext.ts
|
||||
*
|
||||
* Note: Commands are enabled by default unless you use this property.
|
||||
*/
|
||||
enabledCondition?: string
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
Reference in New Issue
Block a user