diff --git a/src/public/app/components/app_context.ts b/src/public/app/components/app_context.ts index b7e5b6a45f..68f50218af 100644 --- a/src/public/app/components/app_context.ts +++ b/src/public/app/components/app_context.ts @@ -3,7 +3,7 @@ import bundleService from "../services/bundle.js"; import RootCommandExecutor from "./root_command_executor.js"; import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js"; import options from "../services/options.js"; -import utils from "../services/utils.js"; +import utils, { hasTouchBar } from "../services/utils.js"; import zoomComponent from "./zoom.js"; import TabManager from "./tab_manager.js"; import Component from "./component.js"; @@ -24,7 +24,8 @@ import type NoteTreeWidget from "../widgets/note_tree.js"; import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.js"; import type EditableTextTypeWidget from "../widgets/type_widgets/editable_text.js"; -import type FAttribute from "../entities/fattribute.js"; +import type { NativeImage, TouchBar } from "electron"; +import TouchBarComponent from "./touch_bar.js"; interface Layout { getRootWidget: (appContext: AppContext) => RootWidget; @@ -170,6 +171,8 @@ export type CommandMappings = { moveNoteDownInHierarchy: ContextMenuCommandData; selectAllNotesInParent: ContextMenuCommandData; + createNoteIntoInbox: CommandData; + addNoteLauncher: ContextMenuCommandData; addScriptLauncher: ContextMenuCommandData; addWidgetLauncher: ContextMenuCommandData; @@ -249,6 +252,7 @@ export type CommandMappings = { scrollToEnd: CommandData; closeThisNoteSplit: CommandData; moveThisNoteSplit: CommandData & { isMovingLeft: boolean }; + jumpToNote: CommandData; // Geomap deleteFromMap: { noteId: string }; @@ -263,6 +267,14 @@ export type CommandMappings = { refreshResults: {}; refreshSearchDefinition: {}; + + geoMapCreateChildNote: CommandData; + + buildTouchBar: CommandData & { + TouchBar: typeof TouchBar; + buildIcon(name: string): NativeImage; + }; + refreshTouchBar: CommandData; }; type EventMappings = { @@ -467,6 +479,10 @@ export class AppContext extends Component { if (utils.isElectron()) { this.child(zoomComponent); } + + if (hasTouchBar) { + this.child(new TouchBarComponent()); + } } renderWidgets() { diff --git a/src/public/app/components/touch_bar.ts b/src/public/app/components/touch_bar.ts new file mode 100644 index 0000000000..4afe651514 --- /dev/null +++ b/src/public/app/components/touch_bar.ts @@ -0,0 +1,135 @@ +import utils from "../services/utils.js"; +import Component from "./component.js"; +import appContext from "./app_context.js"; +import type { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl, TouchBarSpacer } from "@electron/remote"; + +export type TouchBarItem = (TouchBarButton | TouchBarSpacer | TouchBarGroup | TouchBarSegmentedControl); + +export function buildSelectedBackgroundColor(isSelected: boolean) { + return isSelected ? "#757575" : undefined; +} + +export default class TouchBarComponent extends Component { + + nativeImage: typeof import("electron").nativeImage; + remote: typeof import("@electron/remote"); + lastFocusedComponent?: Component; + private $activeModal?: JQuery; + + constructor() { + super(); + this.nativeImage = utils.dynamicRequire("electron").nativeImage; + this.remote = utils.dynamicRequire("@electron/remote") as typeof import("@electron/remote"); + this.$widget = $("
"); + + $(window).on("focusin", async (e) => { + const $target = $(e.target); + + this.$activeModal = $target.closest(".modal-dialog"); + const parentComponentEl = $target.closest(".component"); + this.lastFocusedComponent = appContext.getComponentByEl(parentComponentEl[0]); + this.#refreshTouchBar(); + }); + } + + buildIcon(name: string) { + const sourceImage = this.nativeImage.createFromNamedImage(name, [-1, 0, 1]); + const { width, height } = sourceImage.getSize(); + const newImage = this.nativeImage.createEmpty(); + newImage.addRepresentation({ + scaleFactor: 1, + width: width / 2, + height: height / 2, + buffer: sourceImage.resize({ height: height / 2 }).toBitmap() + }); + newImage.addRepresentation({ + scaleFactor: 2, + width: width, + height: height, + buffer: sourceImage.toBitmap() + }); + return newImage; + } + + #refreshTouchBar() { + const { TouchBar } = this.remote; + const parentComponent = this.lastFocusedComponent; + let touchBar = null; + + if (this.$activeModal?.length) { + touchBar = this.#buildModalTouchBar(); + } else if (parentComponent) { + const items = parentComponent.triggerCommand("buildTouchBar", { + TouchBar, + buildIcon: this.buildIcon.bind(this) + }) as unknown as TouchBarItem[]; + touchBar = this.#buildTouchBar(items); + } + + if (touchBar) { + this.remote.getCurrentWindow().setTouchBar(touchBar); + } + } + + #buildModalTouchBar() { + const { TouchBar } = this.remote; + const { TouchBarButton, TouchBarLabel, TouchBarSpacer } = this.remote.TouchBar; + const items: TouchBarItem[] = []; + + // Look for the modal title. + const $title = this.$activeModal?.find(".modal-title"); + if ($title?.length) { + items.push(new TouchBarLabel({ label: $title.text() })) + } + + items.push(new TouchBarSpacer({ size: "flexible" })); + + // Look for buttons in the modal. + const $buttons = this.$activeModal?.find(".modal-footer button"); + for (const button of $buttons ?? []) { + items.push(new TouchBarButton({ + label: button.innerText, + click: () => button.click(), + enabled: !button.hasAttribute("disabled") + })); + } + + items.push(new TouchBarSpacer({ size: "flexible" })); + return new TouchBar({ items }); + } + + #buildTouchBar(componentSpecificItems?: TouchBarItem[]) { + const { TouchBar } = this.remote; + const { TouchBarButton, TouchBarSpacer, TouchBarGroup, TouchBarSegmentedControl, TouchBarOtherItemsProxy } = this.remote.TouchBar; + + // Disregard recursive calls or empty results. + if (!componentSpecificItems || "then" in componentSpecificItems) { + componentSpecificItems = []; + } + + const items = [ + new TouchBarButton({ + icon: this.buildIcon("NSTouchBarComposeTemplate"), + click: () => this.triggerCommand("createNoteIntoInbox") + }), + new TouchBarSpacer({ size: "small" }), + ...componentSpecificItems, + new TouchBarSpacer({ size: "flexible" }), + new TouchBarOtherItemsProxy(), + new TouchBarButton({ + icon: this.buildIcon("NSTouchBarAddDetailTemplate"), + click: () => this.triggerCommand("jumpToNote") + }) + ].flat(); + + console.log("Update ", items); + return new TouchBar({ + items + }); + } + + refreshTouchBarEvent() { + this.#refreshTouchBar(); + } + +} diff --git a/src/public/app/layouts/desktop_layout.ts b/src/public/app/layouts/desktop_layout.ts index 707481188e..ee6faed631 100644 --- a/src/public/app/layouts/desktop_layout.ts +++ b/src/public/app/layouts/desktop_layout.ts @@ -83,7 +83,7 @@ import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_ref import ScrollPaddingWidget from "../widgets/scroll_padding.js"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import options from "../services/options.js"; -import utils from "../services/utils.js"; +import utils, { hasTouchBar } from "../services/utils.js"; import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js"; import ContextualHelpButton from "../widgets/floating_buttons/help_button.js"; import CloseZenButton from "../widgets/close_zen_button.js"; diff --git a/src/public/app/services/utils.ts b/src/public/app/services/utils.ts index efbb82cf8f..a52b7443a8 100644 --- a/src/public/app/services/utils.ts +++ b/src/public/app/services/utils.ts @@ -147,6 +147,8 @@ function isMac() { return navigator.platform.indexOf("Mac") > -1; } +export const hasTouchBar = (isMac() && isElectron()); + function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent) { return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey); } diff --git a/src/public/app/types.d.ts b/src/public/app/types.d.ts index 1ce3645270..504ae89077 100644 --- a/src/public/app/types.d.ts +++ b/src/public/app/types.d.ts @@ -339,6 +339,11 @@ declare global { mention: MentionConfig }); enableReadOnlyMode(reason: string); + commands: { + get(name: string): { + value: unknown; + }; + } model: { document: { on(event: string, cb: () => void); diff --git a/src/public/app/widgets/floating_buttons/edit_button.ts b/src/public/app/widgets/floating_buttons/edit_button.ts index 0b4d381fd6..2a11f0d011 100644 --- a/src/public/app/widgets/floating_buttons/edit_button.ts +++ b/src/public/app/widgets/floating_buttons/edit_button.ts @@ -23,7 +23,6 @@ export default class EditButton extends OnClickButtonWidget { this.noteContext.viewScope.readOnlyTemporarilyDisabled = true; appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext: this.noteContext }); } - this.refresh(); }); } @@ -68,6 +67,10 @@ export default class EditButton extends OnClickButtonWidget { } } + readOnlyTemporarilyDisabledEvent() { + this.refresh(); + } + async noteTypeMimeChangedEvent({ noteId }: { noteId: string }): Promise { if (this.isNote(noteId)) { await this.refresh(); diff --git a/src/public/app/widgets/note_list.ts b/src/public/app/widgets/note_list.ts index 9306b6ca96..73ebb358dc 100644 --- a/src/public/app/widgets/note_list.ts +++ b/src/public/app/widgets/note_list.ts @@ -1,7 +1,7 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js"; import NoteListRenderer from "../services/note_list_renderer.js"; import type FNote from "../entities/fnote.js"; -import type { EventData } from "../components/app_context.js"; +import type { CommandListener, CommandListenerData, EventData } from "../components/app_context.js"; import type ViewMode from "./view_widgets/view_mode.js"; const TPL = /*html*/` @@ -127,4 +127,11 @@ export default class NoteListWidget extends NoteContextAwareWidget { this.checkRenderStatus(); } } + + buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) { + if (this.viewMode && "buildTouchBarCommand" in this.viewMode) { + return (this.viewMode as CommandListener<"buildTouchBar">).buildTouchBarCommand(data); + } + } + } diff --git a/src/public/app/widgets/note_tree.ts b/src/public/app/widgets/note_tree.ts index b42b27c839..cbc3ec0555 100644 --- a/src/public/app/widgets/note_tree.ts +++ b/src/public/app/widgets/note_tree.ts @@ -25,6 +25,8 @@ import type FNote from "../entities/fnote.js"; import type { NoteType } from "../entities/fnote.js"; import type { AttributeRow, BranchRow } from "../services/load_results.js"; import type { SetNoteOpts } from "../components/note_context.js"; +import type { TouchBarItem } from "../components/touch_bar.js"; +import type { TreeCommandNames } from "../menus/tree_context_menu.js"; const TPL = /*html*/`
@@ -1763,4 +1765,38 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { appContext.tabManager.getActiveContext()?.setNote(resp.note.noteId); } + + buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) { + const triggerCommand = (command: TreeCommandNames) => { + const node = this.getActiveNode(); + const notePath = treeService.getNotePath(node); + + this.triggerCommand(command, { + node, + notePath, + noteId: node.data.noteId, + selectedOrActiveBranchIds: this.getSelectedOrActiveBranchIds(node), + selectedOrActiveNoteIds: this.getSelectedOrActiveNoteIds(node) + }); + } + + const items: TouchBarItem[] = [ + new TouchBar.TouchBarButton({ + icon: buildIcon("NSImageNameTouchBarAddTemplate"), + click: () => { + const node = this.getActiveNode(); + const notePath = treeService.getNotePath(node); + noteCreateService.createNote(notePath, { + isProtected: node.data.isProtected + }); + } + }), + new TouchBar.TouchBarButton({ + icon: buildIcon("NSImageNameTouchBarDeleteTemplate"), + click: () => triggerCommand("deleteNotes") + }) + ]; + + return items; + } } diff --git a/src/public/app/widgets/type_widgets/editable_code.ts b/src/public/app/widgets/type_widgets/editable_code.ts index e27bfa0795..fbdee82442 100644 --- a/src/public/app/widgets/type_widgets/editable_code.ts +++ b/src/public/app/widgets/type_widgets/editable_code.ts @@ -1,9 +1,12 @@ -import type { EventData } from "../../components/app_context.js"; +import type { CommandListenerData, EventData } from "../../components/app_context.js"; import type FNote from "../../entities/fnote.js"; import { t } from "../../services/i18n.js"; import keyboardActionService from "../../services/keyboard_actions.js"; import options from "../../services/options.js"; import AbstractCodeTypeWidget from "./abstract_code_type_widget.js"; +import appContext from "../../components/app_context.js"; +import type { TouchBarItem } from "../../components/touch_bar.js"; +import { hasTouchBar } from "../../services/utils.js"; const TPL = /*html*/`
@@ -61,6 +64,10 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget { }); this.show(); + + if (this.parent && hasTouchBar) { + this.triggerCommand("refreshTouchBar"); + } } getData() { @@ -78,4 +85,19 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget { resolve(this.codeEditor); } + + buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) { + const items: TouchBarItem[] = []; + const note = this.note; + + if (note?.mime.startsWith("application/javascript") || note?.mime === "text/x-sqlite;schema=trilium") { + items.push(new TouchBar.TouchBarButton({ + icon: buildIcon("NSImageNameTouchBarPlayTemplate"), + click: () => appContext.triggerCommand("runActiveNote") + })); + } + + return items; + } + } diff --git a/src/public/app/widgets/type_widgets/editable_text.ts b/src/public/app/widgets/type_widgets/editable_text.ts index b503dfbaf4..edefbcdcda 100644 --- a/src/public/app/widgets/type_widgets/editable_text.ts +++ b/src/public/app/widgets/type_widgets/editable_text.ts @@ -2,18 +2,19 @@ import { t } from "../../services/i18n.js"; import libraryLoader from "../../services/library_loader.js"; import noteAutocompleteService from "../../services/note_autocomplete.js"; import mimeTypesService from "../../services/mime_types.js"; -import utils from "../../services/utils.js"; +import utils, { hasTouchBar } from "../../services/utils.js"; import keyboardActionService from "../../services/keyboard_actions.js"; import froca from "../../services/froca.js"; import noteCreateService from "../../services/note_create.js"; import AbstractTextTypeWidget from "./abstract_text_type_widget.js"; import link from "../../services/link.js"; -import appContext, { type EventData } from "../../components/app_context.js"; +import appContext, { type CommandListenerData, type EventData } from "../../components/app_context.js"; import dialogService from "../../services/dialog.js"; import { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js"; import options from "../../services/options.js"; import toast from "../../services/toast.js"; import { normalizeMimeTypeForCKEditor } from "../../services/mime_type_definitions.js"; +import { buildSelectedBackgroundColor } from "../../components/touch_bar.js"; import { buildConfig, buildToolbarConfig } from "./ckeditor/config.js"; import type FNote from "../../entities/fnote.js"; import { getMermaidConfig } from "../../services/mermaid.js"; @@ -280,6 +281,13 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { CKEditorInspector.attach(editor); } + // Touch bar integration + if (hasTouchBar) { + for (const event of [ "bold", "italic", "underline", "paragraph", "heading" ]) { + editor.commands.get(event).on("change", () => this.triggerCommand("refreshTouchBar")); + } + } + return editor; }); @@ -544,4 +552,60 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { await this.reinitialize(data); } + buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) { + const { TouchBar, buildIcon } = data; + const { TouchBarSegmentedControl, TouchBarGroup, TouchBarButton } = TouchBar; + const { editor } = this.watchdog; + + const commandButton = (icon: string, command: string) => new TouchBarButton({ + icon: buildIcon(icon), + click: () => editor.execute(command), + backgroundColor: buildSelectedBackgroundColor(editor.commands.get(command).value as boolean) + }); + + let headingSelectedIndex = undefined; + const headingCommand = editor.commands.get("heading"); + const paragraphCommand = editor.commands.get("paragraph"); + if (paragraphCommand.value) { + headingSelectedIndex = 0; + } else if (headingCommand.value === "heading2") { + headingSelectedIndex = 1; + } else if (headingCommand.value === "heading3") { + headingSelectedIndex = 2; + } + + return [ + new TouchBarSegmentedControl({ + segments: [ + { label: "P" }, + { label: "H2" }, + { label: "H3" } + ], + change(selectedIndex, isSelected) { + switch (selectedIndex) { + case 0: + editor.execute("paragraph") + break; + case 1: + editor.execute("heading", { value: "heading2" }); + break; + case 2: + editor.execute("heading", { value: "heading3" }); + break; + } + }, + selectedIndex: headingSelectedIndex + }), + new TouchBarGroup({ + items: new TouchBar({ + items: [ + commandButton("NSTouchBarTextBoldTemplate", "bold"), + commandButton("NSTouchBarTextItalicTemplate", "italic"), + commandButton("NSTouchBarTextUnderlineTemplate", "underline") + ] + }) + }) + ]; + } + } diff --git a/src/public/app/widgets/type_widgets/geo_map.ts b/src/public/app/widgets/type_widgets/geo_map.ts index e858694dcc..320fb7c9a7 100644 --- a/src/public/app/widgets/type_widgets/geo_map.ts +++ b/src/public/app/widgets/type_widgets/geo_map.ts @@ -5,7 +5,7 @@ import TypeWidget from "./type_widget.js"; import server from "../../services/server.js"; import toastService from "../../services/toast.js"; import dialogService from "../../services/dialog.js"; -import type { EventData } from "../../components/app_context.js"; +import type { CommandListenerData, EventData } from "../../components/app_context.js"; import { t } from "../../services/i18n.js"; import attributes from "../../services/attributes.js"; import openContextMenu from "./geo_map_context_menu.js"; @@ -15,6 +15,7 @@ import appContext from "../../components/app_context.js"; import markerIcon from "leaflet/dist/images/marker-icon.png"; import markerIconShadow from "leaflet/dist/images/marker-shadow.png"; +import { hasTouchBar } from "../../services/utils.js"; const TPL = /*html*/`\
@@ -107,6 +108,7 @@ export default class GeoMapTypeWidget extends TypeWidget { private currentMarkerData: Record; private currentTrackData: Record; private gpxLoaded?: boolean; + private ignoreNextZoomEvent?: boolean; static getType() { return "geoMap"; @@ -151,6 +153,16 @@ export default class GeoMapTypeWidget extends TypeWidget { map.on("moveend", updateFn); map.on("zoomend", updateFn); map.on("click", (e) => this.#onMapClicked(e)); + + if (hasTouchBar) { + map.on("zoom", () => { + if (!this.ignoreNextZoomEvent) { + this.triggerCommand("refreshTouchBar"); + } + + this.ignoreNextZoomEvent = false; + }); + } } async #restoreViewportAndZoom() { @@ -279,6 +291,9 @@ export default class GeoMapTypeWidget extends TypeWidget { #changeState(newState: State) { this._state = newState; this.geoMapWidget.$container.toggleClass("placing-note", newState === State.NewNote); + if (hasTouchBar) { + this.triggerCommand("refreshTouchBar"); + } } async #onMapClicked(e: LeafletMouseEvent) { @@ -388,4 +403,30 @@ export default class GeoMapTypeWidget extends TypeWidget { this.moveMarker(noteId, null); } + buildTouchBarCommand({ TouchBar }: CommandListenerData<"buildTouchBar">) { + const map = this.geoMapWidget.map; + const that = this; + if (!map) { + return; + } + + return [ + new TouchBar.TouchBarSlider({ + label: "Zoom", + value: map.getZoom(), + minValue: map.getMinZoom(), + maxValue: map.getMaxZoom(), + change(newValue) { + that.ignoreNextZoomEvent = true; + map.setZoom(newValue); + }, + }), + new TouchBar.TouchBarButton({ + label: "New geo note", + click: () => this.triggerCommand("geoMapCreateChildNote", { ntxId: this.ntxId }), + enabled: (this._state === State.Normal) + }) + ]; + } + } diff --git a/src/public/app/widgets/type_widgets/read_only_text.ts b/src/public/app/widgets/type_widgets/read_only_text.ts index 1524255f11..25c20718fa 100644 --- a/src/public/app/widgets/type_widgets/read_only_text.ts +++ b/src/public/app/widgets/type_widgets/read_only_text.ts @@ -2,12 +2,13 @@ import AbstractTextTypeWidget from "./abstract_text_type_widget.js"; import libraryLoader from "../../services/library_loader.js"; import { applySyntaxHighlight } from "../../services/syntax_highlight.js"; import type FNote from "../../entities/fnote.js"; -import type { EventData } from "../../components/app_context.js"; +import type { CommandListenerData, EventData } from "../../components/app_context.js"; import { getLocaleById } from "../../services/i18n.js"; +import appContext from "../../components/app_context.js"; import { getMermaidConfig } from "../../services/mermaid.js"; const TPL = /*html*/` -
+
-
+
`; @@ -266,6 +268,10 @@ export default class CalendarView extends ViewMode { this.debouncedSaveView(); this.lastView = currentView; + + if (hasTouchBar) { + appContext.triggerCommand("refreshTouchBar"); + } } async #onCalendarSelection(e: DateSelectArg) { @@ -595,4 +601,71 @@ export default class CalendarView extends ViewMode { return newDate; } + buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) { + if (!this.calendar) { + return; + } + + const items: TouchBarItem[] = []; + const $toolbarItems = this.$calendarContainer.find(".fc-toolbar-chunk .fc-button-group, .fc-toolbar-chunk > button"); + + for (const item of $toolbarItems) { + // Button groups. + if (item.classList.contains("fc-button-group")) { + let mode: "single" | "buttons" = "single"; + let selectedIndex = 0; + const segments: SegmentedControlSegment[] = []; + const subItems = item.childNodes as NodeListOf; + let index = 0; + for (const subItem of subItems) { + if (subItem.ariaPressed === "true") { + selectedIndex = index; + } + index++; + + // Text button. + if (subItem.innerText) { + segments.push({ label: subItem.innerText }); + continue; + } + + // Icon button. + const iconEl = subItem.querySelector("span.fc-icon"); + let icon = null; + if (iconEl?.classList.contains("fc-icon-chevron-left")) { + icon = "NSImageNameTouchBarGoBackTemplate"; + mode = "buttons"; + } else if (iconEl?.classList.contains("fc-icon-chevron-right")) { + icon = "NSImageNameTouchBarGoForwardTemplate"; + mode = "buttons"; + } + + if (icon) { + segments.push({ + icon: buildIcon(icon) + }); + } + } + + items.push(new TouchBar.TouchBarSegmentedControl({ + mode, + segments, + selectedIndex, + change: (selectedIndex, isSelected) => subItems[selectedIndex].click() + })); + continue; + } + + // Standalone item. + if (item.innerText) { + items.push(new TouchBar.TouchBarButton({ + label: item.innerText, + click: () => item.click() + })); + } + } + + return items; + } + }