Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.

Basic touchbar integration #1549

Merged
merged 45 commits into from
Apr 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
a170bec
feat(touch_bar): basic integration
eliandoran Mar 8, 2025
3358b40
feat(touch_bar): use icon for new note
eliandoran Mar 8, 2025
0430a9c
feat(touch_bar): resize icon even if blurry
eliandoran Mar 8, 2025
f2f0f61
fix(touch_bar): blurry native images
eliandoran Mar 8, 2025
dd57578
feat(touch_bar): functional new note button
eliandoran Mar 8, 2025
d9a689b
feat(touch_bar): functional bold, italic, underline
eliandoran Mar 8, 2025
9c24b89
feat(touch_bar): jump to note
eliandoran Mar 8, 2025
2676596
feat(touch_bar): reduce items moving around
eliandoran Mar 8, 2025
6085995
feat(touch_bar): paragraph and heading buttons
eliandoran Mar 8, 2025
214674c
feat(touch_bar): use segmented control for heading
eliandoran Mar 8, 2025
db8d471
refactor(touch_bar): command-driven approach
eliandoran Mar 8, 2025
c2e4af1
chore(touch_bar): bring back local config
eliandoran Mar 8, 2025
0fe5f79
fix(touch_bar): fix text editor commands
eliandoran Mar 8, 2025
36eac98
feat(touch_bar): zoom slider
eliandoran Mar 8, 2025
ece2696
feat(touch_bar): update zoom slider value
eliandoran Mar 8, 2025
5961e98
feat(touch_bar): new geonote button
eliandoran Mar 8, 2025
323f428
refactor(touch_bar): move geomap to parent typewidget
eliandoran Mar 8, 2025
cbbe10b
fix(touch_bar): jerkiness when zooming
eliandoran Mar 8, 2025
a3c5883
feat(touch_bar): reflect new note state
eliandoran Mar 8, 2025
ff78ab6
feat(touch_bar): use disabled button for geomap
eliandoran Mar 8, 2025
fff140d
feat(touch_bar): reflect state for bold
eliandoran Mar 9, 2025
e71a18f
feat(touch_bar): reflect state for underline, italic
eliandoran Mar 9, 2025
07c9565
feat(touch_bar): reflect state for paragraph and headings
eliandoran Mar 9, 2025
615a5f7
feat(touch_bar): change selected color
eliandoran Mar 9, 2025
975e641
feat(touch_bar): run button for scripts
eliandoran Mar 9, 2025
8a1b565
feat(touch_bar): add unlock button for read-only text
eliandoran Mar 9, 2025
4161bc1
Merge remote-tracking branch 'origin/develop' into feature/touchbar
eliandoran Apr 11, 2025
904e8f7
refactor(touchbar): unnecessary typecast
eliandoran Apr 13, 2025
ce86a2b
feat(touchbar): add spacer
eliandoran Apr 13, 2025
e6e2bde
feat(touchbar): basic implementation for modal buttons
eliandoran Apr 13, 2025
a0447c4
feat(touchbar): display modal title
eliandoran Apr 13, 2025
d1df365
feat(touchbar): calendar view
eliandoran Apr 13, 2025
9d9ed2e
feat(touchbar): refresh properly for calendar view
eliandoran Apr 13, 2025
f98ac84
feat(touchbar): delete note in note tree
eliandoran Apr 13, 2025
cbc6e74
feat(touchbar): create child note in note tree
eliandoran Apr 13, 2025
d734ac9
fix(touchbar): hide read-only button after editing
eliandoran Apr 13, 2025
d6478c2
fix(touchbar): errors refreshing touchbar if parent is missing
eliandoran Apr 13, 2025
3fb2378
fix(touchbar): errors if there is no modal
eliandoran Apr 13, 2025
ef423f1
chore(touchbar): reduce spacer width
eliandoran Apr 13, 2025
342aff8
chore(touchbar): reduce centering
eliandoran Apr 13, 2025
de99759
Merge remote-tracking branch 'origin/develop' into feature/touchbar
eliandoran Apr 13, 2025
83e7e82
chore(touchbar): address self-review
eliandoran Apr 13, 2025
14516d5
chore(touchbar): disable widget on non-mac
eliandoran Apr 13, 2025
c5ca3de
refactor(touchbar): turn into a component
eliandoran Apr 13, 2025
58a33ef
fix(touchbar): crashing on server
eliandoran Apr 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/public/app/components/app_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -170,6 +171,8 @@ export type CommandMappings = {
moveNoteDownInHierarchy: ContextMenuCommandData;
selectAllNotesInParent: ContextMenuCommandData;

createNoteIntoInbox: CommandData;

addNoteLauncher: ContextMenuCommandData;
addScriptLauncher: ContextMenuCommandData;
addWidgetLauncher: ContextMenuCommandData;
Expand Down Expand Up @@ -249,6 +252,7 @@ export type CommandMappings = {
scrollToEnd: CommandData;
closeThisNoteSplit: CommandData;
moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
jumpToNote: CommandData;

// Geomap
deleteFromMap: { noteId: string };
Expand All @@ -263,6 +267,14 @@ export type CommandMappings = {

refreshResults: {};
refreshSearchDefinition: {};

geoMapCreateChildNote: CommandData;

buildTouchBar: CommandData & {
TouchBar: typeof TouchBar;
buildIcon(name: string): NativeImage;
};
refreshTouchBar: CommandData;
};

type EventMappings = {
Expand Down Expand Up @@ -467,6 +479,10 @@ export class AppContext extends Component {
if (utils.isElectron()) {
this.child(zoomComponent);
}

if (hasTouchBar) {
this.child(new TouchBarComponent());
}
}

renderWidgets() {
Expand Down
135 changes: 135 additions & 0 deletions src/public/app/components/touch_bar.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>;

constructor() {
super();
this.nativeImage = utils.dynamicRequire("electron").nativeImage;
this.remote = utils.dynamicRequire("@electron/remote") as typeof import("@electron/remote");
this.$widget = $("<div>");

$(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();
}

}
2 changes: 1 addition & 1 deletion src/public/app/layouts/desktop_layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 2 additions & 0 deletions src/public/app/services/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLCanvasElement>) {
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
}
Expand Down
5 changes: 5 additions & 0 deletions src/public/app/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,11 @@ declare global {
mention: MentionConfig
});
enableReadOnlyMode(reason: string);
commands: {
get(name: string): {
value: unknown;
};
}
model: {
document: {
on(event: string, cb: () => void);
Expand Down
5 changes: 4 additions & 1 deletion src/public/app/widgets/floating_buttons/edit_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export default class EditButton extends OnClickButtonWidget {
this.noteContext.viewScope.readOnlyTemporarilyDisabled = true;
appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext: this.noteContext });
}
this.refresh();
});
}

Expand Down Expand Up @@ -68,6 +67,10 @@ export default class EditButton extends OnClickButtonWidget {
}
}

readOnlyTemporarilyDisabledEvent() {
this.refresh();
}

async noteTypeMimeChangedEvent({ noteId }: { noteId: string }): Promise<void> {
if (this.isNote(noteId)) {
await this.refresh();
Expand Down
9 changes: 8 additions & 1 deletion src/public/app/widgets/note_list.ts
Original file line number Diff line number Diff line change
@@ -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*/`
Expand Down Expand Up @@ -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);
}
}

}
36 changes: 36 additions & 0 deletions src/public/app/widgets/note_tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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*/`
<div class="tree-wrapper">
Expand Down Expand Up @@ -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<TreeCommandNames>(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;
}
}
24 changes: 23 additions & 1 deletion src/public/app/widgets/type_widgets/editable_code.ts
Original file line number Diff line number Diff line change
@@ -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*/`
<div class="note-detail-code note-detail-printable">
Expand Down Expand Up @@ -61,6 +64,10 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
});

this.show();

if (this.parent && hasTouchBar) {
this.triggerCommand("refreshTouchBar");
}
}

getData() {
Expand All @@ -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;
}

}
Loading
Loading