launcher improvements

This commit is contained in:
zadam 2022-12-02 16:46:14 +01:00
parent 7b36709e18
commit b85f335561
17 changed files with 283 additions and 196 deletions

View File

@ -1,15 +1,10 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import keyboardActionsService from "../../services/keyboard_actions.js";
const TPL = `<span class="button-widget icon-action bx"
data-toggle="tooltip"
title=""></span>`;
let actions;
keyboardActionsService.getActions().then(as => actions = as);
export default class ButtonWidget extends NoteContextAwareWidget {
export default class AbstractButtonWidget extends NoteContextAwareWidget {
isEnabled() {
return true;
}
@ -21,8 +16,6 @@ export default class ButtonWidget extends NoteContextAwareWidget {
titlePlacement: 'right',
title: null,
icon: null,
command: null,
onClick: null,
onContextMenu: null
};
}
@ -30,20 +23,6 @@ export default class ButtonWidget extends NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
if (this.settings.onClick) {
this.$widget.on("click", e => {
this.$widget.tooltip("hide");
this.settings.onClick(this, e);
});
} else {
this.$widget.on("click", () => {
this.$widget.tooltip("hide");
this.triggerCommand(this.settings.command);
});
}
if (this.settings.onContextMenu) {
this.$widget.on("contextmenu", e => {
this.$widget.tooltip("hide");
@ -56,26 +35,19 @@ export default class ButtonWidget extends NoteContextAwareWidget {
this.$widget.tooltip({
html: true,
title: () => {
const title = typeof this.settings.title === "function"
? this.settings.title()
: this.settings.title;
const action = actions.find(act => act.actionName === this.settings.command);
if (action && action.effectiveShortcuts.length > 0) {
return `${title} (${action.effectiveShortcuts.join(", ")})`;
}
else {
return title;
}
},
title: () => this.getTitle(),
trigger: "hover"
});
super.doRender();
}
getTitle() {
return typeof this.settings.title === "function"
? this.settings.title()
: this.settings.title;
}
refreshIcon() {
for (const className of this.$widget[0].classList) {
if (className.startsWith("bx-")) {
@ -83,39 +55,36 @@ export default class ButtonWidget extends NoteContextAwareWidget {
}
}
this.$widget.addClass(this.settings.icon);
const icon = typeof this.settings.icon === "function"
? this.settings.icon()
: this.settings.icon;
this.$widget.addClass(icon);
}
initialRenderCompleteEvent() {
this.refreshIcon();
}
/** @param {string|function} icon */
icon(icon) {
this.settings.icon = icon;
return this;
}
/** @param {string|function} title */
title(title) {
this.settings.title = title;
return this;
}
/** @param {string} placement - "top", "bottom", "left", "right" */
titlePlacement(placement) {
this.settings.titlePlacement = placement;
return this;
}
command(command) {
this.settings.command = command;
return this;
}
onClick(handler) {
this.settings.onClick = handler;
return this;
}
onContextMenu(handler) {
this.settings.onContextMenu = handler;
}
}
}

View File

@ -1,8 +1,8 @@
import ButtonWidget from "./button_widget.js";
import froca from "../../services/froca.js";
import attributeService from "../../services/attributes.js";
import CommandButtonWidget from "./command_button.js";
export default class ButtonFromNoteWidget extends ButtonWidget {
export default class ButtonFromNoteWidget extends CommandButtonWidget {
constructor() {
super();

View File

@ -1,6 +1,6 @@
import ButtonWidget from "./button_widget.js";
import OnClickButtonWidget from "./onclick_button.js";
export default class ClosePaneButton extends ButtonWidget {
export default class ClosePaneButton extends OnClickButtonWidget {
isEnabled() {
return super.isEnabled()
// main note context should not be closeable

View File

@ -0,0 +1,54 @@
import keyboardActionsService from "../../services/keyboard_actions.js";
import AbstractButtonWidget from "./abstract_button.js";
let actions;
keyboardActionsService.getActions().then(as => actions = as);
export default class CommandButtonWidget extends AbstractButtonWidget {
doRender() {
super.doRender();
if (this.settings.command) {
this.$widget.on("click", () => {
this.$widget.tooltip("hide");
this.triggerCommand(this._command);
});
} else {
console.warn(`Button widget '${this.componentId}' has no defined command`, this.settings);
}
}
getTitle() {
const title = super.getTitle();
const action = actions.find(act => act.actionName === this._command);
if (action && action.effectiveShortcuts.length > 0) {
return `${title} (${action.effectiveShortcuts.join(", ")})`;
} else {
return title;
}
}
onClick(handler) {
this.settings.onClick = handler;
return this;
}
/**
* @param {function|string} command
* @returns {CommandButtonWidget}
*/
command(command) {
this.settings.command = command;
return this;
}
get _command() {
return typeof this.settings.command === "function"
? this.settings.command()
: this.settings.command;
}
}

View File

@ -1,6 +1,6 @@
import ButtonWidget from "./button_widget.js";
import OnClickButtonWidget from "./onclick_button.js";
export default class CreatePaneButton extends ButtonWidget {
export default class CreatePaneButton extends OnClickButtonWidget {
constructor() {
super();

View File

@ -1,9 +1,9 @@
import ButtonWidget from "./button_widget.js";
import OnClickButtonWidget from "./onclick_button.js";
import appContext from "../../components/app_context.js";
import attributeService from "../../services/attributes.js";
import protectedSessionHolder from "../../services/protected_session_holder.js";
export default class EditButton extends ButtonWidget {
export default class EditButton extends OnClickButtonWidget {
isEnabled() {
return super.isEnabled() && this.noteContext;
}

View File

@ -0,0 +1,33 @@
import shortcutService from "../../../services/shortcuts.js";
import OnClickButtonWidget from "../onclick_button.js";
export default class AbstractLauncher extends OnClickButtonWidget {
constructor(launcherNote) {
super();
/** @type {NoteShort} */
this.launcherNote = launcherNote;
}
launch() {
throw new Error("Abstract implementation");
}
bindNoteShortcutHandler(label) {
const namespace = label.attributeId;
if (label.isDeleted) {
shortcutService.removeGlobalShortcut(namespace);
} else {
shortcutService.bindGlobalShortcut(label.value, () => this.launch(), namespace);
}
}
entitiesReloadedEvent({loadResults}) {
for (const attr of loadResults.getAttributes()) {
if (attr.noteId === this.launcherNote.noteId && attr.type === 'label' && attr.name === 'keyboardShortcut') {
this.bindNoteShortcutHandler(attr);
}
}
}
}

View File

@ -0,0 +1,44 @@
import AbstractLauncher from "./abstract_launcher.js";
import dialogService from "../../../services/dialog.js";
import appContext from "../../../components/app_context.js";
export default class NoteLauncher extends AbstractLauncher {
constructor(launcherNote) {
super(launcherNote);
this.title(this.launcherNote.title)
.icon(this.launcherNote.getIcon())
.onClick(() => this.launch());
}
launch() {
// we're intentionally displaying the launcher title and icon instead of the target
// e.g. you want to make launchers to 2 mermaid diagrams which both have mermaid icon (ok),
// but on the launchpad you want them distinguishable.
// for titles, the note titles may follow a different scheme than maybe desirable on the launchpad
// another reason is the discrepancy between what user sees on the launchpad and in the config (esp. icons).
// The only (but major) downside is more work in setting up the typical case where you actually want to have both title and icon in sync.
const targetNoteId = this.launcherNote.getRelationValue('targetNote');
if (!targetNoteId) {
dialogService.info("This launcher doesn't define target note.");
return;
}
appContext.tabManager.openTabWithNoteWithHoisting(targetNoteId, true);
}
getTitle() {
const shortcuts = this.launcherNote.getLabels("keyboardShortcut")
.map(l => l.value)
.filter(v => !!v)
.join(", ");
let title = super.getTitle();
if (shortcuts) {
title += ` (${shortcuts})`;
}
return title;
}
}

View File

@ -0,0 +1,17 @@
import AbstractLauncher from "./abstract_launcher.js";
export default class ScriptLauncher extends AbstractLauncher {
constructor(launcherNote) {
super(launcherNote);
this.title(this.launcherNote.title)
.icon(this.launcherNote.getIcon())
.onClick(this.handler);
}
async launch() {
const script = await this.launcherNote.getRelationTarget('script');
await script.executeScript();
}
}

View File

@ -1,26 +1,28 @@
import ButtonWidget from "./button_widget.js";
import options from "../../services/options.js";
import splitService from "../../services/resizer.js";
import CommandButtonWidget from "./command_button.js";
export default class LeftPaneToggleWidget extends ButtonWidget {
refreshIcon() {
const isLeftPaneVisible = options.is('leftPaneVisible');
export default class LeftPaneToggleWidget extends CommandButtonWidget {
constructor() {
super();
this.settings.icon = isLeftPaneVisible
this.settings.icon = () => options.is('leftPaneVisible')
? "bx-chevrons-left"
: "bx-chevrons-right";
this.settings.title = isLeftPaneVisible
this.settings.title = () => options.is('leftPaneVisible')
? "Hide panel"
: "Open panel";
this.settings.command = isLeftPaneVisible
this.settings.command = () => options.is('leftPaneVisible')
? "hideLeftPane"
: "showLeftPane";
}
refreshIcon() {
super.refreshIcon();
splitService.setupLeftPaneResizer(isLeftPaneVisible);
splitService.setupLeftPaneResizer(options.is('leftPaneVisible'));
}
entitiesReloadedEvent({loadResults}) {

View File

@ -1,6 +1,6 @@
import ButtonWidget from "./button_widget.js";
import CommandButtonWidget from "./command_button.js";
export default class NoteRevisionsButton extends ButtonWidget {
export default class NoteRevisionsButton extends CommandButtonWidget {
constructor() {
super();

View File

@ -0,0 +1,22 @@
import AbstractButtonWidget from "./abstract_button.js";
export default class OnClickButtonWidget extends AbstractButtonWidget {
doRender() {
super.doRender();
if (this.settings.onClick) {
this.$widget.on("click", e => {
this.$widget.tooltip("hide");
this.settings.onClick(this, e);
});
} else {
console.warn(`Button widget '${this.componentId}' has no defined click handler`, this.settings);
}
}
onClick(handler) {
this.settings.onClick = handler;
return this;
}
}

View File

@ -1,10 +1,10 @@
import ButtonWidget from "./button_widget.js";
import OnClickButtonWidget from "./onclick_button.js";
import appContext from "../../components/app_context.js";
import froca from "../../services/froca.js";
// FIXME: this widget might not be useful anymore
export default class OpenNoteButtonWidget extends ButtonWidget {
export default class OpenNoteButtonWidget extends OnClickButtonWidget {
targetNote(noteId) {
froca.getNote(noteId).then(note => {
if (!note) {

View File

@ -1,29 +1,24 @@
import ButtonWidget from "./button_widget.js";
import protectedSessionHolder from "../../services/protected_session_holder.js";
import CommandButtonWidget from "./command_button.js";
export default class ProtectedSessionStatusWidget extends ButtonWidget {
doRender() {
this.updateSettings();
export default class ProtectedSessionStatusWidget extends CommandButtonWidget {
constructor() {
super();
super.doRender();
}
updateSettings() {
this.settings.icon = protectedSessionHolder.isProtectedSessionAvailable()
this.settings.icon = () => protectedSessionHolder.isProtectedSessionAvailable()
? "bx-check-shield"
: "bx-shield-quarter";
this.settings.title = protectedSessionHolder.isProtectedSessionAvailable()
this.settings.title = () => protectedSessionHolder.isProtectedSessionAvailable()
? "Protected session is active. Click to leave protected session."
: "Click to enter protected session";
this.settings.command = protectedSessionHolder.isProtectedSessionAvailable()
this.settings.command = () => protectedSessionHolder.isProtectedSessionAvailable()
? "leaveProtectedSession"
: "enterProtectedSession";
}
protectedSessionStartedEvent() {
this.updateSettings();
this.refreshIcon();
}
}

View File

@ -1,6 +1,4 @@
import ButtonWidget from "../buttons/button_widget.js";
import dialogService from "../../services/dialog.js";
import appContext from "../../components/app_context.js";
import OnClickButtonWidget from "../buttons/onclick_button.js";
import CalendarWidget from "../buttons/calendar.js";
import SpacerWidget from "../spacer.js";
import BookmarkButtons from "../bookmark_buttons.js";
@ -9,19 +7,15 @@ import SyncStatusWidget from "../sync_status.js";
import BackInHistoryButtonWidget from "../buttons/history/history_back.js";
import ForwardInHistoryButtonWidget from "../buttons/history/history_forward.js";
import BasicWidget from "../basic_widget.js";
import shortcutService from "../../services/shortcuts.js";
import NoteLauncher from "../buttons/launcher/note_launcher.js";
import ScriptLauncher from "../buttons/launcher/script_launcher.js";
import CommandButtonWidget from "../buttons/command_button.js";
export default class LauncherWidget extends BasicWidget {
constructor(launcherNote) {
constructor() {
super();
if (launcherNote.type !== 'launcher') {
throw new Error(`Note '${this.note.noteId}' '${this.note.title}' is not a launcher even though it's in the launcher subtree`);
}
this.note = launcherNote;
this.innerWidget = null;
this.handler = null;
}
isEnabled() {
@ -32,118 +26,74 @@ export default class LauncherWidget extends BasicWidget {
this.$widget = this.innerWidget.render();
}
async initLauncher() {
const launcherType = this.note.getLabelValue("launcherType");
async initLauncher(note) {
if (note.type !== 'launcher') {
throw new Error(`Note '${note.noteId}' '${note.title}' is not a launcher even though it's in the launcher subtree`);
}
const launcherType = note.getLabelValue("launcherType");
if (launcherType === 'command') {
this.handler = () => this.triggerCommand(this.note.getLabelValue("command"));
this.innerWidget = new ButtonWidget()
.title(this.note.title)
.icon(this.note.getIcon())
.onClick(this.handler);
this.innerWidget = this.initCommandWidget(note);
} else if (launcherType === 'note') {
// we're intentionally displaying the launcher title and icon instead of the target
// e.g. you want to make launchers to 2 mermaid diagrams which both have mermaid icon (ok),
// but on the launchpad you want them distinguishable.
// for titles, the note titles may follow a different scheme than maybe desirable on the launchpad
// another reason is the discrepancy between what user sees on the launchpad and in the config (esp. icons).
// The only (but major) downside is more work in setting up the typical case where you actually want to have both title and icon in sync.
this.handler = () => {
const targetNoteId = this.note.getRelationValue('targetNote');
if (!targetNoteId) {
dialogService.info("This launcher doesn't define target note.");
return;
}
appContext.tabManager.openTabWithNoteWithHoisting(targetNoteId, true)
};
this.innerWidget = new ButtonWidget()
.title(this.note.title)
.icon(this.note.getIcon())
.onClick(this.handler);
this.innerWidget = new NoteLauncher(note);
} else if (launcherType === 'script') {
this.handler = async () => {
const script = await this.note.getRelationTarget('script');
await script.executeScript();
};
this.innerWidget = new ButtonWidget()
.title(this.note.title)
.icon(this.note.getIcon())
.onClick(this.handler);
this.innerWidget = new ScriptLauncher(note);
} else if (launcherType === 'customWidget') {
const widget = await this.note.getRelationTarget('widget');
if (widget) {
this.innerWidget = await widget.executeScript();
} else {
throw new Error(`Could not initiate custom widget of launcher '${this.note.noteId}' '${this.note.title}`);
}
this.innerWidget = await this.initCustomWidget(note);
} else if (launcherType === 'builtinWidget') {
const builtinWidget = this.note.getLabelValue("builtinWidget");
if (builtinWidget) {
if (builtinWidget === 'calendar') {
this.innerWidget = new CalendarWidget(this.note.title, this.note.getIcon());
} else if (builtinWidget === 'spacer') {
// || has to be inside since 0 is a valid value
const baseSize = parseInt(this.note.getLabelValue("baseSize") || "40");
const growthFactor = parseInt(this.note.getLabelValue("growthFactor") || "100");
this.innerWidget = new SpacerWidget(baseSize, growthFactor);
} else if (builtinWidget === 'bookmarks') {
this.innerWidget = new BookmarkButtons();
} else if (builtinWidget === 'protectedSession') {
this.innerWidget = new ProtectedSessionStatusWidget();
} else if (builtinWidget === 'syncStatus') {
this.innerWidget = new SyncStatusWidget();
} else if (builtinWidget === 'backInHistoryButton') {
this.innerWidget = new BackInHistoryButtonWidget();
} else if (builtinWidget === 'forwardInHistoryButton') {
this.innerWidget = new ForwardInHistoryButtonWidget();
} else {
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${this.note.noteId} "${this.note.title}"`);
}
}
this.innerWidget = this.initBuiltinWidget(note);
} else {
throw new Error(`Unrecognized launcher type '${launcherType}' for launcher '${this.note.noteId}' title ${this.note.title}`);
throw new Error(`Unrecognized launcher type '${launcherType}' for launcher '${note.noteId}' title ${note.title}`);
}
if (!this.innerWidget) {
throw new Error(`Unknown initialization error for note '${this.note.noteId}', title '${this.note.title}'`);
}
for (const label of this.note.getLabels('keyboardShortcut')) {
this.bindNoteShortcutHandler(label);
throw new Error(`Unknown initialization error for note '${note.noteId}', title '${note.title}'`);
}
this.child(this.innerWidget);
}
bindNoteShortcutHandler(label) {
if (!this.handler) {
return;
}
initCommandWidget(note) {
return new CommandButtonWidget()
.title(note.title)
.icon(note.getIcon())
.command(() => note.getLabelValue("command"));
}
const namespace = label.attributeId;
async initCustomWidget(note) {
const widget = await note.getRelationTarget('widget');
if (label.isDeleted) {
shortcutService.removeGlobalShortcut(namespace);
if (widget) {
return await widget.executeScript();
} else {
shortcutService.bindGlobalShortcut(label.value, this.handler, namespace);
throw new Error(`Custom widget of launcher '${note.noteId}' '${note.title}' is not defined.`);
}
}
entitiesReloadedEvent({loadResults}) {
for (const attr of loadResults.getAttributes()) {
if (attr.noteId === this.note.noteId && attr.type === 'label' && attr.name === 'keyboardShortcut') {
this.bindNoteShortcutHandler(attr);
}
initBuiltinWidget(note) {
const builtinWidget = note.getLabelValue("builtinWidget");
if (builtinWidget === 'calendar') {
return new CalendarWidget(note.title, note.getIcon());
} else if (builtinWidget === 'spacer') {
// || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
return new SpacerWidget(baseSize, growthFactor);
} else if (builtinWidget === 'bookmarks') {
return new BookmarkButtons();
} else if (builtinWidget === 'protectedSession') {
return new ProtectedSessionStatusWidget();
} else if (builtinWidget === 'syncStatus') {
return new SyncStatusWidget();
} else if (builtinWidget === 'backInHistoryButton') {
return new BackInHistoryButtonWidget();
} else if (builtinWidget === 'forwardInHistoryButton') {
return new ForwardInHistoryButtonWidget();
} else {
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
}
}
}

View File

@ -29,12 +29,12 @@ export default class LauncherContainer extends FlexContainer {
(await visibleLaunchersRoot.getChildNotes())
.map(async launcherNote => {
try {
const launcherWidget = new LauncherWidget(launcherNote);
await launcherWidget.initLauncher();
const launcherWidget = new LauncherWidget();
await launcherWidget.initLauncher(launcherNote);
this.child(launcherWidget);
}
catch (e) {
console.error(e.message);
console.error(e);
}
})
);

View File

@ -19,6 +19,7 @@ import options from "../services/options.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import dialogService from "../services/dialog.js";
import shortcutService from "../services/shortcuts.js";
import LauncherContextMenu from "../menus/launcher_context_menu.js";
const TPL = `
<div class="tree-wrapper">
@ -1558,24 +1559,24 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
branchService.moveToParentNote(selectedOrActiveBranchIds, 'lb_availablelaunchers');
}
addNoteShortcutCommand({node}) {
this.createShortcutNote(node, 'note');
addNoteLauncherCommand({node}) {
this.createLauncherNote(node, 'note');
}
addScriptShortcutCommand({node}) {
this.createShortcutNote(node, 'script');
addScriptLauncherCommand({node}) {
this.createLauncherNote(node, 'script');
}
addWidgetShortcutCommand({node}) {
this.createShortcutNote(node, 'customWidget');
addWidgetLauncherCommand({node}) {
this.createLauncherNote(node, 'customWidget');
}
addSpacerShortcutCommand({node}) {
this.createShortcutNote(node, 'spacer');
addSpacerLauncherCommand({node}) {
this.createLauncherNote(node, 'spacer');
}
async createShortcutNote(node, launcherType) {
const resp = await server.post(`special-notes/shortcuts/${node.data.noteId}/${launcherType}`);
async createLauncherNote(node, launcherType) {
const resp = await server.post(`special-notes/launchers/${node.data.noteId}/${launcherType}`);
if (!resp.success) {
alert(resp.message);