diff --git a/package.json b/package.json index 3648b2b..3fb4753 100644 --- a/package.json +++ b/package.json @@ -24,23 +24,27 @@ "clsx": "^2.0.0", "colormaster": "^1.2.1", "gl-matrix": "^3.4.3", + "js-yaml": "^4.1.0", "lucide-react": "^0.263.1", "mathjax-full": "^3.2.2", "monkey-around": "^2.3.0", "nanoid": "^4.0.2", + "opentype.js": "^1.3.4", "polybooljs": "^1.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", "roughjs": "^4.5.2", - "js-yaml": "^4.1.0", - "opentype.js": "^1.3.4", "woff2sfnt-sfnt2woff": "^1.0.0" }, "devDependencies": { - "dotenv": "^16.4.5", "@babel/core": "^7.22.9", "@babel/preset-env": "^7.22.10", "@babel/preset-react": "^7.22.5", + "@codemirror/commands": "^6.3.3", + "@codemirror/language": "^6.10.0", + "@codemirror/search": "^6.5.5", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.23.0", "@excalidraw/eslint-config": "^1.0.3", "@excalidraw/prettier-config": "^1.0.2", "@rollup/plugin-babel": "^6.0.3", @@ -50,14 +54,15 @@ "@rollup/plugin-typescript": "^11.1.6", "@types/chroma-js": "^2.4.0", "@types/js-beautify": "^1.14.0", + "@types/js-yaml": "^4.0.9", "@types/node": "^20.10.5", + "@types/opentype.js": "^1.3.8", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", - "@types/js-yaml": "^4.0.9", - "@types/opentype.js": "^1.3.8", "@zerollup/ts-transform-paths": "^1.7.18", "cross-env": "^7.0.3", "cssnano": "^6.0.2", + "dotenv": "^16.4.5", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "lz-string": "^1.5.0", @@ -70,12 +75,7 @@ "rollup-plugin-typescript2": "^0.34.1", "tslib": "^2.6.1", "ttypescript": "^1.5.15", - "typescript": "^5.2.2", - "@codemirror/commands": "^6.3.3", - "@codemirror/language": "^6.10.0", - "@codemirror/search": "^6.5.5", - "@codemirror/state": "^6.4.0", - "@codemirror/view": "^6.23.0" + "typescript": "^5.2.2" }, "resolutions": { "@typescript-eslint/typescript-estree": "5.3.0" diff --git a/rollup.config.js b/rollup.config.js index 3942ae5..7c089c9 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -100,7 +100,13 @@ const BUILD_CONFIG = { plugins: getRollupPlugins( {tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json"}, ...(isProd ? [ - terser({ toplevel: false, compress: { passes: 2 } }), + terser({ + toplevel: false, + compress: { passes: 2 }, + format: { + comments: false, // Remove all comments + }, + }), //!postprocess - the version available on npmjs does not work, need this update: // npm install brettz9/rollup-plugin-postprocess#update --save-dev // https://github.com/developit/rollup-plugin-postprocess/issues/10 diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index f1651e9..a809334 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -3350,6 +3350,7 @@ export default class ExcalidrawView extends TextFileView { currentStrokeOptions: st.currentStrokeOptions, frameRendering: st.frameRendering, objectsSnapModeEnabled: st.objectsSnapModeEnabled, + activeTool: st.activeTool, }, prevTextMode: this.prevTextMode, files, @@ -5254,6 +5255,7 @@ export default class ExcalidrawView extends TextFileView { } private renderWelcomeScreen () { + if(!this.plugin.settings.showSplashscreen) return null; const React = this.packages.react; const {WelcomeScreen} = this.packages.excalidrawLib; const filecount = this.app.vault.getFiles().filter(f=>this.plugin.isExcalidrawFile(f)).length; diff --git a/src/dialogs/HotkeyEditor.ts b/src/dialogs/HotkeyEditor.ts new file mode 100644 index 0000000..e2e2355 --- /dev/null +++ b/src/dialogs/HotkeyEditor.ts @@ -0,0 +1,162 @@ +import { BaseComponent, Setting, Modifier } from 'obsidian'; +import { DEVICE } from 'src/constants/constants'; +import { t } from 'src/lang/helpers'; +import { ExcalidrawSettings } from 'src/settings'; +import { modifierLabel } from 'src/utils/ModifierkeyHelper'; +import { fragWithHTML } from 'src/utils/Utils'; + +export class HotkeyEditor extends BaseComponent { + private settings: ExcalidrawSettings; + private containerEl: HTMLElement; + private capturing: boolean = false; + private activeModifiers: Modifier[] = []; + public isDirty: boolean = false; + private applySettingsUpdate: Function; + + // Store bound event handlers + private boundKeydownHandler: (event: KeyboardEvent) => void; + private boundKeyupHandler: (event: KeyboardEvent) => void; + + constructor(containerEl: HTMLElement, settings: ExcalidrawSettings, applySettingsUpdate: Function) { + super(); + this.containerEl = containerEl.createDiv(); + this.settings = settings; + this.applySettingsUpdate = applySettingsUpdate; + + // Bind the event handlers once in the constructor + this.boundKeydownHandler = this.onKeydown.bind(this); + this.boundKeyupHandler = this.onKeyup.bind(this); + } + + onload(): void { + this.render(); + } + + private render(): void { + // Clear previous content + this.containerEl.empty(); + + // Render current overrides + this.settings.modifierKeyOverrides.forEach((override, index) => { + const key = override.key.toUpperCase(); + new Setting(this.containerEl) + .setDesc(fragWithHTML(`Code: ${override.modifiers.join("+")} + ${key} | ` + + `Apple: ${modifierLabel(override.modifiers, "Mac")} + ${key} | ` + + `Windows: ${modifierLabel(override.modifiers, "Other")} + ${key}`)) + .addButton((button) => + button + .setButtonText(t("HOTKEY_BUTTON_REMOVE")) + .setCta() + .onClick(() => { + this.settings.modifierKeyOverrides.splice(index, 1); + this.isDirty = true; + this.applySettingsUpdate(); + this.render(); + }) + ); + }); + + // Render Add New Override or Capture Instruction + if (this.capturing) { + new Setting(this.containerEl) + .setName(t("HOTKEY_PRESS_COMBO_NANE")) + .setDesc(t("HOTKEY_PRESS_COMBO_DESC")) + .controlEl.style.cursor = 'pointer'; + } else { + new Setting(this.containerEl) + .addButton((button) => + button + .setButtonText(t("HOTKEY_BUTTON_ADD_OVERRIDE")) + .setCta() + .onClick(() => this.startCapture()) + ); + } + } + + private startCapture(): void { + this.capturing = true; + this.activeModifiers = []; + this.render(); + // Use the pre-bound handlers + window.addEventListener('keydown', this.boundKeydownHandler); + window.addEventListener('keyup', this.boundKeyupHandler); + } + + private onKeydown(event: KeyboardEvent): void { + event.preventDefault(); + event.stopPropagation(); + + const modifiers = this.getModifiersFromEvent(event); + + // If only modifiers are pressed, update activeModifiers and continue listening + if (['Control', 'Shift', 'Alt', 'Meta'].includes(event.key)) { + this.activeModifiers = modifiers; + return; + } + + const key = event.key.length === 1 ? event.key.toLowerCase() : event.key; + + // Check for duplicate overrides + const exists = this.settings.modifierKeyOverrides.some( + (override) => + override.key === key && + override.modifiers.length === modifiers.length && + override.modifiers.every((mod) => modifiers.includes(mod)) + ); + + if (!exists) { + this.settings.modifierKeyOverrides.push({ modifiers, key }); + this.isDirty = true; + this.applySettingsUpdate(); + } + + this.stopCapture(); + } + + private onKeyup(event: KeyboardEvent): void { + // If all modifier keys are released, stop capturing + if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) { + this.stopCapture(); + } + } + + private stopCapture(): void { + this.capturing = false; + // Use the pre-bound handlers for removal + window.removeEventListener('keydown', this.boundKeydownHandler); + window.removeEventListener('keyup', this.boundKeyupHandler); + this.render(); + } + + public unload(): void { + // Ensure listeners are removed when the component is unloaded + this.stopCapture(); + } + + private getModifiersFromEvent(event: KeyboardEvent): Modifier[] { + const modifiers: Modifier[] = []; + + if (DEVICE.isMacOS && event.metaKey) { + modifiers.push('Mod'); + } else if (!DEVICE.isMacOS && event.ctrlKey) { + modifiers.push('Mod'); + } + + if (DEVICE.isMacOS && event.ctrlKey) { + modifiers.push('Ctrl'); + } + + if (!DEVICE.isMacOS && event.metaKey) { + modifiers.push('Meta'); + } + + if (event.shiftKey) { + modifiers.push('Shift'); + } + if (event.altKey) { + modifiers.push('Alt'); + } + + return modifiers; + } +} diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index b078e1d..1757e2f 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -37,6 +37,7 @@ export default { TRANSCLUDE: "Embed a drawing", TRANSCLUDE_MOST_RECENT: "Embed the most recently edited drawing", TOGGLE_LEFTHANDED_MODE: "Toggle left-handed mode", + TOGGLE_SPLASHSCREEN: "Show splash screen in new drawings", FLIP_IMAGE: "Open the back-of-the-note of the selected excalidraw image", NEW_IN_NEW_PANE: "Create new drawing - IN AN ADJACENT WINDOW", NEW_IN_NEW_TAB: "Create new drawing - IN A NEW TAB", @@ -352,6 +353,10 @@ FILENAME_HEAD: "Filename", "
${labelCTRL()}+Enter to edit text or ${labelCTRL()}+K to create an element link ` +
+ "conflict with Obsidian hotkey settings. The hotkey combinations you add below will override Obsidian's hotkey settings while useing Excalidraw, thus " +
+ `you can add ${labelCTRL()}+G if you want to default to Group Object in Excalidraw instead of opening Graph View.`,
THEME_HEAD: "Theme and styling",
ZOOM_HEAD: "Zoom",
DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode",
@@ -739,6 +744,12 @@ FILENAME_HEAD: "Filename",
"the developer of Taskbone (as you can imagine, there is no such thing as 'free', providing this awesome OCR service costs some money to the developer of Taskbone), you can " +
"purchase a paid API key from taskbone.com. In case you have purchased a key, simply overwrite this auto generated free-tier API-key with your paid key.",
+ //HotkeyEditor
+ HOTKEY_PRESS_COMBO_NANE: "Press your hotkey combination",
+ HOTKEY_PRESS_COMBO_DESC: "Please press the desired key combination",
+ HOTKEY_BUTTON_ADD_OVERRIDE: "Add New Override",
+ HOTKEY_BUTTON_REMOVE: "Remove",
+
//openDrawings.ts
SELECT_FILE: "Select a file then press enter.",
SELECT_COMMAND: "Select a command then press enter.",
diff --git a/src/main.ts b/src/main.ts
index b7bd87d..9bc1124 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -2826,37 +2826,53 @@ export default class ExcalidrawPlugin extends Plugin {
this.popScope = null;
}
if (newActiveviewEV) {
- const scope = this.app.keymap.getRootScope();
- const handler_ctrlEnter = scope.register(["Mod"], "Enter", () => true);
- scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
- const handler_ctrlK = scope.register(["Mod"], "k", () => true);
- scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
- const handler_ctrlF = scope.register(["Mod"], "f", () => {
- const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
- if (view) {
- search(view);
- return true;
- }
- return false;
- });
- scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
- const overridSaveShortcut = (
- this.forceSaveCommand &&
- this.forceSaveCommand.hotkeys[0].key === "s" &&
- this.forceSaveCommand.hotkeys[0].modifiers.includes("Ctrl")
- )
- const saveHandler = overridSaveShortcut
- ? scope.register(["Ctrl"], "s", () => this.forceSaveActiveView(false))
- : undefined;
- if(saveHandler) {
- scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
- }
- this.popScope = () => {
- scope.unregister(handler_ctrlEnter);
- scope.unregister(handler_ctrlK);
- scope.unregister(handler_ctrlF);
- Boolean(saveHandler) && scope.unregister(saveHandler);
+ this.registerHotkeyOverrides();
+ }
+ }
+
+ public registerHotkeyOverrides() {
+ //this is repeated here because the same function is called when settings is closed after hotkeys have changed
+ if (this.popScope) {
+ this.popScope();
+ this.popScope = null;
+ }
+
+ if(!this.activeExcalidrawView) {
+ return;
+ }
+
+ const scope = this.app.keymap.getRootScope();
+ // Register overrides from settings
+ const overrideHandlers = this.settings.modifierKeyOverrides.map(override => {
+ return scope.register(override.modifiers, override.key, () => true);
+ });
+ // Force handlers to the front of the list
+ overrideHandlers.forEach(() => scope.keys.unshift(scope.keys.pop()));
+
+ const handler_ctrlF = scope.register(["Mod"], "f", () => {
+ const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
+ if (view) {
+ search(view);
+ return true;
}
+ return false;
+ });
+ scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
+ const overridSaveShortcut = (
+ this.forceSaveCommand &&
+ this.forceSaveCommand.hotkeys[0].key === "s" &&
+ this.forceSaveCommand.hotkeys[0].modifiers.includes("Ctrl")
+ )
+ const saveHandler = overridSaveShortcut
+ ? scope.register(["Ctrl"], "s", () => this.forceSaveActiveView(false))
+ : undefined;
+ if(saveHandler) {
+ scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
+ }
+ this.popScope = () => {
+ overrideHandlers.forEach(handler => scope.unregister(handler));
+ scope.unregister(handler_ctrlF);
+ Boolean(saveHandler) && scope.unregister(saveHandler);
}
}
diff --git a/src/settings.ts b/src/settings.ts
index 5513de7..dea7965 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -3,6 +3,7 @@ import {
ButtonComponent,
DropdownComponent,
getIcon,
+ Modifier,
normalizePath,
PluginSettingTab,
Setting,
@@ -39,6 +40,7 @@ import { EDITOR_FADEOUT } from "./CodeMirrorExtension/EditorHandler";
import { setDebugging } from "./utils/DebugHelper";
import { Rank } from "./menu/ActionIcons";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
+import { HotkeyEditor } from "./dialogs/HotkeyEditor";
export interface ExcalidrawSettings {
folder: string;
@@ -204,6 +206,8 @@ export interface ExcalidrawSettings {
longPressMobile: number;
isDebugMode: boolean;
rank: Rank;
+ modifierKeyOverrides: {modifiers: Modifier[], key: string}[];
+ showSplashscreen: boolean;
}
declare const PLUGIN_VERSION:string;
@@ -464,6 +468,11 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
longPressMobile: 500,
isDebugMode: false,
rank: "Bronze",
+ modifierKeyOverrides: [
+ {modifiers: ["Mod"], key:"Enter"},
+ {modifiers: ["Mod"], key:"k"},
+ ],
+ showSplashscreen: true,
};
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -472,6 +481,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
private requestReloadDrawings: boolean = false;
private requestUpdatePinnedPens: boolean = false;
private requestUpdateDynamicStyling: boolean = false;
+ private hotkeyEditor: HotkeyEditor;
//private reloadMathJax: boolean = false;
//private applyDebounceTimer: number = 0;
@@ -498,21 +508,25 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}
this.plugin.saveSettings();
if (this.requestUpdatePinnedPens) {
- app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
+ this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) v.view.updatePinnedCustomPens()
})
}
if (this.requestUpdateDynamicStyling) {
- app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
+ this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) {
setDynamicStyle(this.plugin.ea,v.view,v.view.previousBackgroundColor,this.plugin.settings.dynamicStyling);
}
})
}
+ this.hotkeyEditor.unload();
+ if (this.hotkeyEditor.isDirty) {
+ this.plugin.registerHotkeyOverrides();
+ }
if (this.requestReloadDrawings) {
const exs =
- app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
+ this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
for (const v of exs) {
if (v.view instanceof ExcalidrawView) {
await v.view.save(false);
@@ -1095,6 +1109,29 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
);
addIframe(detailsEl, "H8Njp7ZXYag",999);
+ new Setting(detailsEl)
+ .setName(t("TOGGLE_SPLASHSCREEN"))
+ .addToggle((toggle) =>
+ toggle
+ .setValue(this.plugin.settings.showSplashscreen)
+ .onChange((value)=> {
+ this.plugin.settings.showSplashscreen = value;
+ this.applySettingsUpdate();
+ })
+ )
+
+ detailsEl = displayDetailsEl.createEl("details");
+ detailsEl.createEl("summary", {
+ text: t("HOTKEY_OVERRIDE_HEAD"),
+ cls: "excalidraw-setting-h3",
+ });
+ detailsEl.createEl("span", {}, (el) => {
+ el.innerHTML = t("HOTKEY_OVERRIDE_DESC");
+ });
+
+ this.hotkeyEditor = new HotkeyEditor(detailsEl, this.plugin.settings, this.applySettingsUpdate);
+ this.hotkeyEditor.onload();
+
detailsEl = displayDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("THEME_HEAD"),
diff --git a/src/utils/ModifierkeyHelper.ts b/src/utils/ModifierkeyHelper.ts
index 92f6f98..017c290 100644
--- a/src/utils/ModifierkeyHelper.ts
+++ b/src/utils/ModifierkeyHelper.ts
@@ -1,3 +1,4 @@
+import { Modifier } from "obsidian";
import { DEVICE } from "src/constants/constants";
import { ExcalidrawSettings } from "src/settings";
export type ModifierKeys = {shiftKey:boolean, ctrlKey: boolean, metaKey: boolean, altKey: boolean};
@@ -177,4 +178,26 @@ export const emulateKeysForLinkClick = (action: PaneTarget): ModifierKeys => {
export const anyModifierKeysPressed = (e: ModifierKeys): boolean => {
return e.shiftKey || e.ctrlKey || e.metaKey || e.altKey;
+}
+
+export function modifierLabel(modifiers: Modifier[], platform?: "Mac" | "Other"): string {
+ const isMacPlatform = platform === "Mac" ||
+ (platform === undefined && (DEVICE.isIOS || DEVICE.isMacOS));
+
+ return modifiers.map(modifier => {
+ switch (modifier) {
+ case "Mod":
+ return isMacPlatform ? "CMD" : "CTRL";
+ case "Ctrl":
+ return "CTRL";
+ case "Meta":
+ return isMacPlatform ? "CMD" : "WIN";
+ case "Shift":
+ return "SHIFT";
+ case "Alt":
+ return isMacPlatform ? "OPTION" : "ALT";
+ default:
+ return modifier;
+ }
+ }).join("+");
}
\ No newline at end of file