Skip to content

Basic touchbar integration #1549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 45 commits into from
Apr 13, 2025
Merged
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
@@ -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() {
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
@@ -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";
2 changes: 2 additions & 0 deletions src/public/app/services/utils.ts
Original file line number Diff line number Diff line change
@@ -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);
}
5 changes: 5 additions & 0 deletions src/public/app/types.d.ts
Original file line number Diff line number Diff line change
@@ -339,6 +339,11 @@ declare global {
mention: MentionConfig
});
enableReadOnlyMode(reason: string);
commands: {
get(name: string): {
value: unknown;
};
}
model: {
document: {
on(event: string, cb: () => void);
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
@@ -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<void> {
if (this.isNote(noteId)) {
await this.refresh();
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*/`
@@ -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
@@ -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">
@@ -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">
@@ -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;
}

}
Loading