From 9139d10bf5e1f957c63233e24b70b27e9f9e1efe Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sun, 30 Jul 2023 20:48:04 +0200 Subject: [PATCH 01/12] 0.2.6 --- manifest.json | 2 +- src/Components/AddToOntologyModal.ts | 205 +++++++++++++++++++++++++++ src/Scene.ts | 4 +- src/excalibrain-main.ts | 164 ++++++++++----------- src/graph/URLParser.ts | 24 ++-- src/lang/locale/en.ts | 6 +- src/utils/dataview.ts | 6 +- 7 files changed, 302 insertions(+), 109 deletions(-) create mode 100644 src/Components/AddToOntologyModal.ts diff --git a/manifest.json b/manifest.json index 20159c6..868b6f3 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "excalibrain", "name": "ExcaliBrain", - "version": "0.2.5", + "version": "0.2.6", "minAppVersion": "1.1.6", "description": "A clean, intuitive and editable graph view for Obsidian", "author": "Zsolt Viczian", diff --git a/src/Components/AddToOntologyModal.ts b/src/Components/AddToOntologyModal.ts new file mode 100644 index 0000000..5093379 --- /dev/null +++ b/src/Components/AddToOntologyModal.ts @@ -0,0 +1,205 @@ +import { create } from "domain"; +import { App, Modal, Notice, Setting } from "obsidian"; +import { createBinaryOps } from "obsidian-dataview/lib/expression/binaryop"; +import ExcaliBrain from "src/excalibrain-main"; +import { t } from "src/lang/helpers"; + +export enum Ontology { + Parent = "parent", + Child = "child", + LeftFriend = "leftFriend", + RightFriend = "rightFriend", + Previous = "previous", + Next = "next", +} + +export class AddToOntologyModal extends Modal { + private ontology:Ontology|null = null; + private fieldName:string; + constructor( + app: App, + private plugin: ExcaliBrain, + ) { + super(app); + } + + private getCurrentOntology():Ontology|null { + const { settings } = this.plugin; + const field = this.fieldName; + + if(settings.hierarchy.parents.includes(field)) { + return Ontology.Parent; + } + if(settings.hierarchy.children.includes(field)) { + return Ontology.Child; + } + if(settings.hierarchy.leftFriends.includes(field)) { + return Ontology.LeftFriend; + } + if(settings.hierarchy.rightFriends.includes(field)) { + return Ontology.RightFriend; + } + if(settings.hierarchy.previous.includes(field)) { + return Ontology.Previous; + } + if(settings.hierarchy.next.includes(field)) { + return Ontology.Next; + } + + return null; + } + + private async setOntology(ontology:Ontology) { + if(this.ontology === ontology) return; + const { settings } = this.plugin; + const plugin = this.plugin; + + //remove from current ontology + switch(this.ontology) { + case Ontology.Parent: + settings.hierarchy.parents = settings.hierarchy.parents.filter(f=>f!==this.fieldName); + plugin.hierarchyLowerCase.parents = []; + settings.hierarchy.parents.forEach(f=>plugin.hierarchyLowerCase.parents.push(f.toLowerCase().replaceAll(" ","-"))); + break; + case Ontology.Child: + settings.hierarchy.children = settings.hierarchy.children.filter(f=>f!==this.fieldName); + plugin.hierarchyLowerCase.children = []; + settings.hierarchy.children.forEach(f=>plugin.hierarchyLowerCase.children.push(f.toLowerCase().replaceAll(" ","-"))); + break; + case Ontology.LeftFriend: + settings.hierarchy.leftFriends = settings.hierarchy.leftFriends.filter(f=>f!==this.fieldName); + plugin.hierarchyLowerCase.leftFriends = []; + settings.hierarchy.leftFriends.forEach(f=>plugin.hierarchyLowerCase.leftFriends.push(f.toLowerCase().replaceAll(" ","-"))); + break; + case Ontology.RightFriend: + settings.hierarchy.rightFriends = settings.hierarchy.rightFriends.filter(f=>f!==this.fieldName); + plugin.hierarchyLowerCase.rightFriends = []; + settings.hierarchy.rightFriends.forEach(f=>plugin.hierarchyLowerCase.rightFriends.push(f.toLowerCase().replaceAll(" ","-"))); + break; + case Ontology.Previous: + settings.hierarchy.previous = settings.hierarchy.previous.filter(f=>f!==this.fieldName); + plugin.hierarchyLowerCase.previous = []; + settings.hierarchy.previous.forEach(f=>plugin.hierarchyLowerCase.previous.push(f.toLowerCase().replaceAll(" ","-"))); + break; + case Ontology.Next: + settings.hierarchy.next = settings.hierarchy.next.filter(f=>f!==this.fieldName); + plugin.hierarchyLowerCase.next = []; + settings.hierarchy.next.forEach(f=>plugin.hierarchyLowerCase.next.push(f.toLowerCase().replaceAll(" ","-"))); + break; + } + + //add to new ontology + switch(ontology) { + case Ontology.Parent: + settings.hierarchy.parents.push(this.fieldName); + settings.hierarchy.parents = settings.hierarchy.parents.sort((a,b)=>a.toLowerCase()plugin.hierarchyLowerCase.parents.push(f.toLowerCase().replaceAll(" ","-"))); + break; + case Ontology.Child: + settings.hierarchy.children.push(this.fieldName); + settings.hierarchy.children = settings.hierarchy.children.sort((a,b)=>a.toLowerCase()plugin.hierarchyLowerCase.children.push(f.toLowerCase().replaceAll(" ","-"))); + break; + case Ontology.LeftFriend: + settings.hierarchy.leftFriends.push(this.fieldName); + settings.hierarchy.leftFriends = settings.hierarchy.leftFriends.sort((a,b)=>a.toLowerCase()plugin.hierarchyLowerCase.leftFriends.push(f.toLowerCase().replaceAll(" ","-"))); + break; + case Ontology.RightFriend: + settings.hierarchy.rightFriends.push(this.fieldName); + settings.hierarchy.rightFriends = settings.hierarchy.rightFriends.sort((a,b)=>a.toLowerCase()plugin.hierarchyLowerCase.rightFriends.push(f.toLowerCase().replaceAll(" ","-"))); + break; + case Ontology.Previous: + settings.hierarchy.previous.push(this.fieldName); + settings.hierarchy.previous = settings.hierarchy.previous.sort((a,b)=>a.toLowerCase()plugin.hierarchyLowerCase.previous.push(f.toLowerCase().replaceAll(" ","-"))); + break; + case Ontology.Next: + settings.hierarchy.next.push(this.fieldName); + settings.hierarchy.next = settings.hierarchy.next.sort((a,b)=>a.toLowerCase()plugin.hierarchyLowerCase.next.push(f.toLowerCase().replaceAll(" ","-"))); + break; + } + await this.plugin.saveSettings(); + if (plugin.scene && !plugin.scene.terminated) { + plugin.scene.vaultFileChanged = true; + } + new Notice(`Added ${this.fieldName} as ${ontology}`); + this.fieldName = null; + this.close(); + } + + async show( fieldName:string ) { + await this.plugin.loadSettings(); + this.fieldName = fieldName; + this.ontology = this.getCurrentOntology(); + this.open(); + } + + public async addFieldToOntology(ontology:Ontology, fieldName:string) { + await this.plugin.loadSettings(); + this.fieldName = fieldName; + this.ontology = this.getCurrentOntology(); + await this.setOntology(ontology); + this.fieldName = null; + } + + open () { + if(!this.fieldName) return; + const { contentEl, titleEl } = this; + titleEl.setText(this.fieldName); + contentEl.createEl("p", {text: t("ADD_TO_ONTOLOGY_MODAL_DESC")}); + const setting = new Setting(contentEl) + .addButton((b) => { + b.buttonEl.style.flex = "1 0 calc(33.33% - var(--size-4-2))"; + b.setButtonText(t("PARENTS_NAME")) + if(this.ontology === Ontology.Parent) b.setCta(); + b.onClick(()=>this.setOntology(Ontology.Parent)) + }) + .addButton((b) => { + b.buttonEl.style.flex = "1 0 calc(33.33% - var(--size-4-2))"; + b.setButtonText(t("CHILDREN_NAME")) + if(this.ontology === Ontology.Child) b.setCta(); + b.onClick(()=>this.setOntology(Ontology.Child)) + }) + .addButton((b) => { + b.buttonEl.style.flex = "1 0 calc(33.33% - var(--size-4-2))"; + b.setButtonText(t("LEFT_FRIENDS_NAME")) + if(this.ontology === Ontology.LeftFriend) b.setCta(); + b.onClick(()=>this.setOntology(Ontology.LeftFriend)) + }) + .addButton((b) => { + b.buttonEl.style.flex = "1 0 calc(33.33% - var(--size-4-2))"; + b.setButtonText(t("RIGHT_FRIENDS_NAME")) + if(this.ontology === Ontology.RightFriend) b.setCta(); + b.onClick(()=>this.setOntology(Ontology.RightFriend)) + }) + .addButton((b) => { + b.buttonEl.style.flex = "1 0 calc(33.33% - var(--size-4-2))"; + b.setButtonText(t("PREVIOUS_NAME")) + if(this.ontology === Ontology.Previous) b.setCta(); + b.onClick(()=>this.setOntology(Ontology.Previous)) + }) + .addButton((b) => { + b.buttonEl.style.flex = "1 0 calc(33.33% - var(--size-4-2))"; + b.setButtonText(t("NEXT_NAME")) + if(this.ontology === Ontology.Next) b.setCta(); + b.onClick(()=>this.setOntology(Ontology.Next)) + }); + setting.controlEl.style.flexWrap = "wrap"; + setting.controlEl.style.justifyContent = "space-between"; + super.open(); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} \ No newline at end of file diff --git a/src/Scene.ts b/src/Scene.ts index a8e2c1b..8c71bd2 100644 --- a/src/Scene.ts +++ b/src/Scene.ts @@ -542,11 +542,11 @@ export class Scene { this.layouts.push(lChildren); const friendOrigoX = isCompactView && isCenterEmbedded - ? centerEmbedWidth/2 + 1.5 * this.nodeWidth + ? centerEmbedWidth/2 + this.nodeWidth : Math.max( (((manyNextFriends?1:0)+Math.max(childrenCols,parentCols)+1.9)/2.4) * this.nodeWidth, // (manyChildren ? -3 : -2) * this.nodeWidth, isCenterEmbedded - ? centerEmbedWidth/2 + 1.5 * this.nodeWidth + ? centerEmbedWidth/2 + this.nodeWidth : 0 ); diff --git a/src/excalibrain-main.ts b/src/excalibrain-main.ts index b643b11..9977e48 100644 --- a/src/excalibrain-main.ts +++ b/src/excalibrain-main.ts @@ -1,4 +1,4 @@ -import { App, MarkdownView, Notice, Plugin, PluginManifest, TextFileView, TFile, TFolder, WorkspaceLeaf } from 'obsidian'; +import { App, Editor, MarkdownView, Menu, MenuItem, Notice, Plugin, PluginManifest, TextFileView, TFile, TFolder, WorkspaceLeaf } from 'obsidian'; import { Page } from './graph/Page'; import { DEFAULT_SETTINGS, ExcaliBrainSettings, ExcaliBrainSettingTab } from './Settings'; import { errorlog, keepOnTop } from './utils/utils'; @@ -16,6 +16,7 @@ import { FieldSuggester } from './Suggesters/OntologySuggester'; import { Literal } from 'obsidian-dataview/lib/data-model/value'; import { isEmbedFileType } from './utils/fileUtils'; import { URLParser } from './graph/URLParser'; +import { AddToOntologyModal, Ontology } from './Components/AddToOntologyModal'; declare module "obsidian" { interface App { @@ -58,6 +59,7 @@ export default class ExcaliBrain extends Plugin { public customNodeLabel: (dvPage: Literal, defaultName:string) => string public navigationHistory: string[] = []; public urlParser: URLParser; + private addToOntologyModal: AddToOntologyModal; constructor(app: App, manifest: PluginManifest) { super(app, manifest); @@ -69,6 +71,7 @@ export default class ExcaliBrain extends Plugin { "Initializing index, please wait" ) ] + this.addToOntologyModal = new AddToOntologyModal(app,this); } async onload() { @@ -76,6 +79,7 @@ export default class ExcaliBrain extends Plugin { this.navigationHistory = this.settings.navigationHistory; this.addSettingTab(new ExcaliBrainSettingTab(this.app, this)); this.registerEditorSuggest(new FieldSuggester(this)); + this.registerEvents(); this.urlParser = new URLParser(this); this.app.workspace.onLayoutReady(()=>{ this.urlParser.init(); @@ -138,6 +142,46 @@ export default class ExcaliBrain extends Plugin { }); } + private registerEvents() { + this.registerEvent( + this.app.workspace.on("editor-menu", this.handleEditorMenu, this) + ); + } + + private getFieldName(editor: Editor): string { + let line = editor.getLine(editor.getCursor().line).substring(0,editor.getCursor().ch); + const regex = /(?:^|[(\[])(?:==|\*\*|~~|\*|_|__)?([^\:\]\()]*?)(?:==|\*\*|~~|\*|_|__)?::/g; + let lastMatch = null; + let match; + + while ((match = regex.exec(line)) !== null) { + lastMatch = match; + } + + //default to the full line, maybe the user positioned the cursor in the middle of the field + if(!lastMatch) { + line = editor.getLine(editor.getCursor().line); + while ((match = regex.exec(line)) !== null) { + lastMatch = match; + } + } + return lastMatch !== null ? lastMatch[1] : null; + } + + private handleEditorMenu(menu: Menu, editor: Editor, view: MarkdownView) { + const field = this.getFieldName(editor); + if(field) { + menu.addItem((item: MenuItem) => { + item + .setTitle(`Add "${field}" to ExcaliBrain Ontology`) + .setIcon("plus") + .onClick(() => { + this.addToOntologyModal.show(field); + }); + }); + } + } + public lowercasePathMap: Map; public async createIndex() { @@ -304,132 +348,72 @@ export default class ExcaliBrain extends Plugin { searchElement?.focus(); } + private addFieldToOntology(field: string, direction: Ontology) { + this.addToOntologyModal.addFieldToOntology(direction, field); + } + private registerCommands() { - const addFieldToOntology = (checking: boolean, desc: string):boolean => { - const activeView = app.workspace.activeLeaf?.view; - if(checking) { - return (activeView instanceof MarkdownView) && activeView.getMode() === "source"; - } - if(!(activeView instanceof MarkdownView) || activeView.getMode() !== "source") { + const addFieldToOntology = (checking: boolean, direction: Ontology | "select"):boolean => { + const activeView = app.workspace.activeLeaf?.view; + if(!activeView || !(activeView instanceof MarkdownView) || activeView.getMode() !== "source") { return false; } - const cursor = activeView.editor.getCursor(); - const line = activeView.editor.getLine(cursor.line); - const field = line.match(/^([^\:]*)::/); + const field = this.getFieldName(activeView.editor); if(!field) { return false; } - - (async() => { - await this.loadSettings(); - - if(this.settings.hierarchy.parents.includes(field[1])) { - new Notice(`${field[1]} is already registered as a PARENT`); - return; - } - if(this.settings.hierarchy.children.includes(field[1])) { - new Notice(`${field[1]} is already registered as a CHILD`); - return; - } - if(this.settings.hierarchy.leftFriends.includes(field[1])) { - new Notice(`${field[1]} is already registered as a LEFT-SIDE FRIEND`); - return; - } - if(this.settings.hierarchy.rightFriends.includes(field[1])) { - new Notice(`${field[1]} is already registered as a RIGHT-SIDE FRIEND`); - return; - } - if(this.settings.hierarchy.previous.includes(field[1])) { - new Notice(`${field[1]} is already registered as a PREVIOUS (FRIEND)`); - return; - } - if(this.settings.hierarchy.next.includes(field[1])) { - new Notice(`${field[1]} is already registered as a NEXT (FRIEND)`); - return; - } - - switch (desc) { - case "parent": - this.settings.hierarchy.parents.push(field[1]); - this.settings.hierarchy.parents = this.settings.hierarchy.parents.sort((a,b)=>a.toLowerCase()this.hierarchyLowerCase.parents.push(f.toLowerCase().replaceAll(" ","-"))) - break; - case "child": - this.settings.hierarchy.children.push(field[1]); - this.settings.hierarchy.children = this.settings.hierarchy.children.sort((a,b)=>a.toLowerCase()this.hierarchyLowerCase.children.push(f.toLowerCase().replaceAll(" ","-"))) - break; - case "rightFriend": - this.settings.hierarchy.rightFriends.push(field[1]); - this.settings.hierarchy.rightFriends = this.settings.hierarchy.rightFriends.sort((a,b)=>a.toLowerCase()this.hierarchyLowerCase.rightFriends.push(f.toLowerCase().replaceAll(" ","-"))) - break; - case "previous": - this.settings.hierarchy.previous.push(field[1]); - this.settings.hierarchy.previous = this.settings.hierarchy.previous.sort((a,b)=>a.toLowerCase()this.hierarchyLowerCase.previous.push(f.toLowerCase().replaceAll(" ","-"))) - break; - case "next": - this.settings.hierarchy.next.push(field[1]); - this.settings.hierarchy.next = this.settings.hierarchy.next.sort((a,b)=>a.toLowerCase()this.hierarchyLowerCase.next.push(f.toLowerCase().replaceAll(" ","-"))) - break; - default: - this.settings.hierarchy.leftFriends.push(field[1]); - this.settings.hierarchy.leftFriends = this.settings.hierarchy.leftFriends.sort((a,b)=>a.toLowerCase()this.hierarchyLowerCase.leftFriends.push(f.toLowerCase().replaceAll(" ","-"))) - } - await this.saveSettings(); - if (this.scene && !this.scene.terminated) { - this.scene.vaultFileChanged = true; - } - new Notice(`Added ${field[1]} as ${desc}`); - })(); - + if(checking) { + return true; + } + if(direction === "select") { + this.addToOntologyModal.show(field); + return true; + } + this.addFieldToOntology(field,direction); return true; - } + } this.addCommand({ id: "excalibrain-addParentField", name: t("COMMAND_ADD_PARENT_FIELD"), - checkCallback: (checking: boolean) => addFieldToOntology(checking, "parent"), + checkCallback: (checking: boolean) => addFieldToOntology(checking, Ontology.Parent), }); this.addCommand({ id: "excalibrain-addChildField", name: t("COMMAND_ADD_CHILD_FIELD"), - checkCallback: (checking: boolean) => addFieldToOntology(checking, "child"), + checkCallback: (checking: boolean) => addFieldToOntology(checking, Ontology.Child), }); this.addCommand({ id: "excalibrain-addLeftFriendField", name: t("COMMAND_ADD_LEFT_FRIEND_FIELD"), - checkCallback: (checking: boolean) => addFieldToOntology(checking, "leftFriend"), + checkCallback: (checking: boolean) => addFieldToOntology(checking, Ontology.LeftFriend), }); this.addCommand({ id: "excalibrain-addRightFriendField", name: t("COMMAND_ADD_RIGHT_FRIEND_FIELD"), - checkCallback: (checking: boolean) => addFieldToOntology(checking, "rightFriend"), + checkCallback: (checking: boolean) => addFieldToOntology(checking, Ontology.RightFriend), }); this.addCommand({ id: "excalibrain-addPreviousField", name: t("COMMAND_ADD_PREVIOUS_FIELD"), - checkCallback: (checking: boolean) => addFieldToOntology(checking, "previous"), + checkCallback: (checking: boolean) => addFieldToOntology(checking, Ontology.Previous), }); this.addCommand({ id: "excalibrain-addNextField", name: t("COMMAND_ADD_NEXT_FIELD"), - checkCallback: (checking: boolean) => addFieldToOntology(checking, "next"), + checkCallback: (checking: boolean) => addFieldToOntology(checking, Ontology.Next), + }); + + this.addCommand({ + id: "excalibrain-selectOntology", + name: t("COMMAND_ADD_ONTOLOGY_MODAL"), + checkCallback: (checking: boolean) => addFieldToOntology(checking, "select"), }); this.addCommand({ diff --git a/src/graph/URLParser.ts b/src/graph/URLParser.ts index 721e238..eb71b4a 100644 --- a/src/graph/URLParser.ts +++ b/src/graph/URLParser.ts @@ -7,8 +7,13 @@ export interface FileURL { origin: string; } -export const linkRegex = /\[([^[\]]+)\]\((https?:\d*?\/\/[a-z0-9&#=.\/\-?_]+)\)/gi; // Matches links in markdown format [label](url) -export const plainLinkRegex = /(https?:\d*?\/\/[a-z0-9&#=.\/\-?_]+)/gi; // Matches plain links +// Matches links in markdown format [label](url) +//export const linkRegex = /\[([^[\]]+)\]\(((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))\)/gi; +// Matches plain links +//export const plainLinkRegex = /((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/gi; +//export const plainLinkRegex = /((?:(?:ftp|https?|sftp|shttp|tftp):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/gi; + +export const linkRegex = /(?:\[([^[\]]+)\]\()((?:(?:ftp|https?|sftp|shttp|tftp):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>"']|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))\)|\b()((?:(?:ftp|https?|sftp|shttp|tftp):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>"']|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))\b/gi; export class URLParser { fileToUrlMap: Map = new Map(); @@ -52,7 +57,11 @@ export class URLParser { let match; while ((match = linkRegex.exec(content)) !== null) { - const [, alias, url] = match; + const alias = match[1] ?? match[3] ?? ""; + let url = match[2] ?? match[4] ?? ""; + if(!url.match(/^(?:ftp|https?|sftp|shttp|tftp):/)) { + url = "https://" + url; + } if (!links.has(url)) { const origin = this.getOrigin(url,file); links.set(url,{ url, alias, origin}); @@ -65,15 +74,6 @@ export class URLParser { } } - while ((match = plainLinkRegex.exec(content)) !== null) { - const url = match[0]; - if (!links.has(url)) { - const origin = this.getOrigin(url,file); - links.set(url, { url, alias: '', origin}); - this.updateInverseMap(url, file, origin); - } - } - const linkArray = Array.from(links.values()); if (linkArray.length > 0) { this.fileToUrlMap.set(file, linkArray); diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index d62841f..fa949de 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -172,6 +172,7 @@ export default { COMMAND_ADD_RIGHT_FRIEND_FIELD: "Add dataview field to ontology as RIGHT-SIDE FRIEND", COMMAND_ADD_PREVIOUS_FIELD: "Add dataview field to ontology as PREVIOUS", COMMAND_ADD_NEXT_FIELD: "Add dataview field to ontology as NEXT", + COMMAND_ADD_ONTOLOGY_MODAL: "Add dataview field to ontology: Open Ontology Modal", COMMAND_START: "ExcaliBrain Normal", COMMAND_START_HOVER: "ExcaliBrain Hover-Editor", COMMAND_START_POPOUT: "ExcaliBrain Popout Window", @@ -191,5 +192,8 @@ export default { SHOW_HIDE_FOLDER: "Show/Hide folder nodes", SHOW_HIDE_TAG: "Show/Hide tag nodes", SHOW_HIDE_PAGES: "Show/Hide page nodes (incl. defined, inferred, virtual and attachments)", - PIN_LEAF: "Link ExcaliBrain to most recent active leaf" + PIN_LEAF: "Link ExcaliBrain to most recent active leaf", + + //AddToOntologyModal + ADD_TO_ONTOLOGY_MODAL_DESC: "Select the direction of the ontology. If one of the buttons is highlighted, then the field is already part of the ontology in that direction.", } diff --git a/src/utils/dataview.ts b/src/utils/dataview.ts index cd0f8ad..330095a 100644 --- a/src/utils/dataview.ts +++ b/src/utils/dataview.ts @@ -1,7 +1,7 @@ import { App, TFile } from "obsidian"; import { Literal } from "obsidian-dataview/lib/data-model/value"; import ExcaliBrain from "src/excalibrain-main"; -import { plainLinkRegex } from "src/graph/URLParser"; +import { linkRegex } from "src/graph/URLParser"; import { ExcaliBrainSettings } from "src/Settings"; import { NodeStyle } from "src/types"; @@ -32,8 +32,8 @@ const readLinksFromString = (data: string, file:TFile):string[] => { } let match; - while ((match = plainLinkRegex.exec(data)) !== null) { - res.add(match[0]); + while ((match = linkRegex.exec(data)) !== null) { + res.add(match[2]??match[4]); } return Array.from(res); From 4d5846143758f4c18bbbaed53afe3e55858aaa81 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Mon, 31 Jul 2023 08:00:53 +0200 Subject: [PATCH 02/12] tools panel rerender when embedCentralNode, arrow direction, --- src/Scene.ts | 5 +++-- src/Settings.ts | 13 +++++++++++++ src/excalibrain-main.ts | 12 ++++++------ src/graph/Links.ts | 6 +++--- src/graph/Page.ts | 28 ++++++++++++++-------------- src/graph/Pages.ts | 24 ++++++++++++------------ src/lang/locale/en.ts | 2 ++ 7 files changed, 53 insertions(+), 37 deletions(-) diff --git a/src/Scene.ts b/src/Scene.ts index 9c266f6..f6a8f82 100644 --- a/src/Scene.ts +++ b/src/Scene.ts @@ -102,7 +102,8 @@ export class Scene { const centralPage = this.plugin.pages.get(this.centralPagePath); if( centralPage?.file && - !(centralPage.isFolder || centralPage.isTag || centralPage.isVirtual) + !(centralPage.isFolder || centralPage.isTag || centralPage.isVirtual) && + !this.plugin.settings.embedCentralNode ) { if(!this.centralLeaf) { this.ea.openFileInNewOrAdjacentLeaf(centralPage.file); @@ -113,7 +114,7 @@ export class Scene { this.centralLeaf.openFile(centralPage.file, {active: false}); } } - await this.render(); + await this.render(this.plugin.settings.embedCentralNode); } private getCentralPage():Page { diff --git a/src/Settings.ts b/src/Settings.ts index 0f2e224..0e0c270 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -27,6 +27,7 @@ export interface ExcaliBrainSettings { hierarchy: Hierarchy; inferAllLinksAsFriends: boolean; inverseInfer: boolean; + inverseArrowDirection: boolean; renderAlias: boolean; nodeTitleScript: string; backgroundColor: string; @@ -85,6 +86,7 @@ export const DEFAULT_SETTINGS: ExcaliBrainSettings = { hierarchy: DEFAULT_HIERARCHY_DEFINITION, inferAllLinksAsFriends: false, inverseInfer: false, + inverseArrowDirection: false, renderAlias: true, nodeTitleScript: "", backgroundColor: "#0c3e6aff", @@ -1565,6 +1567,17 @@ export class ExcaliBrainSettingTab extends PluginSettingTab { }) ) + new Setting(containerEl) + .setName(t("INVERSE_ARROW_DIRECTION_NAME")) + .setDesc(fragWithHTML(t("INVERSE_ARROW_DIRECTION_DESC"))) + .addToggle(toggle => + toggle + .setValue(this.plugin.settings.inverseArrowDirection) + .onChange(value => { + this.plugin.settings.inverseArrowDirection = value; + this.dirty = true; + }) + ) let pSetting:Setting, cSetting:Setting, fSetting:Setting, gSetting: Setting, mSetting: Setting, bSetting: Setting; diff --git a/src/excalibrain-main.ts b/src/excalibrain-main.ts index 9977e48..15be89f 100644 --- a/src/excalibrain-main.ts +++ b/src/excalibrain-main.ts @@ -221,16 +221,16 @@ export default class ExcaliBrain extends Plugin { if(f instanceof TFolder) { const child = new Page(this.pages,"folder:"+f.path, null, this, true, false, f.name); this.pages.add("folder:"+f.path,child); - child.addParent(parent,RelationType.DEFINED,LinkDirection.FROM,"file-tree"); - parent.addChild(child,RelationType.DEFINED,LinkDirection.TO,"file-tree"); + child.addParent(parent,RelationType.DEFINED,LinkDirection.TO,"file-tree"); + parent.addChild(child,RelationType.DEFINED,LinkDirection.FROM,"file-tree"); addFolderChildren(f,child); return; } else { this.lowercasePathMap.set(f.path.toLowerCase(),f.path); //path case sensitivity issue (see Pages.ts and Scene.ts for more) const child = new Page(this.pages,f.path,f as TFile,this); this.pages.add(f.path,child); - child.addParent(parent,RelationType.DEFINED,LinkDirection.FROM,"file-tree"); - parent.addChild(child,RelationType.DEFINED,LinkDirection.TO,"file-tree"); + child.addParent(parent,RelationType.DEFINED,LinkDirection.TO,"file-tree"); + parent.addChild(child,RelationType.DEFINED,LinkDirection.FROM,"file-tree"); } }) } @@ -313,7 +313,7 @@ export default class ExcaliBrain extends Plugin { } private excalidrawAvailable():boolean { - const ea = getEA(); + const ea = getEA(this.scene?.leaf?.view); if(!ea) { this.EA = null; if(this.scene) { @@ -322,7 +322,7 @@ export default class ExcaliBrain extends Plugin { new Notice("ExcaliBrain: Please start Excalidraw and try again.",4000); return false; } - if(!this.EA !== ea) { + if(this.EA !== ea) { this.EA = ea; this.registerExcalidrawAutomateHooks() } diff --git a/src/graph/Links.ts b/src/graph/Links.ts index 728a495..f6c5083 100644 --- a/src/graph/Links.ts +++ b/src/graph/Links.ts @@ -30,13 +30,13 @@ export class Links { } const key2 = nodeB.page.path+SEPARATOR+nodeA.page.path; const link = new Link( - linkDirection===LinkDirection.FROM + linkDirection===(settings.inverseArrowDirection ? LinkDirection.TO : LinkDirection.FROM) ? nodeB : nodeA, - linkDirection===LinkDirection.FROM + linkDirection===(settings.inverseArrowDirection ? LinkDirection.TO : LinkDirection.FROM) ? nodeA : nodeB, - linkDirection===LinkDirection.FROM + linkDirection===(settings.inverseArrowDirection ? LinkDirection.TO : LinkDirection.FROM) ? nodeBRole === Role.LEFT || nodeBRole === Role.RIGHT ? nodeBRole === Role.LEFT ? Role.LEFT : Role.RIGHT : nodeBRole === Role.CHILD diff --git a/src/graph/Page.ts b/src/graph/Page.ts index ca3d233..17035fd 100644 --- a/src/graph/Page.ts +++ b/src/graph/Page.ts @@ -95,8 +95,8 @@ export class Page { return; } if(this.neighbours.has(path)) return; //https://github.com/zsviczian/excalibrain/issues/74 - child.addParent(this,RelationType.DEFINED,LinkDirection.FROM,"tag-tree"); - this.addChild(child,RelationType.DEFINED,LinkDirection.TO,"tag-tree"); + child.addParent(this,RelationType.DEFINED,LinkDirection.TO,"tag-tree"); + this.addChild(child,RelationType.DEFINED,LinkDirection.FROM,"tag-tree"); }); return; } @@ -129,8 +129,8 @@ export class Page { //log(`Unexpected: ${this.file.path} references ${item.link} in DV, but it was not found in app.metadataCache. The page was skipped.`); //return; } - this.addParent(referencedPage,RelationType.DEFINED,LinkDirection.TO, item.field); - referencedPage.addChild(this,RelationType.DEFINED,LinkDirection.FROM, item.field); + this.addParent(referencedPage,RelationType.DEFINED,LinkDirection.FROM, item.field); + referencedPage.addChild(this,RelationType.DEFINED,LinkDirection.TO, item.field); }); const childFields = this.plugin.hierarchyLowerCase.children; @@ -141,8 +141,8 @@ export class Page { //log(`Unexpected: ${this.file.path} references ${item.link} in DV, but it was not found in app.metadataCache. The page was skipped.`); //return; } - this.addChild(referencedPage,RelationType.DEFINED,LinkDirection.TO, item.field); - referencedPage.addParent(this,RelationType.DEFINED,LinkDirection.FROM, item.field); + this.addChild(referencedPage,RelationType.DEFINED,LinkDirection.FROM, item.field); + referencedPage.addParent(this,RelationType.DEFINED,LinkDirection.TO, item.field); }); const leftFriendFields = this.plugin.hierarchyLowerCase.leftFriends; @@ -153,8 +153,8 @@ export class Page { //log(`Unexpected: ${this.file.path} references ${item.link} in DV, but it was not found in app.metadataCache. The page was skipped.`); //return; } - this.addLeftFriend(referencedPage,RelationType.DEFINED,LinkDirection.TO,item.field); - referencedPage.addLeftFriend(this,RelationType.DEFINED,LinkDirection.FROM, item.field); + this.addLeftFriend(referencedPage,RelationType.DEFINED,LinkDirection.FROM,item.field); + referencedPage.addLeftFriend(this,RelationType.DEFINED,LinkDirection.TO, item.field); }); const rightFriendFields = this.plugin.hierarchyLowerCase.rightFriends; @@ -165,8 +165,8 @@ export class Page { //log(`Unexpected: ${this.file.path} references ${item.link} in DV, but it was not found in app.metadataCache. The page was skipped.`); //return; } - this.addRightFriend(referencedPage,RelationType.DEFINED,LinkDirection.TO,item.field); - referencedPage.addRightFriend(this,RelationType.DEFINED,LinkDirection.FROM, item.field); + this.addRightFriend(referencedPage,RelationType.DEFINED,LinkDirection.FROM,item.field); + referencedPage.addRightFriend(this,RelationType.DEFINED,LinkDirection.TO, item.field); }); const previousFields = this.plugin.hierarchyLowerCase.previous; @@ -177,8 +177,8 @@ export class Page { //log(`Unexpected: ${this.file.path} references ${item.link} in DV, but it was not found in app.metadataCache. The page was skipped.`); //return; } - this.addPreviousFriend(referencedPage,RelationType.DEFINED,LinkDirection.TO,item.field); - referencedPage.addNextFriend(this,RelationType.DEFINED,LinkDirection.FROM, item.field); + this.addPreviousFriend(referencedPage,RelationType.DEFINED,LinkDirection.FROM,item.field); + referencedPage.addNextFriend(this,RelationType.DEFINED,LinkDirection.TO, item.field); }); const nextFields = this.plugin.hierarchyLowerCase.next; @@ -189,8 +189,8 @@ export class Page { //log(`Unexpected: ${this.file.path} references ${item.link} in DV, but it was not found in app.metadataCache. The page was skipped.`); //return; } - this.addNextFriend(referencedPage,RelationType.DEFINED,LinkDirection.TO,item.field); - referencedPage.addPreviousFriend(this,RelationType.DEFINED,LinkDirection.FROM, item.field); + this.addNextFriend(referencedPage,RelationType.DEFINED,LinkDirection.FROM,item.field); + referencedPage.addPreviousFriend(this,RelationType.DEFINED,LinkDirection.TO, item.field); }); } diff --git a/src/graph/Pages.ts b/src/graph/Pages.ts index 7f036a3..ff1eadd 100644 --- a/src/graph/Pages.ts +++ b/src/graph/Pages.ts @@ -51,15 +51,15 @@ export class Pages { private addInferredParentChild(parent: Page, child: Page) { if(this.plugin.settings.inferAllLinksAsFriends) { - child.addLeftFriend(parent,RelationType.INFERRED, LinkDirection.FROM); - parent.addLeftFriend(child,RelationType.INFERRED, LinkDirection.TO); + child.addLeftFriend(parent,RelationType.INFERRED, LinkDirection.TO); + parent.addLeftFriend(child,RelationType.INFERRED, LinkDirection.FROM); } else { if(this.plugin.settings.inverseInfer) { //https://github.com/zsviczian/excalibrain/issues/78 - child.addChild(parent,RelationType.INFERRED, LinkDirection.FROM); - parent.addParent(child,RelationType.INFERRED, LinkDirection.TO); + child.addChild(parent,RelationType.INFERRED, LinkDirection.TO); + parent.addParent(child,RelationType.INFERRED, LinkDirection.FROM); } else { - child.addParent(parent,RelationType.INFERRED, LinkDirection.FROM); - parent.addChild(child,RelationType.INFERRED, LinkDirection.TO); + child.addParent(parent,RelationType.INFERRED, LinkDirection.TO); + parent.addChild(child,RelationType.INFERRED, LinkDirection.FROM); } } } @@ -134,15 +134,15 @@ export class Pages { export const addUnresolvedPage = (childPath: string, parent: Page, plugin: ExcaliBrain, pages: Pages):Page => { const newPage = pages.get(childPath) ?? new Page(pages,childPath,null,plugin); if(plugin.settings.inferAllLinksAsFriends) { - newPage.addLeftFriend(parent,RelationType.INFERRED, LinkDirection.FROM); - parent.addLeftFriend(newPage,RelationType.INFERRED, LinkDirection.TO); + newPage.addLeftFriend(parent,RelationType.INFERRED, LinkDirection.TO); + parent.addLeftFriend(newPage,RelationType.INFERRED, LinkDirection.FROM); } else { if(plugin.settings.inverseInfer) { //https://github.com/zsviczian/excalibrain/issues/78 - newPage.addChild(parent,RelationType.INFERRED, LinkDirection.FROM); - parent.addParent(newPage,RelationType.INFERRED, LinkDirection.TO); + newPage.addChild(parent,RelationType.INFERRED, LinkDirection.TO); + parent.addParent(newPage,RelationType.INFERRED, LinkDirection.FROM); } else { - newPage.addParent(parent,RelationType.INFERRED, LinkDirection.FROM); - parent.addChild(newPage,RelationType.INFERRED, LinkDirection.TO); + newPage.addParent(parent,RelationType.INFERRED, LinkDirection.TO); + parent.addChild(newPage,RelationType.INFERRED, LinkDirection.FROM); } } pages.add(childPath,newPage); diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index fa949de..4c83aeb 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -28,6 +28,8 @@ export default { "
  • If files mutually link to each other, they are FRIENDS
  • ", REVERSE_NAME: "Reverse infer logic", REVERSE_DESC: "Toggle ON: Treat backlinks as children and forward links as parents.
    Toggle OFF: Treat backlinks as parents and forward links as children", + INVERSE_ARROW_DIRECTION_NAME: "Inverse arrow direction", + INVERSE_ARROW_DIRECTION_DESC: "Toggle ON: Display arrow heads in the opposite direction of the link direction.
    Toggle OFF: Display arrow heads in the same direction as the link direction", PARENTS_NAME: "Parents", CHILDREN_NAME: "Children", LEFT_FRIENDS_NAME: "Left-Side Friends", From 394f0870dc5b41d4c9f2b9846aeb6dde2e5d642f Mon Sep 17 00:00:00 2001 From: zsviczian Date: Mon, 31 Jul 2023 09:30:44 +0200 Subject: [PATCH 03/12] fixed excalidrawAvailable() --- src/excalibrain-main.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/excalibrain-main.ts b/src/excalibrain-main.ts index 15be89f..95bd242 100644 --- a/src/excalibrain-main.ts +++ b/src/excalibrain-main.ts @@ -22,6 +22,7 @@ declare module "obsidian" { interface App { plugins: { disablePlugin(plugin: string):Promise; + plugins: { [key: string]: Plugin; }; }; } interface WorkspaceLeaf { @@ -313,6 +314,10 @@ export default class ExcaliBrain extends Plugin { } private excalidrawAvailable():boolean { + if(this.app.plugins.plugins["obsidian-excalidraw-plugin"] === this.EA.plugin) { + return true; + } + const ea = getEA(this.scene?.leaf?.view); if(!ea) { this.EA = null; @@ -322,10 +327,8 @@ export default class ExcaliBrain extends Plugin { new Notice("ExcaliBrain: Please start Excalidraw and try again.",4000); return false; } - if(this.EA !== ea) { - this.EA = ea; - this.registerExcalidrawAutomateHooks() - } + this.EA = ea; + this.registerExcalidrawAutomateHooks() return true; } From 56735d4f7f96c6443332929f2b7a493a9b002b43 Mon Sep 17 00:00:00 2001 From: CarlB01 <125663371+CarlB01@users.noreply.github.com> Date: Mon, 31 Jul 2023 16:29:24 +0200 Subject: [PATCH 04/12] compacting There are others working on the same project, but - this fix makes a good generalised 'compact' without breaking any of the ather changes. Feel free to discard them, if they do not work in the longer run. (the more spesific layout handling the better). Mine is something in between, dealing with the group of parents, children etc. so maybe it is needed after all. As of 0.26 i think the display is nice. --- src/Scene.ts | 291 ++++++++++++++++++++++++++++++++++++---------- src/Types.ts | 1 + src/graph/Node.ts | 6 +- src/graph/Page.ts | 3 +- 4 files changed, 236 insertions(+), 65 deletions(-) diff --git a/src/Scene.ts b/src/Scene.ts index f6a8f82..aa1dfa2 100644 --- a/src/Scene.ts +++ b/src/Scene.ts @@ -128,6 +128,18 @@ export class Scene { } return centralPage; } + + /** iterates through a neighbour stack and returns the longest title length found. + * @description: Possibly time consuming - consider its use? + */ + private longestTitle(neighbours: Neighbour[], checkMax:number=10): number { + const lengths:number[] = [0]; + for (let index = 0; (index { @@ -365,6 +374,7 @@ export class Scene { if(n.page.path === this.ea.targetView.file.path) { return; } + n.page.maxLabelLength = x.layout.spec.maxLabelLength; const node = new Node({ ea: this.ea, page: n.page, @@ -480,7 +490,52 @@ export class Scene { this.layouts = []; const manyFriends = friends.length >= 10; const manyNextFriends = nextFriends.length >= 10; - const baseStyle = settings.baseNodeStyle; + const minLabelLength = 7; + + //needs to be in rendersection + const style = { + ...settings.baseNodeStyle, + ...settings.centralNodeStyle, + }; + const basestyle = settings.baseNodeStyle; + ea.style.fontFamily = basestyle.fontFamily; + ea.style.fontSize = basestyle.fontSize; + const baseChar4x = this.ea.measureText("mi3L".repeat(1)); + const baseChar = { + width: baseChar4x.width * 0.25, + height: baseChar4x.height + }; + this.nodeWidth = isCompactView + ? basestyle.maxLabelLength * baseChar.width + 2 * basestyle.padding + : this.textSize.width + 2 * style.padding; + + const compactFactor = isCompactView ? 1.4 : 2; + + this.nodeHeight = compactFactor * ( + isCompactView + ? baseChar.height + 2 * basestyle.padding + : this.textSize.height + 2 * style.padding + ); + const padding = 3 * basestyle.padding; + + const isCenterEmbedded = + settings.embedCentralNode && + !centralPage.isVirtual && + !centralPage.isFolder && + !centralPage.isTag; + const centerEmbedWidth = settings.centerEmbedWidth; + const centerEmbedHeight = settings.centerEmbedHeight; + + /** container */ + const container = this.leaf.view.containerEl; + const h = container.innerHeight-150; + const w = container.innerWidth; + const rf = 1/(h/w); + const rfCorr = Math.min(rf,1); + + const correctedMaxLabelLength = Math.round(style.maxLabelLength*rfCorr); + const correctedMinLabelLength = Math.max(minLabelLength, correctedMaxLabelLength); + const siblingsCols = siblings.length >= 20 ? 3 : siblings.length >= 10 @@ -501,113 +556,225 @@ export class Scene { ? [1, 1, 2, 3, 2][parents.length] : 3); + const friendCols = isCompactView && isCenterEmbedded + ? Math.ceil((friends.length*this.nodeHeight)/centerEmbedHeight) + : manyFriends + ? 2 + : 1; - const isCenterEmbedded = - settings.embedCentralNode && - !centralPage.isVirtual && - !centralPage.isFolder && - !centralPage.isTag; - const centerEmbedWidth = settings.centerEmbedWidth; - const centerEmbedHeight = settings.centerEmbedHeight; + const nextFriendCols = isCompactView && isCenterEmbedded + ? Math.ceil((nextFriends.length*this.nodeHeight)/centerEmbedHeight) + : manyNextFriends + ? 2 + : 1; + + //center + const rootTitle = centralPage.getTitle(); + const rootNode = ea.measureText(rootTitle.repeat(1)); + const actualRootLength = [...new Intl.Segmenter().segment(rootTitle)].length; + const rootNodeLength = isCompactView + ? Math.min(actualRootLength, style.maxLabelLength) + : style.maxLabelLength; + const rootWidth = isCompactView ? rootNode.width + 2 * style.padding : this.nodeWidth; + + const heightInCenter = isCenterEmbedded + ? centerEmbedHeight + 2 * this.nodeHeight + : 4 *this.nodeHeight; + //parents + const parentLabelLength = isCompactView + ? Math.min(this.longestTitle(parents), correctedMinLabelLength) + : style.maxLabelLength; + + const parentWidth = isCompactView + ? parentLabelLength * baseChar.width + padding + : this.nodeWidth; + + //children + const childLength = isCompactView + ? Math.min(this.longestTitle(children,20), correctedMinLabelLength) + : style.maxLabelLength; + + const childWidth = isCompactView + ? childLength * baseChar.width + padding + : this.nodeWidth; + + // friends + const friendLength = isCompactView + ? Math.min(this.longestTitle(friends),correctedMinLabelLength) + : style.maxLabelLength; + + const friendWidth = isCompactView + ? friendLength * baseChar.width + padding + : this.nodeWidth; + + // nextfriends + const nextFriendLength = isCompactView + ? Math.min(this.longestTitle(nextFriends), correctedMinLabelLength) + : style.maxLabelLength; + + const nextFriendWidth = isCompactView + ? nextFriendLength * baseChar.width + padding + : this.nodeWidth; + + //siblings + const siblingsStyle = settings.siblingNodeStyle; + const siblingsPadding = siblingsStyle.padding??settings.baseNodeStyle.padding; + const siblingsLabelLength = isCompactView + ? Math.min(this.longestTitle(siblings,20), correctedMinLabelLength) + : siblingsStyle.maxLabelLength??settings.baseNodeStyle.maxLabelLength; + ea.style.fontFamily = siblingsStyle.fontFamily; + ea.style.fontSize = siblingsStyle.fontSize; + const siblingsTextSize = ea.measureText("m".repeat(siblingsLabelLength+3)); + const siblingsNodeWidth = siblingsTextSize.width + 3 * siblingsPadding; + const siblingsNodeHeight = compactFactor * (siblingsTextSize.height + 2 * siblingsPadding); + + // layout areas + const friendsArea = { + width: friends.length>0? friendCols*friendWidth:0, + height: friends.length>0? Math.ceil(friends.length/friendCols)*this.nodeHeight:0 + } + const nextFriendsArea = { + width: nextFriends.length>0? nextFriendCols*nextFriendWidth:0, + height: nextFriends.length>0? Math.ceil(nextFriends.length/nextFriendCols)*this.nodeHeight:0 + } + const parentsArea = { + width: parents.length>0? parentCols*parentWidth:0, + height: parents.length>0? Math.ceil(parents.length/parentCols)*this.nodeHeight:0 + } + const childrenArea = { + width: children.length>0? childrenCols*childWidth:0, + height: children.length>0? Math.ceil(children.length/childrenCols)*this.nodeHeight:0 + } + const siblingsArea = { + width: siblings.length>0? siblingsNodeWidth*siblingsCols:0, + height: siblings.length>0? Math.ceil(siblings.length/siblingsCols)*siblingsNodeHeight:0 + } + + // Origos + const parentsOrigoY = isCompactView + ? (parentsArea.height + Math.max(friendsArea.height,nextFriendsArea.height,heightInCenter))*0.5 + padding + : (parentsArea.height + heightInCenter)*0.5; + + const childrenOrigoY = isCompactView + ? (childrenArea.height + Math.max(friendsArea.height,nextFriendsArea.height,heightInCenter))*0.5 + padding + : (childrenArea.height + heightInCenter)*0.5; + + const friendOrigoX = (isCompactView + ? Math.max( + (isCenterEmbedded?centerEmbedWidth:rootWidth) + friendsArea.width, + childrenArea.width-friendsArea.width, + parentsArea.width-friendsArea.width + ) + : Math.max( + centerEmbedWidth, + parentsArea.width, + childrenArea.width + ) + friendsArea.width + )/2 + (friendCols + 1) * padding; + + const nextFriendOrigoX = (isCompactView + ? Math.max( + (isCenterEmbedded?centerEmbedWidth:rootWidth) + nextFriendsArea.width, + childrenArea.width-nextFriendsArea.width, + parentsArea.width-nextFriendsArea.width + ) + : Math.max( + centerEmbedWidth, + parentsArea.width, + childrenArea.width + ) + nextFriendsArea.width + )/2 + (nextFriendCols + 1) * padding; + + const siblingsOrigoX = ( + Math.max( + parentsArea.width, + (isCenterEmbedded?centerEmbedWidth:rootWidth) + ) + siblingsArea.width)/2 + 3*siblingsPadding*(1 + siblingsCols); + + const siblingsOrigoY = (Math.max(parentsArea.height, siblingsArea.height) + nextFriendsArea.height) /2 + 2 * this.nodeHeight + + // layout const lCenter = new Layout({ origoX: 0, origoY: isCenterEmbedded - ? centerEmbedHeight - this.nodeHeight/2 + ? centerEmbedHeight/2 - this.nodeHeight/2 : 0, top: null, bottom: null, columns: 1, columnWidth: isCenterEmbedded ? centerEmbedWidth - : this.nodeWidth, + : rootWidth, rowHeight: isCenterEmbedded ? centerEmbedHeight - : this.nodeHeight, + : rootNode.height, + maxLabelLength: rootNodeLength }); this.layouts.push(lCenter); const lChildren = new Layout({ origoX: 0, - origoY: isCenterEmbedded - ? centerEmbedHeight + 1.5 * this.nodeHeight - : 2.5 * this.nodeHeight, - top: isCenterEmbedded - ? centerEmbedHeight + this.nodeHeight - : 2 * this.nodeHeight, + origoY: childrenOrigoY, + top: 0, bottom: null, columns: childrenCols, - columnWidth: this.nodeWidth, - rowHeight: this.nodeHeight + columnWidth: childWidth, + rowHeight: this.nodeHeight, + maxLabelLength: childLength }); this.layouts.push(lChildren); - - const friendOrigoX = isCompactView && isCenterEmbedded - ? centerEmbedWidth/2 + this.nodeWidth - : Math.max( - (((manyNextFriends?1:0)+Math.max(childrenCols,parentCols)+1.9)/2.4) * this.nodeWidth, // (manyChildren ? -3 : -2) * this.nodeWidth, - isCenterEmbedded - ? centerEmbedWidth/2 + this.nodeWidth - : 0 - ); - + const lFriends = new Layout({ origoX: -friendOrigoX, - origoY: isCenterEmbedded - ? centerEmbedHeight/2 - : 0, + origoY: 0, top: null, bottom: null, - columns: 1, - columnWidth: this.nodeWidth, - rowHeight: this.nodeHeight + columns: friendCols, + columnWidth: friendWidth, + rowHeight: this.nodeHeight, + maxLabelLength: friendLength }); this.layouts.push(lFriends); const lNextFriends = new Layout({ - origoX: friendOrigoX, - origoY: isCenterEmbedded - ? centerEmbedHeight/2 - : 0, + origoX: nextFriendOrigoX, + origoY: 0, top: null, bottom: null, - columns: 1, - columnWidth: this.nodeWidth, - rowHeight: this.nodeHeight + columns: nextFriendCols, + columnWidth: nextFriendWidth, + rowHeight: this.nodeHeight, + maxLabelLength: nextFriendLength }); this.layouts.push(lNextFriends); - + const lParents = new Layout({ - origoX: 0, - origoY: -2.5 * this.nodeHeight, + origoX:0, + origoY: - parentsOrigoY, top: null, bottom: -2 * this.nodeHeight, columns: parentCols, // 3, - columnWidth: this.nodeWidth, - rowHeight: this.nodeHeight + columnWidth: parentWidth, + rowHeight: this.nodeHeight, + maxLabelLength: parentLabelLength }); this.layouts.push(lParents); - - const siblingsStyle = settings.siblingNodeStyle; - const siblingsPadding = siblingsStyle.padding??baseStyle.padding; - const siblingsLabelLength = siblingsStyle.maxLabelLength??baseStyle.maxLabelLength; - ea.style.fontFamily = siblingsStyle.fontFamily; - ea.style.fontSize = siblingsStyle.fontSize; - const siblingsTextSize = ea.measureText("m".repeat(siblingsLabelLength+3)); - const siblingsNodeWidth = siblingsTextSize.width + 3 * siblingsPadding; - const siblingsNodeHeight = 2 * (siblingsTextSize.height + 2 * siblingsPadding); const lSiblings = new Layout({ - origoX: this.nodeWidth * ((parentCols-1)/2 + (siblingsCols+1.5)/3), - origoY: -2.5 * this.nodeHeight, + //origoX: this.nodeWidth * ((parentCols-1)/2 + (siblingsCols+1.5)/3), //orig + origoX: siblingsOrigoX, + origoY: - siblingsOrigoY, top: null, - bottom: - this.nodeHeight/2, + bottom: null, columns: siblingsCols, columnWidth: siblingsNodeWidth, rowHeight: siblingsNodeHeight, + maxLabelLength: siblingsLabelLength }) this.layouts.push(lSiblings); + centralPage.maxLabelLength = rootNodeLength; this.rootNode = new Node({ ea, page: centralPage, diff --git a/src/Types.ts b/src/Types.ts index 2cb3ee9..5253242 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -130,6 +130,7 @@ export type LayoutSpecification = { bottom: number; rowHeight: number; columnWidth: number; + maxLabelLength: number; } export type Dimensions = {width:number, height:number}; diff --git a/src/graph/Node.ts b/src/graph/Node.ts index 33f21ae..7e7632c 100644 --- a/src/graph/Node.ts +++ b/src/graph/Node.ts @@ -77,8 +77,10 @@ export class Node { private displayText(): string { const label = (this.style.prefix??"") + this.title; - return label.length > this.style.maxLabelLength - ? label.substring(0,this.style.maxLabelLength-1) + "..." + const segmentedLength = [...new Intl.Segmenter().segment(label)].length; //'Unicode-proof' https://stackoverflow.com/questions/54369513/how-to-count-the-correct-length-of-a-string-with-emojis-in-javascript + const lengthCorrection = label.length-segmentedLength; + return segmentedLength > this.page.maxLabelLength + ? label.substring(0,this.page.maxLabelLength+lengthCorrection-3) + "..." : label; } diff --git a/src/graph/Page.ts b/src/graph/Page.ts index 17035fd..acd8bfa 100644 --- a/src/graph/Page.ts +++ b/src/graph/Page.ts @@ -55,7 +55,8 @@ export class Page { public dvPage: Record; public primaryStyleTag: string; public dvIndexReady: boolean = false; - + public maxLabelLength: number; + constructor( private pages: Pages, public path:string, From 7a2bc6bd665e61dde0a03aee69d9fbd5c3c7e042 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Mon, 31 Jul 2023 19:00:25 +0200 Subject: [PATCH 05/12] Revert "Merge pull request #137 from CarlB01/master" This reverts commit f8ba9c3e67871d178e61cd444feda18d4100e222, reversing changes made to 394f0870dc5b41d4c9f2b9846aeb6dde2e5d642f. --- src/Scene.ts | 291 ++++++++++------------------------------------ src/Types.ts | 1 - src/graph/Node.ts | 6 +- src/graph/Page.ts | 3 +- 4 files changed, 65 insertions(+), 236 deletions(-) diff --git a/src/Scene.ts b/src/Scene.ts index aa1dfa2..f6a8f82 100644 --- a/src/Scene.ts +++ b/src/Scene.ts @@ -128,18 +128,6 @@ export class Scene { } return centralPage; } - - /** iterates through a neighbour stack and returns the longest title length found. - * @description: Possibly time consuming - consider its use? - */ - private longestTitle(neighbours: Neighbour[], checkMax:number=10): number { - const lengths:number[] = [0]; - for (let index = 0; (index { @@ -374,7 +365,6 @@ export class Scene { if(n.page.path === this.ea.targetView.file.path) { return; } - n.page.maxLabelLength = x.layout.spec.maxLabelLength; const node = new Node({ ea: this.ea, page: n.page, @@ -490,52 +480,7 @@ export class Scene { this.layouts = []; const manyFriends = friends.length >= 10; const manyNextFriends = nextFriends.length >= 10; - const minLabelLength = 7; - - //needs to be in rendersection - const style = { - ...settings.baseNodeStyle, - ...settings.centralNodeStyle, - }; - const basestyle = settings.baseNodeStyle; - ea.style.fontFamily = basestyle.fontFamily; - ea.style.fontSize = basestyle.fontSize; - const baseChar4x = this.ea.measureText("mi3L".repeat(1)); - const baseChar = { - width: baseChar4x.width * 0.25, - height: baseChar4x.height - }; - this.nodeWidth = isCompactView - ? basestyle.maxLabelLength * baseChar.width + 2 * basestyle.padding - : this.textSize.width + 2 * style.padding; - - const compactFactor = isCompactView ? 1.4 : 2; - - this.nodeHeight = compactFactor * ( - isCompactView - ? baseChar.height + 2 * basestyle.padding - : this.textSize.height + 2 * style.padding - ); - const padding = 3 * basestyle.padding; - - const isCenterEmbedded = - settings.embedCentralNode && - !centralPage.isVirtual && - !centralPage.isFolder && - !centralPage.isTag; - const centerEmbedWidth = settings.centerEmbedWidth; - const centerEmbedHeight = settings.centerEmbedHeight; - - /** container */ - const container = this.leaf.view.containerEl; - const h = container.innerHeight-150; - const w = container.innerWidth; - const rf = 1/(h/w); - const rfCorr = Math.min(rf,1); - - const correctedMaxLabelLength = Math.round(style.maxLabelLength*rfCorr); - const correctedMinLabelLength = Math.max(minLabelLength, correctedMaxLabelLength); - + const baseStyle = settings.baseNodeStyle; const siblingsCols = siblings.length >= 20 ? 3 : siblings.length >= 10 @@ -556,225 +501,113 @@ export class Scene { ? [1, 1, 2, 3, 2][parents.length] : 3); - const friendCols = isCompactView && isCenterEmbedded - ? Math.ceil((friends.length*this.nodeHeight)/centerEmbedHeight) - : manyFriends - ? 2 - : 1; - - const nextFriendCols = isCompactView && isCenterEmbedded - ? Math.ceil((nextFriends.length*this.nodeHeight)/centerEmbedHeight) - : manyNextFriends - ? 2 - : 1; - - //center - const rootTitle = centralPage.getTitle(); - const rootNode = ea.measureText(rootTitle.repeat(1)); - const actualRootLength = [...new Intl.Segmenter().segment(rootTitle)].length; - const rootNodeLength = isCompactView - ? Math.min(actualRootLength, style.maxLabelLength) - : style.maxLabelLength; - const rootWidth = isCompactView ? rootNode.width + 2 * style.padding : this.nodeWidth; - - const heightInCenter = isCenterEmbedded - ? centerEmbedHeight + 2 * this.nodeHeight - : 4 *this.nodeHeight; - - //parents - const parentLabelLength = isCompactView - ? Math.min(this.longestTitle(parents), correctedMinLabelLength) - : style.maxLabelLength; - - const parentWidth = isCompactView - ? parentLabelLength * baseChar.width + padding - : this.nodeWidth; - - //children - const childLength = isCompactView - ? Math.min(this.longestTitle(children,20), correctedMinLabelLength) - : style.maxLabelLength; - - const childWidth = isCompactView - ? childLength * baseChar.width + padding - : this.nodeWidth; - - // friends - const friendLength = isCompactView - ? Math.min(this.longestTitle(friends),correctedMinLabelLength) - : style.maxLabelLength; - - const friendWidth = isCompactView - ? friendLength * baseChar.width + padding - : this.nodeWidth; - - // nextfriends - const nextFriendLength = isCompactView - ? Math.min(this.longestTitle(nextFriends), correctedMinLabelLength) - : style.maxLabelLength; - - const nextFriendWidth = isCompactView - ? nextFriendLength * baseChar.width + padding - : this.nodeWidth; - - //siblings - const siblingsStyle = settings.siblingNodeStyle; - const siblingsPadding = siblingsStyle.padding??settings.baseNodeStyle.padding; - const siblingsLabelLength = isCompactView - ? Math.min(this.longestTitle(siblings,20), correctedMinLabelLength) - : siblingsStyle.maxLabelLength??settings.baseNodeStyle.maxLabelLength; - ea.style.fontFamily = siblingsStyle.fontFamily; - ea.style.fontSize = siblingsStyle.fontSize; - const siblingsTextSize = ea.measureText("m".repeat(siblingsLabelLength+3)); - const siblingsNodeWidth = siblingsTextSize.width + 3 * siblingsPadding; - const siblingsNodeHeight = compactFactor * (siblingsTextSize.height + 2 * siblingsPadding); - // layout areas - const friendsArea = { - width: friends.length>0? friendCols*friendWidth:0, - height: friends.length>0? Math.ceil(friends.length/friendCols)*this.nodeHeight:0 - } - const nextFriendsArea = { - width: nextFriends.length>0? nextFriendCols*nextFriendWidth:0, - height: nextFriends.length>0? Math.ceil(nextFriends.length/nextFriendCols)*this.nodeHeight:0 - } - const parentsArea = { - width: parents.length>0? parentCols*parentWidth:0, - height: parents.length>0? Math.ceil(parents.length/parentCols)*this.nodeHeight:0 - } - const childrenArea = { - width: children.length>0? childrenCols*childWidth:0, - height: children.length>0? Math.ceil(children.length/childrenCols)*this.nodeHeight:0 - } - const siblingsArea = { - width: siblings.length>0? siblingsNodeWidth*siblingsCols:0, - height: siblings.length>0? Math.ceil(siblings.length/siblingsCols)*siblingsNodeHeight:0 - } - - // Origos - const parentsOrigoY = isCompactView - ? (parentsArea.height + Math.max(friendsArea.height,nextFriendsArea.height,heightInCenter))*0.5 + padding - : (parentsArea.height + heightInCenter)*0.5; - - const childrenOrigoY = isCompactView - ? (childrenArea.height + Math.max(friendsArea.height,nextFriendsArea.height,heightInCenter))*0.5 + padding - : (childrenArea.height + heightInCenter)*0.5; - - const friendOrigoX = (isCompactView - ? Math.max( - (isCenterEmbedded?centerEmbedWidth:rootWidth) + friendsArea.width, - childrenArea.width-friendsArea.width, - parentsArea.width-friendsArea.width - ) - : Math.max( - centerEmbedWidth, - parentsArea.width, - childrenArea.width - ) + friendsArea.width - )/2 + (friendCols + 1) * padding; - - const nextFriendOrigoX = (isCompactView - ? Math.max( - (isCenterEmbedded?centerEmbedWidth:rootWidth) + nextFriendsArea.width, - childrenArea.width-nextFriendsArea.width, - parentsArea.width-nextFriendsArea.width - ) - : Math.max( - centerEmbedWidth, - parentsArea.width, - childrenArea.width - ) + nextFriendsArea.width - )/2 + (nextFriendCols + 1) * padding; + const isCenterEmbedded = + settings.embedCentralNode && + !centralPage.isVirtual && + !centralPage.isFolder && + !centralPage.isTag; + const centerEmbedWidth = settings.centerEmbedWidth; + const centerEmbedHeight = settings.centerEmbedHeight; - const siblingsOrigoX = ( - Math.max( - parentsArea.width, - (isCenterEmbedded?centerEmbedWidth:rootWidth) - ) + siblingsArea.width)/2 + 3*siblingsPadding*(1 + siblingsCols); - - const siblingsOrigoY = (Math.max(parentsArea.height, siblingsArea.height) + nextFriendsArea.height) /2 + 2 * this.nodeHeight - - // layout const lCenter = new Layout({ origoX: 0, origoY: isCenterEmbedded - ? centerEmbedHeight/2 - this.nodeHeight/2 + ? centerEmbedHeight - this.nodeHeight/2 : 0, top: null, bottom: null, columns: 1, columnWidth: isCenterEmbedded ? centerEmbedWidth - : rootWidth, + : this.nodeWidth, rowHeight: isCenterEmbedded ? centerEmbedHeight - : rootNode.height, - maxLabelLength: rootNodeLength + : this.nodeHeight, }); this.layouts.push(lCenter); const lChildren = new Layout({ origoX: 0, - origoY: childrenOrigoY, - top: 0, + origoY: isCenterEmbedded + ? centerEmbedHeight + 1.5 * this.nodeHeight + : 2.5 * this.nodeHeight, + top: isCenterEmbedded + ? centerEmbedHeight + this.nodeHeight + : 2 * this.nodeHeight, bottom: null, columns: childrenCols, - columnWidth: childWidth, - rowHeight: this.nodeHeight, - maxLabelLength: childLength + columnWidth: this.nodeWidth, + rowHeight: this.nodeHeight }); this.layouts.push(lChildren); - + + const friendOrigoX = isCompactView && isCenterEmbedded + ? centerEmbedWidth/2 + this.nodeWidth + : Math.max( + (((manyNextFriends?1:0)+Math.max(childrenCols,parentCols)+1.9)/2.4) * this.nodeWidth, // (manyChildren ? -3 : -2) * this.nodeWidth, + isCenterEmbedded + ? centerEmbedWidth/2 + this.nodeWidth + : 0 + ); + const lFriends = new Layout({ origoX: -friendOrigoX, - origoY: 0, + origoY: isCenterEmbedded + ? centerEmbedHeight/2 + : 0, top: null, bottom: null, - columns: friendCols, - columnWidth: friendWidth, - rowHeight: this.nodeHeight, - maxLabelLength: friendLength + columns: 1, + columnWidth: this.nodeWidth, + rowHeight: this.nodeHeight }); this.layouts.push(lFriends); const lNextFriends = new Layout({ - origoX: nextFriendOrigoX, - origoY: 0, + origoX: friendOrigoX, + origoY: isCenterEmbedded + ? centerEmbedHeight/2 + : 0, top: null, bottom: null, - columns: nextFriendCols, - columnWidth: nextFriendWidth, - rowHeight: this.nodeHeight, - maxLabelLength: nextFriendLength + columns: 1, + columnWidth: this.nodeWidth, + rowHeight: this.nodeHeight }); this.layouts.push(lNextFriends); - + const lParents = new Layout({ - origoX:0, - origoY: - parentsOrigoY, + origoX: 0, + origoY: -2.5 * this.nodeHeight, top: null, bottom: -2 * this.nodeHeight, columns: parentCols, // 3, - columnWidth: parentWidth, - rowHeight: this.nodeHeight, - maxLabelLength: parentLabelLength + columnWidth: this.nodeWidth, + rowHeight: this.nodeHeight }); this.layouts.push(lParents); + + const siblingsStyle = settings.siblingNodeStyle; + const siblingsPadding = siblingsStyle.padding??baseStyle.padding; + const siblingsLabelLength = siblingsStyle.maxLabelLength??baseStyle.maxLabelLength; + ea.style.fontFamily = siblingsStyle.fontFamily; + ea.style.fontSize = siblingsStyle.fontSize; + const siblingsTextSize = ea.measureText("m".repeat(siblingsLabelLength+3)); + const siblingsNodeWidth = siblingsTextSize.width + 3 * siblingsPadding; + const siblingsNodeHeight = 2 * (siblingsTextSize.height + 2 * siblingsPadding); const lSiblings = new Layout({ - //origoX: this.nodeWidth * ((parentCols-1)/2 + (siblingsCols+1.5)/3), //orig - origoX: siblingsOrigoX, - origoY: - siblingsOrigoY, + origoX: this.nodeWidth * ((parentCols-1)/2 + (siblingsCols+1.5)/3), + origoY: -2.5 * this.nodeHeight, top: null, - bottom: null, + bottom: - this.nodeHeight/2, columns: siblingsCols, columnWidth: siblingsNodeWidth, rowHeight: siblingsNodeHeight, - maxLabelLength: siblingsLabelLength }) this.layouts.push(lSiblings); - centralPage.maxLabelLength = rootNodeLength; this.rootNode = new Node({ ea, page: centralPage, diff --git a/src/Types.ts b/src/Types.ts index 5253242..2cb3ee9 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -130,7 +130,6 @@ export type LayoutSpecification = { bottom: number; rowHeight: number; columnWidth: number; - maxLabelLength: number; } export type Dimensions = {width:number, height:number}; diff --git a/src/graph/Node.ts b/src/graph/Node.ts index 7e7632c..33f21ae 100644 --- a/src/graph/Node.ts +++ b/src/graph/Node.ts @@ -77,10 +77,8 @@ export class Node { private displayText(): string { const label = (this.style.prefix??"") + this.title; - const segmentedLength = [...new Intl.Segmenter().segment(label)].length; //'Unicode-proof' https://stackoverflow.com/questions/54369513/how-to-count-the-correct-length-of-a-string-with-emojis-in-javascript - const lengthCorrection = label.length-segmentedLength; - return segmentedLength > this.page.maxLabelLength - ? label.substring(0,this.page.maxLabelLength+lengthCorrection-3) + "..." + return label.length > this.style.maxLabelLength + ? label.substring(0,this.style.maxLabelLength-1) + "..." : label; } diff --git a/src/graph/Page.ts b/src/graph/Page.ts index acd8bfa..17035fd 100644 --- a/src/graph/Page.ts +++ b/src/graph/Page.ts @@ -55,8 +55,7 @@ export class Page { public dvPage: Record; public primaryStyleTag: string; public dvIndexReady: boolean = false; - public maxLabelLength: number; - + constructor( private pages: Pages, public path:string, From 345dab5c080af1661b8b0d4e2c4b5d6b1716fe16 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Mon, 31 Jul 2023 19:14:14 +0200 Subject: [PATCH 06/12] Revert "Merge branch 'master' of https://github.com/cycsd/excalibrain" This reverts commit c70bc7b2a05982180bd4e0087763487ceb704bca, reversing changes made to 509d8b356fce18cbfd92374858e6bafe32e25dc5. --- manifest.json | 1 - src/Components/ToolsPanel.ts | 8 +++--- src/Scene.ts | 26 ++++++----------- src/Settings.ts | 44 +++++++++-------------------- src/excalibrain-main.ts | 54 ++++++++---------------------------- src/graph/Node.ts | 27 ++++++------------ src/graph/Page.ts | 25 ++++------------- src/graph/Pages.ts | 33 ++++++++-------------- src/lang/locale/en.ts | 5 +--- src/utils/ParseURLs.ts | 23 +++++++++++++++ styles.css | 4 +-- 11 files changed, 91 insertions(+), 159 deletions(-) create mode 100644 src/utils/ParseURLs.ts diff --git a/manifest.json b/manifest.json index 868b6f3..51dd371 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,5 @@ "description": "A clean, intuitive and editable graph view for Obsidian", "author": "Zsolt Viczian", "authorUrl": "https://zsolt.blog", - "fundingUrl": "https://ko-fi.com/zsolt", "isDesktopOnly": false } diff --git a/src/Components/ToolsPanel.ts b/src/Components/ToolsPanel.ts index 7da69f8..7bd06be 100644 --- a/src/Components/ToolsPanel.ts +++ b/src/Components/ToolsPanel.ts @@ -234,11 +234,11 @@ export class ToolsPanel { //------------ // Render weblinks in page //------------ - this.buttons.push( + /*this.buttons.push( new ToggleButton( this.plugin, - ()=>this.plugin.settings.showURLNodes, - (val:boolean)=>this.plugin.settings.showURLNodes = val, + ()=>this.plugin.settings.showURLs, + (val:boolean)=>this.plugin.settings.showURLs = val, buttonsWrapperDiv, { display: "🌐", @@ -247,7 +247,7 @@ export class ToolsPanel { }, false ) - ) + )*/ //------------ //Display siblings diff --git a/src/Scene.ts b/src/Scene.ts index f6a8f82..17eb756 100644 --- a/src/Scene.ts +++ b/src/Scene.ts @@ -147,7 +147,7 @@ export class Scene { return; } - const isFile = !(page.isFolder || page.isTag || page.isVirtual || page.isURL); + const isFile = !(page.isFolder || page.isTag || page.isVirtual); if(isFile && !page.file) { this.blockUpdateTimer = false; @@ -169,12 +169,10 @@ export class Scene { keepOnTop(this.plugin.EA); const centralPage = this.getCentralPage(); - const isSameFileAsCurrent = centralPage && - ((isFile && centralPage.file === page.file) || - (page.isURL && centralPage.isURL && centralPage.url === page.url)) + const isSameFileAsCurrent = centralPage && isFile && centralPage.file === page.file // if the file hasn't changed don't update the graph - if(isSameFileAsCurrent && (page.isURL || (page.file.stat.mtime === centralPage.mtime))) { + if(isSameFileAsCurrent && page.file.stat.mtime === centralPage.mtime) { this.blockUpdateTimer = false; return; //don't reload the file if it has not changed } @@ -182,7 +180,7 @@ export class Scene { if(isFile && shouldOpenFile && !settings.embedCentralNode) { const centralLeaf = this.getCentralLeaf(); //@ts-ignore - if(!centralLeaf || !this.app.workspace.getLeafById(centralLeaf.id)) { + if(!centralLeaf || !app.workspace.getLeafById(centralLeaf.id)) { this.centralLeaf = this.ea.openFileInNewOrAdjacentLeaf(page.file); } else { centralLeaf.openFile(page.file, {active: false}); @@ -197,11 +195,6 @@ export class Scene { this.toolsPanel.rerender(); } - if(page.isURL && !settings.showURLNodes) { - settings.showURLNodes = true; - this.toolsPanel.rerender(); - } - if(page.isTag && !settings.showTagNodes) { settings.showTagNodes = true; this.toolsPanel.rerender(); @@ -285,7 +278,6 @@ export class Scene { ...settings.baseNodeStyle, ...settings.centralNodeStyle, }; - style.textColor = settings.baseNodeStyle.textColor; let counter = 0; ea.clear(); @@ -342,7 +334,7 @@ export class Scene { } const frame3 = async () => { if(this.plugin.settings.allowAutozoom) { - setTimeout(()=>api.zoomToFit(null, this.plugin.settings.maxZoom, 0.15),100); + api.zoomToFit(null, 5, 0.15); } ea.targetView.linksAlwaysOpenInANewPane = true; await this.addEventHandler(); @@ -403,7 +395,7 @@ export class Scene { const ea = this.ea; retainCentralNode = retainCentralNode && Boolean(this.rootNode) && - settings.embedCentralNode && ((centralPage.file && isEmbedFileType(centralPage.file,ea)) || centralPage.isURL); + settings.embedCentralNode && isEmbedFileType(centralPage.file,ea); this.zoomToFitOnNextBrainLeafActivate = !ea.targetView.containerEl.isShown(); @@ -717,8 +709,8 @@ export class Scene { ea.targetView.clearDirty(); //hack to prevent excalidraw from saving the changes excalidrawAPI.updateScene({appState: {viewBackgroundColor: settings.backgroundColor}}); - if(settings.allowAutozoom && !retainCentralNode) { - setTimeout(()=>excalidrawAPI.zoomToFit(ea.getViewElements(),settings.maxZoom,0.15),100); + if(settings.allowAutozoom) { + setTimeout(()=>excalidrawAPI.zoomToFit(ea.getViewElements(),5,0.15)); } this.toolsPanel.rerender(); @@ -786,7 +778,7 @@ export class Scene { if(this.zoomToFitOnNextBrainLeafActivate) { this.zoomToFitOnNextBrainLeafActivate = false; if(settings.allowAutozoom) { - this.ea.getExcalidrawAPI().zoomToFit(null, settings.maxZoom, 0.15); + this.ea.getExcalidrawAPI().zoomToFit(null, 5, 0.15); } } this.blockUpdateTimer = false; diff --git a/src/Settings.ts b/src/Settings.ts index 0e0c270..3e03258 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -34,7 +34,7 @@ export interface ExcaliBrainSettings { excludeFilepaths: string[]; showInferredNodes: boolean; showAttachments: boolean; - showURLNodes: boolean; + showURLs: boolean; showVirtualNodes: boolean; showFolderNodes: boolean; showTagNodes: boolean; @@ -46,7 +46,6 @@ export interface ExcaliBrainSettings { baseNodeStyle: NodeStyle; centralNodeStyle: NodeStyle; inferredNodeStyle: NodeStyle; - urlNodeStyle: NodeStyle; virtualNodeStyle: NodeStyle; siblingNodeStyle: NodeStyle; attachmentNodeStyle: NodeStyle; @@ -71,7 +70,6 @@ export interface ExcaliBrainSettings { ontologySuggesterMidSentenceTrigger: string; boldFields: boolean; allowAutozoom: boolean; - maxZoom: number; allowAutofocuOnSearch: boolean; defaultAlwaysOnTop: boolean; embedCentralNode: boolean; @@ -93,7 +91,7 @@ export const DEFAULT_SETTINGS: ExcaliBrainSettings = { excludeFilepaths: [], showInferredNodes: true, showAttachments: true, - showURLNodes: true, + showURLs: true, showVirtualNodes: true, showFolderNodes: false, showTagNodes: false, @@ -105,16 +103,13 @@ export const DEFAULT_SETTINGS: ExcaliBrainSettings = { baseNodeStyle: DEFAULT_NODE_STYLE, centralNodeStyle: { fontSize: 30, - backgroundColor: "#B5B5B5", + backgroundColor: "#C49A13FF", textColor: "#000000ff", }, inferredNodeStyle: { backgroundColor: "#000005b3", textColor: "#95c7f3ff", }, - urlNodeStyle: { - prefix: "🌐 " - }, virtualNodeStyle: { backgroundColor: "#ff000066", fillStyle: "hachure", @@ -163,7 +158,6 @@ export const DEFAULT_SETTINGS: ExcaliBrainSettings = { ontologySuggesterMidSentenceTrigger: "(", boldFields: false, allowAutozoom: true, - maxZoom: 1, allowAutofocuOnSearch: true, defaultAlwaysOnTop: false, embedCentralNode: false, @@ -1852,28 +1846,16 @@ export class ExcaliBrainSettingTab extends PluginSettingTab { this.dirty = true; })) - this.numberslider( - containerEl, - t("MAX_AUTOZOOM_NAME"), - t("MAX_AUTOZOOM_DESC"), - {min:10,max:1000, step:10}, - ()=>this.plugin.settings.maxZoom*100, - (val)=>this.plugin.settings.maxZoom = val/100, - ()=>{}, - false, - 100 - ) - - new Setting(containerEl) - .setName(t("ALLOW_AUTOFOCUS_ON_SEARCH_NAME")) - .setDesc(fragWithHTML(t("ALLOW_AUTOFOCUS_ON_SEARCH_DESC"))) - .addToggle(toggle => - toggle - .setValue(this.plugin.settings.allowAutofocuOnSearch) - .onChange(value => { - this.plugin.settings.allowAutofocuOnSearch = value; - this.dirty = true; - })) + new Setting(containerEl) + .setName(t("ALLOW_AUTOFOCUS_ON_SEARCH_NAME")) + .setDesc(fragWithHTML(t("ALLOW_AUTOFOCUS_ON_SEARCH_DESC"))) + .addToggle(toggle => + toggle + .setValue(this.plugin.settings.allowAutofocuOnSearch) + .onChange(value => { + this.plugin.settings.allowAutofocuOnSearch = value; + this.dirty = true; + })) new Setting(containerEl) .setName(t("ALWAYS_ON_TOP_NAME")) diff --git a/src/excalibrain-main.ts b/src/excalibrain-main.ts index 95bd242..bf6ea39 100644 --- a/src/excalibrain-main.ts +++ b/src/excalibrain-main.ts @@ -83,11 +83,10 @@ export default class ExcaliBrain extends Plugin { this.registerEvents(); this.urlParser = new URLParser(this); this.app.workspace.onLayoutReady(()=>{ - this.urlParser.init(); this.DVAPI = getAPI(); if(!this.DVAPI) { (new WarningPrompt( - this.app, + app, "⚠ ExcaliBrain Disabled: DataView Plugin not found", t("DATAVIEW_NOT_FOUND")) ).show(async (result: boolean) => { @@ -99,7 +98,7 @@ export default class ExcaliBrain extends Plugin { } if(!this.DVAPI.version.compare('>=', '0.5.31')) { (new WarningPrompt( - this.app, + app, "⚠ ExcaliBrain Disabled: Dataview upgrade requried", t("DATAVIEW_UPGRADE")) ).show(async (result: boolean) => { @@ -202,19 +201,6 @@ export default class ExcaliBrain extends Plugin { await sleep(100); } - counter = 0; - while(!this.urlParser.initalized) { - if(counter++ % 100 === 10) { - new Notice("ExcaliBrain is waiting for URLParser to finish indexing",1000); - } - await sleep(100); - } - - //Add all host urls - this.urlParser.hosts.forEach((url)=>{ - this.pages.add(url, new Page(this.pages, url, null, this, false, false, url, url)); - }); - //Add all folders and files const addFolderChildren = (parentFolder: TFolder, parent: Page) => { const children = parentFolder.children; @@ -270,17 +256,13 @@ export default class ExcaliBrain extends Plugin { //Add all links as inferred children to pages on which they were found this.pages.addResolvedLinks(); - //Add all urls as inferred children to pages on which they were found - //and inferred children of their origins - this.pages.addPageURLs(); - const self = this; setTimeout(async()=>{ //@ts-ignore - const bookmarksPlugin = this.app.internalPlugins.getPluginById("bookmarks"); + const bookmarksPlugin = app.internalPlugins.getPluginById("bookmarks"); if(!bookmarksPlugin) { //code to be removed when bookmarks plugin is released, only leave return //@ts-ignore - const starredPlugin = this.app.internalPlugins.getPluginById("starred"); + const starredPlugin = app.internalPlugins.getPluginById("starred"); if(!starredPlugin) { return; } @@ -580,7 +562,7 @@ export default class ExcaliBrain extends Plugin { } this.EA.onLinkClickHook = (element,linkText,event) => { - const path = linkText.match(/\[\[([^\]]*)/)?.[1] ?? linkText.match(/(http.*)/)?.[1]; + const path = linkText.match(/\[\[([^\]]*)/)?.[1]; if(!path) return true; const page = this.pages.get(path); const ea = this.EA; @@ -613,20 +595,15 @@ export default class ExcaliBrain extends Plugin { return false; } - //if centralPage is in embeddedFrame + //if centralPage is in embeddedFrame, simply render the scene if(this.settings.embedCentralNode) { - //the user clicked the link handle in the top left, then open the file in a leaf if(this.scene.centralPagePath === page.path) { - if(page.isURL) { - return true; //let Excalidraw open the webpage - } else { - if(this.scene.isCentralLeafStillThere()) { - this.scene.centralLeaf.openFile(page.file,{active:true}); - return false; - } - ea.targetView.linksAlwaysOpenInANewPane = false; - setTimeout(()=>ea.targetView.linksAlwaysOpenInANewPane = true,300); + if(this.scene.isCentralLeafStillThere()) { + this.scene.centralLeaf.openFile(page.file,{active:true}); + return false; } + ea.targetView.linksAlwaysOpenInANewPane = false; + setTimeout(()=>ea.targetView.linksAlwaysOpenInANewPane = true,300); return true; } this.scene.renderGraphForPath(path); @@ -635,7 +612,7 @@ export default class ExcaliBrain extends Plugin { const centralLeaf = this.scene.getCentralLeaf(); //handle click on link to existing file - if(!page.isFolder && !page.isTag && !page.isURL) { + if(!page.isFolder && !page.isTag) { //if the leaf attached to ExcaliBrain already has the new file open, render the associated graph if((centralLeaf?.view as TextFileView)?.file?.path === path) { this.scene.renderGraphForPath(path); @@ -844,13 +821,6 @@ export default class ExcaliBrain extends Plugin { display: t("NODESTYLE_INFERRED"), getInheritedStyle: ()=> this.settings.baseNodeStyle }; - this.nodeStyles["url"] = { - style: this. settings.urlNodeStyle, - allowOverride: true, - userStyle: false, - display: t("NODESTYLE_URL"), - getInheritedStyle: ()=> this.settings.baseNodeStyle - }, this.nodeStyles["virtual"] = { style: this.settings.virtualNodeStyle, allowOverride: true, diff --git a/src/graph/Node.ts b/src/graph/Node.ts index 33f21ae..dd76ea8 100644 --- a/src/graph/Node.ts +++ b/src/graph/Node.ts @@ -5,7 +5,6 @@ import { getTagStyle } from "src/utils/dataview"; import { Page } from "./Page"; import { ExcalidrawImageElement } from "@zsviczian/excalidraw/types/element/types"; import { isEmbedFileType } from "src/utils/fileUtils"; -import { getEmbeddableDimensions } from "src/utils/embeddableHelper"; export class Node { page: Page; @@ -60,7 +59,6 @@ export class Node { this.style = { ...this.settings.baseNodeStyle, ...x.isInferred?this.settings.inferredNodeStyle:{}, - ...x.page.isURL?this.settings.urlNodeStyle:{}, ...x.page.isVirtual?this.settings.virtualNodeStyle:{}, ...x.isCentral?this.settings.centralNodeStyle:{}, ...x.isSibling?this.settings.siblingNodeStyle:{}, @@ -104,25 +102,20 @@ export class Node { async renderEmbedded():Promise { const ea = this.ea; - let maxDimensions = {width: this.style.embedWidth, height: this.style.embedHeight}; - if((this.page.file && isEmbedFileType(this.page.file, ea)) || this.page.isURL) { - if(this.page.isURL) { - maxDimensions = getEmbeddableDimensions(this.page.url, maxDimensions); - } + const maxDimensions = {width: this.style.embedWidth, height: this.style.embedHeight}; + if(isEmbedFileType(this.page.file, ea)) { this.id = ea.addEmbeddable( this.center.x - maxDimensions.width/2, this.center.y - maxDimensions.height/2, maxDimensions.width, maxDimensions.height, - this.page.isURL ? this.page.url : undefined, - this.page.isURL ? undefined : this.page.file + undefined, + this.page.file ); - const embeddable = ea.getElement(this.id) as any; - //overriding the default link with the full filepath - embeddable.link = this.page.isURL ? this.page.url : `[[${this.page.file.path}]]`; - embeddable.backgroundColor = this.style.backgroundColor; - embeddable.strokeColor = this.style.borderColor; - embeddable.strokeStyle = this.style.strokeStyle; + const box = ea.getElement(this.id) as any; + box.backgroundColor = this.style.backgroundColor; + box.strokeColor = this.style.borderColor; + box.strokeStyle = this.style.strokeStyle; this.embeddedElementIds.push(this.id); return maxDimensions; } else { @@ -134,8 +127,6 @@ export class Node { false, ) const imgEl = ea.getElement(this.id) as Mutable; - //overriding the default link with the full filepath - imgEl.link = `[[${this.page.file.path}]]`; let width = imgEl.width; let height = imgEl.height; @@ -194,7 +185,7 @@ export class Node { } ); const box = ea.getElement(this.id) as any; - box.link = this.page.isURL ? this.page.url : `[[${this.page.file?.path??this.page.path}]]`; + box.link = `[[${this.page.file?.path??this.page.path}]]`; box.backgroundColor = this.style.backgroundColor; box.strokeColor = this.style.borderColor; box.strokeStyle = this.style.strokeStyle; diff --git a/src/graph/Page.ts b/src/graph/Page.ts index 17035fd..1e32c14 100644 --- a/src/graph/Page.ts +++ b/src/graph/Page.ts @@ -63,15 +63,14 @@ export class Page { public plugin: ExcaliBrain, public isFolder: boolean=false, public isTag: boolean=false, - public name?: string, - public url:string = null + public name?: string ) { if(!name) { this.name = file ? (file.extension === "md") ? file.basename : file.name - : Boolean(url) ? url : getFilenameFromPath(path); + : getFilenameFromPath(path); } this.mtime = file ? file.stat.mtime : null; this.neighbours = new Map(); @@ -195,12 +194,6 @@ export class Page { } public getTitle(): string { - if(this.isURL) { - if(this.plugin.settings.renderAlias && this.name && this.name !== "") { - return this.name; - } - return this.url; - } const aliases = (this.file && this.plugin.settings.renderAlias) ? (this.dvPage?.file?.aliases?.values??[]) : []; @@ -260,23 +253,18 @@ export class Page { this.addDVFieldLinksToPage(); this.neighbours.forEach(n=>n.target.addDVFieldLinksToPage()); - const { showVirtualNodes, showAttachments, showFolderNodes, showTagNodes, showPageNodes, showURLNodes } = this.plugin.settings + const { showVirtualNodes, showAttachments, showFolderNodes, showTagNodes, showPageNodes } = this.plugin.settings return Array.from(this.neighbours) .filter(x=> (showVirtualNodes || !x[1].target.isVirtual) && (showAttachments || !x[1].target.isAttachment) && (showFolderNodes || !x[1].target.isFolder) && (showTagNodes || !x[1].target.isTag) && - (showPageNodes || x[1].target.isFolder || x[1].target.isTag || x[1].target.isAttachment || x[1].target.isURL) && - (showURLNodes || !x[1].target.isURL) - ) + (showPageNodes || x[1].target.isFolder || x[1].target.isTag || x[1].target.isAttachment) + ) } public get isVirtual(): boolean { - return (this.file === null) && !this.isFolder && !this.isTag && !this.isURL; - } - - public get isURL(): boolean { - return Boolean(this.url); + return (this.file === null) && !this.isFolder && !this.isTag; } public get isAttachment(): boolean { @@ -451,7 +439,6 @@ export class Page { : null; }; - childrenCount():number { return this.getNeighbours() .reduce((prev,x) => { diff --git a/src/graph/Pages.ts b/src/graph/Pages.ts index ff1eadd..a2de8cc 100644 --- a/src/graph/Pages.ts +++ b/src/graph/Pages.ts @@ -77,29 +77,20 @@ export class Pages { //path case sensitivity issue child = this.pages.get(this.plugin.lowercasePathMap.get(childPath.toLowerCase())); } - this.addInferredParentChild(parent,child); - }) - }); - } - - public addPageURLs() { - this.plugin.urlParser.fileToUrlMap.forEach((value, key)=>{ - const page=this.get(key.path); - if(!page) return; - value.forEach(url=>{ - let urlPage = this.get(url.url); - if(!urlPage) { - urlPage = new Page(this,url.url,null,this.plugin,false,false,url.alias,url.url); - this.add(url.url, urlPage); - } - this.addInferredParentChild(page,urlPage); - const originPage = this.get(url.origin); - if(originPage) { - urlPage.addParent(originPage,RelationType.INFERRED,LinkDirection.FROM); - originPage.addChild(urlPage,RelationType.INFERRED,LinkDirection.TO); + if(this.plugin.settings.inferAllLinksAsFriends) { + child.addLeftFriend(parent,RelationType.INFERRED, LinkDirection.FROM); + parent.addLeftFriend(child,RelationType.INFERRED, LinkDirection.TO); + } else { + if(this.plugin.settings.inverseInfer) { //https://github.com/zsviczian/excalibrain/issues/78 + child.addChild(parent,RelationType.INFERRED, LinkDirection.FROM); + parent.addParent(child,RelationType.INFERRED, LinkDirection.TO); + } else { + child.addParent(parent,RelationType.INFERRED, LinkDirection.FROM); + parent.addChild(child,RelationType.INFERRED, LinkDirection.TO); + } } }) - }) + }); } /** diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 4c83aeb..9117e89 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -101,8 +101,6 @@ export default { SHOW_COUNT_DESC: "Show the number of children, parents, friends next to the node gate", ALLOW_AUTOZOOM_NAME: "Autozoom", ALLOW_AUTOZOOM_DESC: "Toggle ON: Allow autozoom
    Toggle OFF: Disable autozoom", - MAX_AUTOZOOM_NAME: "Maximum autozoom level [%]", - MAX_AUTOZOOM_DESC: "Maximum zoom level to apply when autozoom is enabled. The higher the number the more zoomed in the graph will be.", ALLOW_AUTOFOCUS_ON_SEARCH_NAME: "Autofocus on search", ALLOW_AUTOFOCUS_ON_SEARCH_DESC: "Toggle ON: Allow autofocus on Search
    Toggle OFF: Disable autofocus", ALWAYS_ON_TOP_NAME: "Popout default 'always on top' behavior", @@ -144,7 +142,6 @@ export default { NODESTYLE_BASE: "Base node style", NODESTYLE_CENTRAL: "Style of central node", NODESTYLE_INFERRED: "Style of inferred nodes", - NODESTYLE_URL: "Style of web page nodes", NODESTYLE_VIRTUAL: "Style of virtual nodes", NODESTYLE_SIBLING: "Style of sibling nodes", NODESTYLE_ATTACHMENT: "Style of attachment nodes", @@ -186,7 +183,7 @@ export default { SEARCH_IN_VAULT: "Starred items will be listed in empty search.\nSearch for a file, a folder or a tag in your Vault.\nToggle folders and tags on/off to show in the list.", SHOW_HIDE_ATTACHMENTS: "Show/Hide attachments", SHOW_HIDE_VIRTUAL: "Show/Hide virtual nodes", - SHOW_HIDE_INFERRED: "Show/Hide inferred relationships", + SHOW_HIDE_INFERRED: "Show/Hide inferred nodes", SHOW_HIDE_ALIAS: "Show/Hide document alias", SHOW_HIDE_SIBLINGS: "Show/Hide siblings", SHOW_HIDE_EMBEDDEDCENTRAL: "Display central node as embedded frame", diff --git a/src/utils/ParseURLs.ts b/src/utils/ParseURLs.ts new file mode 100644 index 0000000..f369530 --- /dev/null +++ b/src/utils/ParseURLs.ts @@ -0,0 +1,23 @@ +export function extractURLs(input: string): { url: string; alias: string }[] { + const urlRegex = /\[([^\]]+)\]\((https?:\/\/\S+)\)/g; // [alias](url) markdown link format + const plainTextUrlRegex = /(?:^|\s)(https?:\/\/\S+)/g; // Plain text URL format + + const urls: { url: string; alias: string }[] = []; + + // Extract URLs from [alias](url) markdown links + let match; + while ((match = urlRegex.exec(input))) { + const alias = match[1]; + const url = match[2]; + urls.push({ url, alias }); + } + + // Extract plain text URLs + while ((match = plainTextUrlRegex.exec(input))) { + const alias = match[1]; // Alias is the URL itself for plain text URLs + const url = match[1]; + urls.push({ url, alias }); + } + + return urls; +} \ No newline at end of file diff --git a/styles.css b/styles.css index 1880aeb..be8e9b7 100644 --- a/styles.css +++ b/styles.css @@ -46,7 +46,7 @@ } .excalibrain-toolspanel-wrapper { - z-index: 3; + z-index: 1; position: absolute; top: 10px; padding-left: 10px; @@ -56,7 +56,7 @@ } .excalibrain-history-wrapper { - z-index: 3; + z-index: 1; position: absolute; bottom: 0px; padding-left: 7rem; From 83e9f17fb85c5b3ee5c612e95242cb6b70c23c55 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Mon, 31 Jul 2023 19:17:58 +0200 Subject: [PATCH 07/12] Revert "fix overlap in compact view" This reverts commit 509d8b356fce18cbfd92374858e6bafe32e25dc5. --- src/Scene.ts | 2 +- src/graph/Layout.ts | 139 ++++---------------------------------------- src/graph/Node.ts | 15 ----- 3 files changed, 11 insertions(+), 145 deletions(-) diff --git a/src/Scene.ts b/src/Scene.ts index 17eb756..e772bec 100644 --- a/src/Scene.ts +++ b/src/Scene.ts @@ -689,7 +689,7 @@ export class Scene { //------------------------------------------------------- // Render ea.style.opacity = 100; - await Promise.all(this.layouts.map(async (layout) => await layout.render(isCompactView))); + await Promise.all(this.layouts.map(async (layout) => await layout.render())); const nodeElements = ea.getElements(); this.links.render(Array.from(this.toolsPanel.linkTagFilter.selectedLinks)); diff --git a/src/graph/Layout.ts b/src/graph/Layout.ts index fe6a1a0..72542d4 100644 --- a/src/graph/Layout.ts +++ b/src/graph/Layout.ts @@ -54,128 +54,7 @@ export class Layout { : getRowLayout(itemCount % columns).map(idx => idx ? sortedNodes[i*columns+idx-1]:null)); } - private setCompactPosition(center00: { x: number, y: number }) { - const fixOverlap = (center00: { x: number, y: number }) => { - - //I think if LayoutSpecification could add layoutPositionTag property: 'center' | 'children' | ... - //,this will be more simple and clear. - const layoutPosition = this.spec.origoX === 0 - ? (this.spec.top === null && this.spec.bottom === null) - ? 'center' - : this.spec.origoY < 0 ? 'top' : 'bottom' - : (this.spec.top === null && this.spec.bottom === null) - ? this.spec.origoX < 0 ? 'left' : 'right' - : 'sibling'; - - if (layoutPosition === 'center') { - return this.setPosition(center00); - } - if (layoutPosition === 'sibling') { - const maxNodeWidth = Math.max(...this.renderedNodes.map(row => { - return row.first().labelSize().width - })); - const gap = Math.abs(this.spec.origoX); - const offset = maxNodeWidth > gap - ? (maxNodeWidth - gap) + 0.5 * maxNodeWidth - : 0; - const newCenter00 = { - x: center00.x + offset, - y: center00.y, - } - return this.setPosition(newCenter00); - } - if (layoutPosition === 'left' || layoutPosition === 'right') { - const maxNodeWidth = Math.max(...this.renderedNodes.flat().map(node => node.labelSize().width)); - const offset = maxNodeWidth > this.spec.columnWidth - ? (maxNodeWidth - this.spec.columnWidth) / 2 + 0.2 * maxNodeWidth - : 0; - const newCenter00 = { - x: layoutPosition === 'left' - ? center00.x - offset - : center00.x + offset, - y: center00.y, - } - return this.setPosition(newCenter00); - - } - else { - const rowWidth = this.spec.columns * this.spec.columnWidth; - const nodeGap = this.spec.columnWidth * 0.5; - const initialCenter = (spec: LayoutSpecification, width: number, row: number) => { - return width > spec.columnWidth - ? { - x: center00.x + (width - spec.columnWidth) / 2, - y: center00.y + row * spec.rowHeight, - } - : { - x: center00.x - (spec.columnWidth - width) / 2, - y: center00.y + row * spec.rowHeight, - } - } - const nodes = this.renderedNodes.flat().filter(node => !!node); - let stackWidth = 0; - let nodesInfo: { x: number, y: number, width: number }[][] = []; - const alignCenter = (nodeInfo: { x: number, y: number, width: number }[]) => { - const residue = rowWidth - stackWidth; - const offset = (residue / 2) / nodeInfo.length; - return nodeInfo.map(node => { return { ...node, x: node.x + offset }; }); - } - - nodes.forEach((node, index) => { - const width = node.labelSize().width; - const row = nodesInfo.length - 1; - if (index === 0) { - const init = initialCenter(this.spec, width, 0); - stackWidth = width; - nodesInfo.push([{ ...init, width }]); - } - else if ((stackWidth + nodeGap + width) > rowWidth) { - nodesInfo[row] = alignCenter(nodesInfo[row]); - const newRow = row + 1; - const init = initialCenter(this.spec, width, newRow); - stackWidth = width; - nodesInfo.push([{ width, ...init }]); - } - else { - const prev_center = nodesInfo[row].last(); - nodesInfo[row].push({ - ...prev_center, - width, - x: prev_center.x + prev_center.width / 2 + nodeGap + width / 2, - }) - stackWidth = stackWidth + nodeGap + width; - } - - }); - - const nodePosition = nodesInfo.flatMap((nodes, row) => { - if (row === nodesInfo.length - 1) { - return alignCenter(nodes) - } - return nodes - }); - - return nodes.map((node, index) => { - const info = nodePosition[index]; - node?.setCenter({ ...info }); - return node; - }); - } - } - return fixOverlap(center00); - } - private setPosition(center00: { x: number, y: number }) { - return this.renderedNodes.map((nodes, row) => { - return nodes.map((node, idx) => { - node?.setCenter({ - x: center00.x + idx * this.spec.columnWidth, - y: center00.y + row * this.spec.rowHeight - }); - return node - }) - }).flat(); - } - async render(isCompactView:boolean) { + async render() { this.layout(); const rows = this.renderedNodes.length; const height = rows * this.spec.rowHeight; @@ -192,13 +71,15 @@ export class Layout { x: this.spec.origoX - (this.spec.columns === 1 ? 0 : (this.spec.columns-1)/2*this.spec.columnWidth), y: top }; - - const renderedNodesWithPosition = isCompactView - ? this.setCompactPosition(center00) - : this.setPosition(center00); - for (const [_, node] of renderedNodesWithPosition.entries()) { - if (node) { - await node.render(); + for (const [row, nodes] of this.renderedNodes.entries()) { + for (const [idx, node] of nodes.entries()) { + if(node) { + node.setCenter({ + x: center00.x + idx*this.spec.columnWidth, + y: center00.y + row*this.spec.rowHeight + }); + await node.render(); + } } } } diff --git a/src/graph/Node.ts b/src/graph/Node.ts index dd76ea8..eb9f649 100644 --- a/src/graph/Node.ts +++ b/src/graph/Node.ts @@ -80,21 +80,6 @@ export class Node { : label; } - labelSize(){ - const ea = this.ea; - ea.style.fontSize = this.style.fontSize; - ea.style.fontFamily = this.style.fontFamily; - ea.style.fillStyle = this.style.fillStyle; - ea.style.roughness = this.style.roughness; - ea.style.strokeSharpness = this.style.strokeShaprness; - ea.style.strokeWidth = this.style.strokeWidth; - ea.style.strokeColor = this.style.textColor; - ea.style.backgroundColor = "transparent"; - const label = this.displayText(); - const labelSize = ea.measureText(`${label}`); - return {width:labelSize.width + 2*this.style.padding,height:labelSize.height} ; - } - setCenter(center:{x:number, y:number}) { this.center = center; } From 2bf194d0fe31ddd9e1cfcb194cf1f94e837aad7b Mon Sep 17 00:00:00 2001 From: zsviczian Date: Mon, 31 Jul 2023 19:18:19 +0200 Subject: [PATCH 08/12] Delete ParseURLs.ts --- src/utils/ParseURLs.ts | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 src/utils/ParseURLs.ts diff --git a/src/utils/ParseURLs.ts b/src/utils/ParseURLs.ts deleted file mode 100644 index f369530..0000000 --- a/src/utils/ParseURLs.ts +++ /dev/null @@ -1,23 +0,0 @@ -export function extractURLs(input: string): { url: string; alias: string }[] { - const urlRegex = /\[([^\]]+)\]\((https?:\/\/\S+)\)/g; // [alias](url) markdown link format - const plainTextUrlRegex = /(?:^|\s)(https?:\/\/\S+)/g; // Plain text URL format - - const urls: { url: string; alias: string }[] = []; - - // Extract URLs from [alias](url) markdown links - let match; - while ((match = urlRegex.exec(input))) { - const alias = match[1]; - const url = match[2]; - urls.push({ url, alias }); - } - - // Extract plain text URLs - while ((match = plainTextUrlRegex.exec(input))) { - const alias = match[1]; // Alias is the URL itself for plain text URLs - const url = match[1]; - urls.push({ url, alias }); - } - - return urls; -} \ No newline at end of file From 9a366e52dc0c3688a5dfafc274c8ad83470c6825 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Mon, 31 Jul 2023 22:57:22 +0200 Subject: [PATCH 09/12] midway through restoring code after rollback --- src/Components/ToolsPanel.ts | 8 +- src/Scene.ts | 19 +- src/Scene2.ts | 985 +++++++++++++++++++++++++++++++++++ src/Settings.ts | 44 +- src/excalibrain-main.ts | 55 +- src/graph/Node.ts | 27 +- src/graph/Page.ts | 25 +- src/graph/Pages.ts | 33 +- src/graph/URLParser.ts | 9 - src/lang/locale/en.ts | 7 +- styles.css | 4 +- 11 files changed, 1140 insertions(+), 76 deletions(-) create mode 100644 src/Scene2.ts diff --git a/src/Components/ToolsPanel.ts b/src/Components/ToolsPanel.ts index 7bd06be..7da69f8 100644 --- a/src/Components/ToolsPanel.ts +++ b/src/Components/ToolsPanel.ts @@ -234,11 +234,11 @@ export class ToolsPanel { //------------ // Render weblinks in page //------------ - /*this.buttons.push( + this.buttons.push( new ToggleButton( this.plugin, - ()=>this.plugin.settings.showURLs, - (val:boolean)=>this.plugin.settings.showURLs = val, + ()=>this.plugin.settings.showURLNodes, + (val:boolean)=>this.plugin.settings.showURLNodes = val, buttonsWrapperDiv, { display: "🌐", @@ -247,7 +247,7 @@ export class ToolsPanel { }, false ) - )*/ + ) //------------ //Display siblings diff --git a/src/Scene.ts b/src/Scene.ts index e772bec..631a791 100644 --- a/src/Scene.ts +++ b/src/Scene.ts @@ -337,6 +337,7 @@ export class Scene { api.zoomToFit(null, 5, 0.15); } ea.targetView.linksAlwaysOpenInANewPane = true; + ea.targetView.allowFrameButtonsInViewMode = true; await this.addEventHandler(); this.historyPanel = new HistoryPanel((this.leaf.view as TextFileView).contentEl.querySelector(".excalidraw"),this.plugin); new Notice("ExcaliBrain On"); @@ -737,10 +738,17 @@ export class Scene { } private async brainEventHandler (leaf:WorkspaceLeaf, startup:boolean = false) { + const settings = this.plugin.settings; + + if(!this.ea.targetView?.file || this.ea.targetView.file.path !== settings.excalibrainFilepath) { + this.unloadScene(); + return; + } + if(this.disregardLeafChange) { return; } - const settings = this.plugin.settings; + if(!startup && settings.embedCentralNode) { return; } @@ -757,11 +765,6 @@ export class Scene { } if(this.pinLeaf && leaf !== this.centralLeaf) return; - - if(!this.ea.targetView?.file || this.ea.targetView.file.path !== settings.excalibrainFilepath) { - this.unloadScene(); - return; - } if(!(leaf?.view && (leaf.view instanceof FileView) && leaf.view.file)) { this.blockUpdateTimer = false; @@ -920,6 +923,10 @@ export class Scene { this.ea.targetView.linksAlwaysOpenInANewPane = false; } + if(this.ea.targetView && isBoolean(this.ea.targetView.allowFrameButtonsInViewMode)) { + this.ea.targetView.allowFrameButtonsInViewMode = false; + } + if(this.ea.targetView && this.ea.targetView.excalidrawAPI) { try { this.ea.targetView.semaphores.saving = false; diff --git a/src/Scene2.ts b/src/Scene2.ts new file mode 100644 index 0000000..1c11a79 --- /dev/null +++ b/src/Scene2.ts @@ -0,0 +1,985 @@ +import { App, FileView, Notice, TextFileView, TFile, WorkspaceLeaf } from "obsidian"; +import { ExcalidrawAutomate } from "obsidian-excalidraw-plugin/lib/ExcalidrawAutomate"; +import { EMPTYBRAIN } from "./constants/emptyBrainFile"; +import { Layout } from "./graph/Layout"; +import { Links } from "./graph/Links"; +import { Node } from "./graph/Node"; +import ExcaliBrain from "./excalibrain-main"; +import { ExcaliBrainSettings } from "./Settings"; +import { ToolsPanel } from "./Components/ToolsPanel"; +import { Mutable, Neighbour, RelationType, Role } from "./types"; +import { HistoryPanel } from "./Components/HistoryPanel"; +import { WarningPrompt } from "./utils/Prompts"; +import { keepOnTop } from "./utils/utils"; +import { ExcalidrawElement } from "obsidian-excalidraw-plugin"; +import { isEmbedFileType } from "./utils/fileUtils"; +import { Page } from "./graph/Page"; +import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types" + +export class Scene { + ea: ExcalidrawAutomate; + plugin: ExcaliBrain; + app: App; + leaf: WorkspaceLeaf; + centralPagePath: string; //path of the page in the center of the graph + centralPageFile: TFile; + public centralLeaf: WorkspaceLeaf; //workspace leaf containing the central page + textSize: {width:number, height:number}; + nodeWidth: number; + nodeHeight: number; + public disregardLeafChange: boolean = false; + public terminated: boolean; + public nodesMap: Map = new Map(); + public links: Links; + private layouts: Layout[] = []; + private removeEH: Function; + private removeTimer: Function; + private removeOnCreate: Function; + private removeOnModify: Function; + private removeOnDelete: Function; + private removeOnRename: Function; + private blockUpdateTimer: boolean = false; + public toolsPanel: ToolsPanel; + private historyPanel: HistoryPanel; + public vaultFileChanged: boolean = false; + public pinLeaf: boolean = false; + public focusSearchAfterInitiation: boolean = true; + private zoomToFitOnNextBrainLeafActivate: boolean = false; //this addresses the issue caused in Obsidian 0.16.0 when the brain graph is rendered while the leaf is hidden because tab is not active + private rootNode: Node; + + constructor(plugin: ExcaliBrain, newLeaf: boolean, leaf?: WorkspaceLeaf) { + this.ea = plugin.EA; + this.plugin = plugin; + this.app = plugin.app; + this.leaf = leaf ?? app.workspace.getLeaf(newLeaf); + this.terminated = false; + this.links = new Links(plugin); + } + + public getCentralLeaf(): WorkspaceLeaf { + if(this.plugin.settings.embedCentralNode) { + return null; + } + return this.centralLeaf; + } + + public async initialize(focusSearchAfterInitiation: boolean) { + this.focusSearchAfterInitiation = focusSearchAfterInitiation; + await this.plugin.loadSettings(); + if(!this.leaf?.view) return; + this.toolsPanel = new ToolsPanel((this.leaf.view as TextFileView).contentEl.querySelector(".excalidraw"),this.plugin); + this.initializeScene(); + } + + /** + * Check if ExcaliBrain is currently active + * @returns boolean; true if active + */ + public isActive() { + //@ts-ignore + return !this.terminated && app.workspace.getLeafById(this.leaf?.id) + } + + /** + * Updates the current Scene applying changes in the Index + * @returns + */ + public async reRender(updateIndex:boolean = true) { + if(!this.isActive()) { + return; + } + + if(!this.centralPagePath) { + return; + } + + if(updateIndex) { + this.vaultFileChanged = false; + await this.plugin.createIndex(); //temporary + } + + keepOnTop(this.ea); + const centralPage = this.plugin.pages.get(this.centralPagePath); + if( + centralPage?.file && + !(centralPage.isFolder || centralPage.isTag || centralPage.isVirtual) && + !this.plugin.settings.embedCentralNode + ) { + if(!this.centralLeaf) { + this.ea.openFileInNewOrAdjacentLeaf(centralPage.file); + } else if ( + //@ts-ignore + this.centralLeaf.view?.file?.path !== centralPage.file.path + ) { + this.centralLeaf.openFile(centralPage.file, {active: false}); + } + } + await this.render(this.plugin.settings.embedCentralNode); + } + + private getCentralPage():Page { + //centralPagePath might no longer be valid in case the user changed the filename of the central page + //this is relevant only when the central page is embedded, since if the file is in another leaf the leaf.view.file will + //have the right new path + let centralPage = this.plugin.pages.get(this.centralPagePath) + if(!centralPage && this.centralPageFile) { + this.centralPagePath = this.centralPageFile.path; + centralPage = this.plugin.pages.get(this.centralPageFile.path); + } + return centralPage; + } + + /** + * Renders the ExcaliBrain graph for the file provided by its path + * @param path + * @returns + */ + public async renderGraphForPath(path: string, shouldOpenFile:boolean = true) { + if(!this.isActive()) { + return; + } + + this.blockUpdateTimer = true; //blocks the updateTimer + const settings = this.plugin.settings; + const page = this.plugin.pages.get(path); + if(!page) { + this.blockUpdateTimer = false; + return; + } + + const isFile = !(page.isFolder || page.isTag || page.isVirtual || page.isURL); + + if(isFile && !page.file) { + this.blockUpdateTimer = false; + return; + } + + //abort excalibrain if the file in the Obsidian view has changed + if(!this.ea.targetView?.file || this.ea.targetView.file.path !== settings.excalibrainFilepath) { + this.unloadScene(); + return; + } + + //don't render if the user is trying to render the excaliBrain file itself + if (isFile && (page.file.path === this.ea.targetView.file.path)) { //brainview drawing is the active leaf + this.blockUpdateTimer = false; + return; + } + + keepOnTop(this.plugin.EA); + + const centralPage = this.getCentralPage(); + const isSameFileAsCurrent = centralPage && + ((isFile && centralPage.file === page.file) || + (page.isURL && centralPage.isURL && centralPage.url === page.url)) + + // if the file hasn't changed don't update the graph + if(isSameFileAsCurrent && (page.isURL || (page.file.stat.mtime === centralPage.mtime))) { + this.blockUpdateTimer = false; + return; //don't reload the file if it has not changed + } + + if(isFile && shouldOpenFile && !settings.embedCentralNode) { + const centralLeaf = this.getCentralLeaf(); + //@ts-ignore + if(!centralLeaf || !this.app.workspace.getLeafById(centralLeaf.id)) { + this.centralLeaf = this.ea.openFileInNewOrAdjacentLeaf(page.file); + } else { + centralLeaf.openFile(page.file, {active: false}); + } + this.addToHistory(page.file.path); + } else { + this.addToHistory(page.path); + } + + if(page.isFolder && !settings.showFolderNodes) { + settings.showFolderNodes = true; + this.toolsPanel.rerender(); + } + + if(page.isURL && !settings.showURLNodes) { + settings.showURLNodes = true; + this.toolsPanel.rerender(); + } + + if(page.isTag && !settings.showTagNodes) { + settings.showTagNodes = true; + this.toolsPanel.rerender(); + } + + this.centralPagePath = path; + this.centralPageFile = page.file; + await this.render(isSameFileAsCurrent); + } + + async addToHistory(path: string) { + const nh = this.plugin.navigationHistory; + if(nh.last() === path) { + return; + } + const i = nh.indexOf(path); + if(i>-1) { + nh.splice(i,1); + } + if(nh.length>50) { + nh.shift(); + } + nh.push(path); + } + + public static async openExcalidrawLeaf(ea: ExcalidrawAutomate, settings: ExcaliBrainSettings, leaf: WorkspaceLeaf) { + let counter = 0; + + let file = app.vault.getAbstractFileByPath(settings.excalibrainFilepath); + if(file && !(file instanceof TFile)) { + new Notice(`Please check settings. ExcaliBrain path (${settings.excalibrainFilepath}) points to a folder, not a file`); + return; + } + if(!file) { + file = await app.vault.create(settings.excalibrainFilepath,EMPTYBRAIN); + //an ugly temporary hack waiting for metadataCache to index the new file + while(file instanceof TFile && !ea.isExcalidrawFile(file) && counter++<10) { + await sleep(50); + } + } + counter = 0; + if(file && file instanceof TFile && !ea.isExcalidrawFile(file)) { + (new WarningPrompt( + app, + "⚠ File Exists", + `${file.path} already exists in your Vault. Is it ok to overwrite this file? If not, change ExcaliBrain file path in plugin settings.`) + ).show(async (result: boolean) => { + if(result) { + await app.vault.modify(file as TFile,EMPTYBRAIN); + while(file instanceof TFile && !ea.isExcalidrawFile(file) && counter++<10) { + await sleep(50); + } + Scene.openExcalidrawLeaf(ea, settings, leaf); + } else { + new Notice(`Could not start ExcaliBrain. Please change the ExcaliBrain file path in plugin settings.`); + } + }); + return; + } + if(!leaf) { + leaf = app.workspace.getLeaf(false); + if(leaf.getViewState().type !== "empty") { + leaf = ea.getLeaf(leaf, "new-pane"); + } + } + if(settings.defaultAlwaysOnTop && leaf && ea.DEVICE.isDesktop) { + //@ts-ignore + const ownerWindow = leaf.view?.ownerWindow; + if(ownerWindow && (ownerWindow !== window) && !ownerWindow.electronWindow?.isMaximized()) { + ownerWindow.electronWindow.setAlwaysOnTop(true); + } + } + await leaf.openFile(file as TFile); + } + + public async initializeScene() { + this.disregardLeafChange = false; + const ea = this.ea; + const settings = this.plugin.settings; + const style = { + ...settings.baseNodeStyle, + ...settings.centralNodeStyle, + }; + style.textColor = settings.baseNodeStyle.textColor; + + let counter = 0; + ea.clear(); + ea.setView(this.leaf.view as any); + //delete existing elements from view. The actual delete will happen when addElementsToView is called + //I delete them this way to avoid the splash screen flashing up when the scene is cleared + ea.copyViewElementsToEAforEditing(ea.getViewElements()); + ea.getElements().forEach((el: Mutable)=>el.isDeleted=true); + + while(!ea.targetView.excalidrawAPI && counter++<10) { + await sleep(50); + } + if(!ea.targetView.excalidrawAPI) { + new Notice(`Error initializing Excalidraw view`); + return; + } + + const api = ea.getExcalidrawAPI(); + this.ea.registerThisAsViewEA(); + this.ea.targetView.semaphores.saving = true; //disable saving by setting this Excalidraw flag (not published API) + api.setMobileModeAllowed(false); //disable mobile view https://github.com/zsviczian/excalibrain/issues/9 + ea.style.fontFamily = style.fontFamily; + ea.style.fontSize = style.fontSize; + this.textSize = ea.measureText("m".repeat(style.maxLabelLength)); + this.nodeWidth = this.textSize.width + 2 * style.padding; + if(this.plugin.settings.compactView) { + this.nodeWidth = this.nodeWidth * 0.6; + } + this.nodeHeight = 2 * (this.textSize.height + 2 * style.padding); + + const frame1 = () => { + api.updateScene({ + appState: { + viewModeEnabled:true, + activeTool: { + lastActiveToolBeforeEraser: null, + locked: false, + type: "selection" + }, + theme: "light", + viewBackgroundColor: this.plugin.settings.backgroundColor + } + }); + } + const frame2 = () => { + ea.style.strokeColor = style.textColor; + ea.addText(0,0,"🚀 To get started\nselect a document using the search in the top left or\n" + + "open a document in another pane.\n\n" + + "✨ For the best experience enable 'Open in adjacent pane'\nin Excalidraw settings " + + "under 'Links and Transclusion'.\n\n⚠ ExcaliBrain may need to wait for " + + "DataView to initialize its index.\nThis can take up to a few minutes after starting Obsidian.", {textAlign:"center"}); + ea.addElementsToView(false,false); + ea.targetView.clearDirty(); //hack to prevent excalidraw from saving the changes + } + const frame3 = async () => { + if(this.plugin.settings.allowAutozoom) { + setTimeout(()=>api.zoomToFit(null, this.plugin.settings.maxZoom, 0.15),100); + } + ea.targetView.linksAlwaysOpenInANewPane = true; + ea.targetView.allowFrameButtonsInViewMode = true; + await this.addEventHandler(); + this.historyPanel = new HistoryPanel((this.leaf.view as TextFileView).contentEl.querySelector(".excalidraw"),this.plugin); + new Notice("ExcaliBrain On"); + } + frame1(); + frame2(); + frame3(); + } + + addNodes(x:{ + neighbours:Neighbour[], + layout:Layout, + isCentral:boolean, + isSibling:boolean, + friendGateOnLeft: boolean + }) { + x.neighbours.forEach(n => { + if(n.page.path === this.ea.targetView.file.path) { + return; + } + const node = new Node({ + ea: this.ea, + page: n.page, + isInferred: n.relationType === RelationType.INFERRED, + isCentral: x.isCentral, + isSibling: x.isSibling, + friendGateOnLeft: x.friendGateOnLeft + }); + this.nodesMap.set(n.page.path,node); + x.layout.nodes.push(node); + }); + } + + /** + * if retainCentralNode is true, the central node is not removed from the scene when the scene is rendered + * this will ensure that the embedded frame in the center is not reloaded + * @param retainCentralNode + * @returns + */ + private async render(retainCentralNode:boolean = false) { + if(this.historyPanel) { + this.historyPanel.rerender() + } + if(!this.centralPagePath) return; + const settings = this.plugin.settings; + const isCompactView = settings.compactView; + let centralPage = this.plugin.pages.get(this.centralPagePath); + if(!centralPage) { + //path case sensitivity issue + this.centralPagePath = this.plugin.lowercasePathMap.get(this.centralPagePath.toLowerCase()); + centralPage = this.plugin.pages.get(this.centralPagePath); + if(!centralPage) return; + this.centralPageFile = centralPage.file; + } + + const ea = this.ea; + retainCentralNode = + retainCentralNode && Boolean(this.rootNode) && + settings.embedCentralNode && ((centralPage.file && isEmbedFileType(centralPage.file,ea)) || centralPage.isURL); + + this.zoomToFitOnNextBrainLeafActivate = !ea.targetView.containerEl.isShown(); + + ea.clear(); + const excalidrawAPI = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI; + ea.copyViewElementsToEAforEditing(ea.getViewElements()); + //delete existing elements from view. The actual delete will happen when addElementsToView is called + //I delete them this way to avoid the splash screen flashing up when the scene is cleared + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1248#event-9940972555 + ea.getElements() + .filter((el: ExcalidrawElement)=>!retainCentralNode || !this.rootNode.embeddedElementIds.includes(el.id)) + .forEach((el: Mutable)=>el.isDeleted=true); + ea.style.verticalAlign = "middle"; + + //Extract URLs as child nodes + + //List nodes for the graph + const parents = centralPage.getParents() + .filter(x => + (x.page.path !== centralPage.path) && + !settings.excludeFilepaths.some(p => x.page.path.startsWith(p)) && + (!x.page.primaryStyleTag || !this.toolsPanel.linkTagFilter.selectedTags.has(x.page.primaryStyleTag))) + .slice(0,settings.maxItemCount); + const parentPaths = parents.map(x=>x.page.path); + + const children =centralPage.getChildren() + .filter(x => + (x.page.path !== centralPage.path) && + !settings.excludeFilepaths.some(p => x.page.path.startsWith(p)) && + (!x.page.primaryStyleTag || !this.toolsPanel.linkTagFilter.selectedTags.has(x.page.primaryStyleTag))) + .slice(0,settings.maxItemCount); + + const friends = centralPage.getLeftFriends().concat(centralPage.getPreviousFriends()) + .filter(x => + (x.page.path !== centralPage.path) && + !settings.excludeFilepaths.some(p => x.page.path.startsWith(p)) && + (!x.page.primaryStyleTag || !this.toolsPanel.linkTagFilter.selectedTags.has(x.page.primaryStyleTag))) + .slice(0,settings.maxItemCount); + + const nextFriends = centralPage.getRightFriends().concat(centralPage.getNextFriends()) + .filter(x => + (x.page.path !== centralPage.path) && + !settings.excludeFilepaths.some(p => x.page.path.startsWith(p)) && + (!x.page.primaryStyleTag || !this.toolsPanel.linkTagFilter.selectedTags.has(x.page.primaryStyleTag))) + .slice(0,settings.maxItemCount); + + const rawSiblings = centralPage + .getSiblings() + .filter(s => + //the node is not included already as a parent, child, or friend + !(parents.some(p=>p.page.path === s.page.path) || + children.some(c=>c.page.path === s.page.path) || + friends.some(f=>f.page.path === s.page.path) || + nextFriends.some(f=>f.page.path === s.page.path) || + //or not exluded via folder path in settings + settings.excludeFilepaths.some(p => s.page.path.startsWith(p)) + ) && + //it is not the current central page + (s.page.path !== centralPage.path)); + + const siblings = rawSiblings + .filter(s => + //Only display siblings for which the parents are actually displayed. + //There might be siblings whose parnets have been filtered from view + s.page.getParents().map(x=>x.page.path).some(y=>parentPaths.includes(y)) && + //filter based on primary tag + (!s.page.primaryStyleTag || !this.toolsPanel.linkTagFilter.selectedTags.has(s.page.primaryStyleTag))) + .slice(0,settings.maxItemCount); + + //------------------------------------------------------- + // Generate layout and nodes + this.nodesMap = new Map(); + this.links = new Links(this.plugin); + this.layouts = []; + const manyFriends = friends.length >= 10; + const manyNextFriends = nextFriends.length >= 10; + const baseStyle = settings.baseNodeStyle; + const siblingsCols = siblings.length >= 20 + ? 3 + : siblings.length >= 10 + ? 2 + : 1; + const childrenCols = isCompactView + ? (children.length <= 12 + ? [1, 1, 2, 3, 3, 3, 3, 2, 2, 3, 3, 2, 2][children.length] + : 3) + : (children.length <= 12 + ? [1, 1, 2, 3, 3, 3, 3, 4, 4, 5, 5, 4, 4][children.length] + : 5); + const parentCols = isCompactView + ? (parents.length < 2 + ? 1 + : 2) + : (parents.length < 5 + ? [1, 1, 2, 3, 2][parents.length] + : 3); + + + const isCenterEmbedded = + settings.embedCentralNode && + !centralPage.isVirtual && + !centralPage.isFolder && + !centralPage.isTag; + const centerEmbedWidth = settings.centerEmbedWidth; + const centerEmbedHeight = settings.centerEmbedHeight; + + const lCenter = new Layout({ + origoX: 0, + origoY: isCenterEmbedded + ? centerEmbedHeight - this.nodeHeight/2 + : 0, + top: null, + bottom: null, + columns: 1, + columnWidth: isCenterEmbedded + ? centerEmbedWidth + : this.nodeWidth, + rowHeight: isCenterEmbedded + ? centerEmbedHeight + : this.nodeHeight, + }); + this.layouts.push(lCenter); + + const lChildren = new Layout({ + origoX: 0, + origoY: isCenterEmbedded + ? centerEmbedHeight + 1.5 * this.nodeHeight + : 2.5 * this.nodeHeight, + top: isCenterEmbedded + ? centerEmbedHeight + this.nodeHeight + : 2 * this.nodeHeight, + bottom: null, + columns: childrenCols, + columnWidth: this.nodeWidth, + rowHeight: this.nodeHeight + }); + this.layouts.push(lChildren); + + const friendOrigoX = isCompactView && isCenterEmbedded + ? centerEmbedWidth/2 + this.nodeWidth + : Math.max( + (((manyNextFriends?1:0)+Math.max(childrenCols,parentCols)+1.9)/2.4) * this.nodeWidth, // (manyChildren ? -3 : -2) * this.nodeWidth, + isCenterEmbedded + ? centerEmbedWidth/2 + this.nodeWidth + : 0 + ); + + const lFriends = new Layout({ + origoX: -friendOrigoX, + origoY: isCenterEmbedded + ? centerEmbedHeight/2 + : 0, + top: null, + bottom: null, + columns: 1, + columnWidth: this.nodeWidth, + rowHeight: this.nodeHeight + }); + this.layouts.push(lFriends); + + const lNextFriends = new Layout({ + origoX: friendOrigoX, + origoY: isCenterEmbedded + ? centerEmbedHeight/2 + : 0, + top: null, + bottom: null, + columns: 1, + columnWidth: this.nodeWidth, + rowHeight: this.nodeHeight + }); + this.layouts.push(lNextFriends); + + const lParents = new Layout({ + origoX: 0, + origoY: -2.5 * this.nodeHeight, + top: null, + bottom: -2 * this.nodeHeight, + columns: parentCols, // 3, + columnWidth: this.nodeWidth, + rowHeight: this.nodeHeight + }); + this.layouts.push(lParents); + + const siblingsStyle = settings.siblingNodeStyle; + const siblingsPadding = siblingsStyle.padding??baseStyle.padding; + const siblingsLabelLength = siblingsStyle.maxLabelLength??baseStyle.maxLabelLength; + ea.style.fontFamily = siblingsStyle.fontFamily; + ea.style.fontSize = siblingsStyle.fontSize; + const siblingsTextSize = ea.measureText("m".repeat(siblingsLabelLength+3)); + const siblingsNodeWidth = siblingsTextSize.width + 3 * siblingsPadding; + const siblingsNodeHeight = 2 * (siblingsTextSize.height + 2 * siblingsPadding); + + const lSiblings = new Layout({ + origoX: this.nodeWidth * ((parentCols-1)/2 + (siblingsCols+1.5)/3), + origoY: -2.5 * this.nodeHeight, + top: null, + bottom: - this.nodeHeight/2, + columns: siblingsCols, + columnWidth: siblingsNodeWidth, + rowHeight: siblingsNodeHeight, + }) + this.layouts.push(lSiblings); + + this.rootNode = new Node({ + ea, + page: centralPage, + isInferred: false, + isCentral: true, + isSibling: false, + friendGateOnLeft: true, + isEmbeded: isCenterEmbedded, + embeddedElementIds: retainCentralNode ? this.rootNode?.embeddedElementIds : undefined, + }); + + this.nodesMap.set(centralPage.path,this.rootNode); + lCenter.nodes.push(this.rootNode); + + this.addNodes({ + neighbours: parents, + layout: lParents, + isCentral: false, + isSibling: false, + friendGateOnLeft: true + }); + + this.addNodes({ + neighbours: children, + layout: lChildren, + isCentral: false, + isSibling: false, + friendGateOnLeft: true + }); + + this.addNodes({ + neighbours: friends, + layout: lFriends, + isCentral: false, + isSibling: false, + friendGateOnLeft: false + }); + + this.addNodes({ + neighbours: nextFriends, + layout: lNextFriends, + isCentral: false, + isSibling: false, + friendGateOnLeft: true + }); + + if(settings.renderSiblings) { + this.addNodes({ + neighbours: siblings, + layout: lSiblings, + isCentral: false, + isSibling: true, + friendGateOnLeft: true + }); + } + + //------------------------------------------------------- + // Generate links for all displayed nodes + const addLinks = (nodeA: Node, neighbours:Neighbour[],role: Role) => { + neighbours.forEach(neighbour=>{ + const nodeB = this.nodesMap.get(neighbour.page.path); + if(!nodeB) { + return; + } + this.links.addLink( + nodeA, + nodeB, + role, + neighbour.relationType, + neighbour.typeDefinition, + neighbour.linkDirection, + ea, + settings + ) + }) + } + + Array.from(this.nodesMap.values()).forEach(nodeA => { + addLinks(nodeA, nodeA.page.getChildren(),Role.CHILD); + addLinks(nodeA, nodeA.page.getParents(),Role.PARENT); + addLinks(nodeA, nodeA.page.getLeftFriends(),Role.LEFT); + addLinks(nodeA, nodeA.page.getPreviousFriends(),Role.LEFT); + addLinks(nodeA, nodeA.page.getRightFriends(),Role.RIGHT); + addLinks(nodeA, nodeA.page.getNextFriends(),Role.RIGHT); + }); + + //------------------------------------------------------- + // Render + ea.style.opacity = 100; + await Promise.all(this.layouts.map(async (layout) => await layout.render())); + const nodeElements = ea.getElements(); + this.links.render(Array.from(this.toolsPanel.linkTagFilter.selectedLinks)); + + const linkElements = ea.getElements().filter(el=>!nodeElements.includes(el)); + + + //hack to send link elements behind node elements + const newImagesDict = linkElements.concat(nodeElements) + .reduce((dict:{[key:string]:any}, obj:ExcalidrawElement) => { + dict[obj.id] = obj; + return dict; + }, {}); + + ea.elementsDict = newImagesDict; + + ea.addElementsToView(false,false); + ea.targetView.clearDirty(); //hack to prevent excalidraw from saving the changes + + excalidrawAPI.updateScene({appState: {viewBackgroundColor: settings.backgroundColor}}); + if(settings.allowAutozoom && !retainCentralNode) { + setTimeout(()=>excalidrawAPI.zoomToFit(ea.getViewElements(),settings.maxZoom,0.15),100); + } + + this.toolsPanel.rerender(); + if(this.focusSearchAfterInitiation && settings.allowAutofocuOnSearch) { + this.toolsPanel.searchElement.focus(); + this.focusSearchAfterInitiation = false; + } + + this.blockUpdateTimer = false; + } + + public isCentralLeafStillThere():boolean { + const settings = this.plugin.settings; + //@ts-ignore + const noCentralLeaf = app.workspace.getLeafById(this.centralLeaf.id) === null ; + if(noCentralLeaf) { + return false; + } + //@ts-ignore + if (this.centralLeaf.view?.file?.path === settings.excalibrainFilepath) { + return false; + } + return true; + } + + private async brainEventHandler (leaf:WorkspaceLeaf, startup:boolean = false) { + const settings = this.plugin.settings; + + if(!this.ea.targetView?.file || this.ea.targetView.file.path !== settings.excalibrainFilepath) { + this.unloadScene(); + return; + } + if(this.disregardLeafChange) { + return; + } + + if(!startup && settings.embedCentralNode) { + return; + } + + this.blockUpdateTimer = true; + await sleep(100); + + //------------------------------------------------------- + //terminate event handler if view no longer exists or file has changed + + if(this.pinLeaf && !this.isCentralLeafStillThere()) { + this.pinLeaf = false; + this.toolsPanel.rerender(); + } + + if(this.pinLeaf && leaf !== this.centralLeaf) return; + + if(!(leaf?.view && (leaf.view instanceof FileView) && leaf.view.file)) { + this.blockUpdateTimer = false; + return; + } + + const rootFile = leaf.view.file; + + if (rootFile.path === this.ea.targetView.file.path) { //brainview drawing is the active leaf + if(this.vaultFileChanged) { + this.zoomToFitOnNextBrainLeafActivate = false; + await this.reRender(true); + } + if(this.zoomToFitOnNextBrainLeafActivate) { + this.zoomToFitOnNextBrainLeafActivate = false; + if(settings.allowAutozoom) { + this.ea.getExcalidrawAPI().zoomToFit(null, settings.maxZoom, 0.15); + } + } + this.blockUpdateTimer = false; + return; + } + + const centralPage = this.getCentralPage(); + if( + centralPage && + centralPage.path === rootFile.path && + rootFile.stat.mtime === centralPage.mtime + ) { + this.blockUpdateTimer = false; + return; //don't reload the file if it has not changed + } + + if(!this.plugin.pages.get(rootFile.path)) { + await this.plugin.createIndex(); + } + + this.addToHistory(rootFile.path); + this.centralPagePath = rootFile.path; + this.centralPageFile = rootFile; + this.centralLeaf = leaf; + this.render(); + } + + private async addEventHandler() { + const fileChangeHandler = () => { + this.vaultFileChanged = true; + } + + const beh = (leaf:WorkspaceLeaf)=>this.brainEventHandler(leaf); + this.app.workspace.on("active-leaf-change", beh); + this.removeEH = () => app.workspace.off("active-leaf-change",beh); + this.setTimer(); + this.app.vault.on("rename",fileChangeHandler); + this.removeOnRename = () => app.vault.off("rename",fileChangeHandler) + this.app.vault.on("modify",fileChangeHandler); + this.removeOnModify = () => app.vault.off("modify",fileChangeHandler) + this.app.vault.on("create",fileChangeHandler); + this.removeOnCreate = () => app.vault.off("create",fileChangeHandler) + this.app.vault.on("delete",fileChangeHandler); + this.removeOnDelete = () => app.vault.off("delete",fileChangeHandler) + + const leaves: WorkspaceLeaf[] = []; + app.workspace.iterateAllLeaves(l=>{ + if( (l.view instanceof FileView) && l.view.file && l.view.file.path !== this.ea.targetView.file.path) { + leaves.push(l); + } + }) + + await this.plugin.createIndex(); //temporary + + let leafToOpen = leaves[0]; + if(leaves.length>0) { + const lastFilePath = app.workspace.getLastOpenFiles()[0]; + if(lastFilePath && lastFilePath !== "") { + const leaf = leaves.filter(l=>(l.view as FileView)?.file.path === lastFilePath); + if(leaf.length>0) { + leafToOpen = leaf[0]; + } + } + keepOnTop(this.plugin.EA); + this.brainEventHandler(leafToOpen, true); + } else { + if(this.plugin.navigationHistory.length>0) { + const lastFilePath = this.plugin.navigationHistory.last(); + setTimeout(()=>this.renderGraphForPath(lastFilePath,true),100); + } + } + } + + setTimer() { + const updateTimer = async () => { + if(this.blockUpdateTimer) { + return; + } + if(this.vaultFileChanged) { + this.vaultFileChanged = false; + await this.plugin.createIndex(); + if(this.centralPagePath) { + const centralPage = this.getCentralPage(); + if(!centralPage) { + //@ts-ignore + if(this.centralLeaf && this.centralLeaf.view && this.centralLeaf.view.file) { + //@ts-ignore + this.centralPageFile = this.centralLeaf.view.file; + this.centralPagePath = this.centralPageFile.path; + } + } + } + this.render(true); + } + } + + if(this.removeTimer) { + this.removeTimer(); + this.removeTimer = undefined; + } + + const timer = setInterval(updateTimer,this.plugin.settings.indexUpdateInterval); + this.removeTimer = () => clearInterval(timer); + } + + + public unloadScene(saveSettings:boolean = true, silent: boolean = false) { + if(this.removeEH) { + this.removeEH(); + this.removeEH = undefined; + } + + if(this.removeTimer) { + this.removeTimer(); + this.removeTimer = undefined; + } + + if(this.removeOnRename) { + this.removeOnRename(); + this.removeOnRename = undefined; + } + + if(this.removeOnModify) { + this.removeOnModify(); + this.removeOnModify = undefined; + } + + if(this.removeOnCreate) { + this.removeOnCreate(); + this.removeOnCreate = undefined; + } + + if(this.removeOnDelete) { + this.removeOnDelete(); + this.removeOnDelete = undefined; + } + + if(this.ea.targetView && isBoolean(this.ea.targetView.linksAlwaysOpenInANewPane)) { + this.ea.targetView.linksAlwaysOpenInANewPane = false; + } + + if(this.ea.targetView && isBoolean(this.ea.targetView.allowFrameButtonsInViewMode)) { + this.ea.targetView.allowFrameButtonsInViewMode = false; + } + + if(this.ea.targetView && this.ea.targetView.excalidrawAPI) { + try { + this.ea.targetView.semaphores.saving = false; + this.ea.targetView.excalidrawAPI.setMobileModeAllowed(true); + this.ea.targetView.excalidrawAPI.updateScene({appState:{viewModeEnabled:false}}); + } catch {} + } + //@ts-ignore + if(this.ea.targetView && this.ea.targetView._loaded) { + try { + this.ea.deregisterThisAsViewEA(); + } catch {} + } + // timout is to make sure Obsidian is not being terminated when scene closes, + // becasue that can lead to crippled settings file + // if the plugin is still there after 400ms, it is safe to save the settings + if(saveSettings) { + setTimeout(async () => { + await this.plugin.loadSettings(); //only overwrite the navigation history, save other synchronized settings + this.plugin.settings.navigationHistory = [...this.plugin.navigationHistory]; + await this.plugin.saveSettings(); + },400); + } + this.toolsPanel?.terminate(); + this.toolsPanel = undefined; + this.historyPanel?.terminate(); + this.historyPanel = undefined; + this.ea.targetView = undefined; + this.leaf = undefined; + this.centralLeaf = undefined; + this.centralPagePath = undefined; + this.centralPageFile = undefined; + this.terminated = true; + //@ts-ignore + if(!this.app.plugins.plugins["obsidian-excalidraw-plugin"]) { + this.plugin.EA = null; + } + if(!silent) { + new Notice("Brain Graph Off"); + } + const mostRecentLeaf = this.app.workspace.getMostRecentLeaf(); + if(mostRecentLeaf) { + this.app.workspace.setActiveLeaf( + mostRecentLeaf, + { focus: true }, + ) + } + } +} \ No newline at end of file diff --git a/src/Settings.ts b/src/Settings.ts index 3e03258..0e0c270 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -34,7 +34,7 @@ export interface ExcaliBrainSettings { excludeFilepaths: string[]; showInferredNodes: boolean; showAttachments: boolean; - showURLs: boolean; + showURLNodes: boolean; showVirtualNodes: boolean; showFolderNodes: boolean; showTagNodes: boolean; @@ -46,6 +46,7 @@ export interface ExcaliBrainSettings { baseNodeStyle: NodeStyle; centralNodeStyle: NodeStyle; inferredNodeStyle: NodeStyle; + urlNodeStyle: NodeStyle; virtualNodeStyle: NodeStyle; siblingNodeStyle: NodeStyle; attachmentNodeStyle: NodeStyle; @@ -70,6 +71,7 @@ export interface ExcaliBrainSettings { ontologySuggesterMidSentenceTrigger: string; boldFields: boolean; allowAutozoom: boolean; + maxZoom: number; allowAutofocuOnSearch: boolean; defaultAlwaysOnTop: boolean; embedCentralNode: boolean; @@ -91,7 +93,7 @@ export const DEFAULT_SETTINGS: ExcaliBrainSettings = { excludeFilepaths: [], showInferredNodes: true, showAttachments: true, - showURLs: true, + showURLNodes: true, showVirtualNodes: true, showFolderNodes: false, showTagNodes: false, @@ -103,13 +105,16 @@ export const DEFAULT_SETTINGS: ExcaliBrainSettings = { baseNodeStyle: DEFAULT_NODE_STYLE, centralNodeStyle: { fontSize: 30, - backgroundColor: "#C49A13FF", + backgroundColor: "#B5B5B5", textColor: "#000000ff", }, inferredNodeStyle: { backgroundColor: "#000005b3", textColor: "#95c7f3ff", }, + urlNodeStyle: { + prefix: "🌐 " + }, virtualNodeStyle: { backgroundColor: "#ff000066", fillStyle: "hachure", @@ -158,6 +163,7 @@ export const DEFAULT_SETTINGS: ExcaliBrainSettings = { ontologySuggesterMidSentenceTrigger: "(", boldFields: false, allowAutozoom: true, + maxZoom: 1, allowAutofocuOnSearch: true, defaultAlwaysOnTop: false, embedCentralNode: false, @@ -1846,16 +1852,28 @@ export class ExcaliBrainSettingTab extends PluginSettingTab { this.dirty = true; })) - new Setting(containerEl) - .setName(t("ALLOW_AUTOFOCUS_ON_SEARCH_NAME")) - .setDesc(fragWithHTML(t("ALLOW_AUTOFOCUS_ON_SEARCH_DESC"))) - .addToggle(toggle => - toggle - .setValue(this.plugin.settings.allowAutofocuOnSearch) - .onChange(value => { - this.plugin.settings.allowAutofocuOnSearch = value; - this.dirty = true; - })) + this.numberslider( + containerEl, + t("MAX_AUTOZOOM_NAME"), + t("MAX_AUTOZOOM_DESC"), + {min:10,max:1000, step:10}, + ()=>this.plugin.settings.maxZoom*100, + (val)=>this.plugin.settings.maxZoom = val/100, + ()=>{}, + false, + 100 + ) + + new Setting(containerEl) + .setName(t("ALLOW_AUTOFOCUS_ON_SEARCH_NAME")) + .setDesc(fragWithHTML(t("ALLOW_AUTOFOCUS_ON_SEARCH_DESC"))) + .addToggle(toggle => + toggle + .setValue(this.plugin.settings.allowAutofocuOnSearch) + .onChange(value => { + this.plugin.settings.allowAutofocuOnSearch = value; + this.dirty = true; + })) new Setting(containerEl) .setName(t("ALWAYS_ON_TOP_NAME")) diff --git a/src/excalibrain-main.ts b/src/excalibrain-main.ts index bf6ea39..fcfc923 100644 --- a/src/excalibrain-main.ts +++ b/src/excalibrain-main.ts @@ -83,10 +83,11 @@ export default class ExcaliBrain extends Plugin { this.registerEvents(); this.urlParser = new URLParser(this); this.app.workspace.onLayoutReady(()=>{ + this.urlParser.init(); this.DVAPI = getAPI(); if(!this.DVAPI) { (new WarningPrompt( - app, + this.app, "⚠ ExcaliBrain Disabled: DataView Plugin not found", t("DATAVIEW_NOT_FOUND")) ).show(async (result: boolean) => { @@ -98,7 +99,7 @@ export default class ExcaliBrain extends Plugin { } if(!this.DVAPI.version.compare('>=', '0.5.31')) { (new WarningPrompt( - app, + this.app, "⚠ ExcaliBrain Disabled: Dataview upgrade requried", t("DATAVIEW_UPGRADE")) ).show(async (result: boolean) => { @@ -201,6 +202,19 @@ export default class ExcaliBrain extends Plugin { await sleep(100); } + counter = 0; + while(!this.urlParser.initalized) { + if(counter++ % 100 === 10) { + new Notice("ExcaliBrain is waiting for URLParser to finish indexing",1000); + } + await sleep(100); + } + + //Add all host urls + this.urlParser.hosts.forEach((url)=>{ + this.pages.add(url, new Page(this.pages, url, null, this, false, false, url, url)); + }); + //Add all folders and files const addFolderChildren = (parentFolder: TFolder, parent: Page) => { const children = parentFolder.children; @@ -256,13 +270,17 @@ export default class ExcaliBrain extends Plugin { //Add all links as inferred children to pages on which they were found this.pages.addResolvedLinks(); + //Add all urls as inferred children to pages on which they were found + //and inferred children of their origins + this.pages.addPageURLs(); + const self = this; setTimeout(async()=>{ //@ts-ignore - const bookmarksPlugin = app.internalPlugins.getPluginById("bookmarks"); + const bookmarksPlugin = this.app.internalPlugins.getPluginById("bookmarks"); if(!bookmarksPlugin) { //code to be removed when bookmarks plugin is released, only leave return //@ts-ignore - const starredPlugin = app.internalPlugins.getPluginById("starred"); + const starredPlugin = this.app.internalPlugins.getPluginById("starred"); if(!starredPlugin) { return; } @@ -562,7 +580,7 @@ export default class ExcaliBrain extends Plugin { } this.EA.onLinkClickHook = (element,linkText,event) => { - const path = linkText.match(/\[\[([^\]]*)/)?.[1]; + const path = linkText.match(/\[\[([^\]]*)/)?.[1] ?? linkText.match(/(http.*)/)?.[1]; if(!path) return true; const page = this.pages.get(path); const ea = this.EA; @@ -595,15 +613,20 @@ export default class ExcaliBrain extends Plugin { return false; } - //if centralPage is in embeddedFrame, simply render the scene + //if centralPage is in embeddedFrame if(this.settings.embedCentralNode) { + //the user clicked the link handle in the top left, then open the file in a leaf if(this.scene.centralPagePath === page.path) { - if(this.scene.isCentralLeafStillThere()) { - this.scene.centralLeaf.openFile(page.file,{active:true}); - return false; + if(page.isURL) { + return true; //let Excalidraw open the webpage + } else { + if(this.scene.isCentralLeafStillThere()) { + this.scene.centralLeaf.openFile(page.file,{active:true}); + return false; + } + ea.targetView.linksAlwaysOpenInANewPane = false; + setTimeout(()=>ea.targetView.linksAlwaysOpenInANewPane = true,300); } - ea.targetView.linksAlwaysOpenInANewPane = false; - setTimeout(()=>ea.targetView.linksAlwaysOpenInANewPane = true,300); return true; } this.scene.renderGraphForPath(path); @@ -612,7 +635,7 @@ export default class ExcaliBrain extends Plugin { const centralLeaf = this.scene.getCentralLeaf(); //handle click on link to existing file - if(!page.isFolder && !page.isTag) { + if(!page.isFolder && !page.isTag && !page.isURL) { //if the leaf attached to ExcaliBrain already has the new file open, render the associated graph if((centralLeaf?.view as TextFileView)?.file?.path === path) { this.scene.renderGraphForPath(path); @@ -821,6 +844,13 @@ export default class ExcaliBrain extends Plugin { display: t("NODESTYLE_INFERRED"), getInheritedStyle: ()=> this.settings.baseNodeStyle }; + this.nodeStyles["url"] = { + style: this. settings.urlNodeStyle, + allowOverride: true, + userStyle: false, + display: t("NODESTYLE_URL"), + getInheritedStyle: ()=> this.settings.baseNodeStyle + }, this.nodeStyles["virtual"] = { style: this.settings.virtualNodeStyle, allowOverride: true, @@ -918,4 +948,3 @@ export default class ExcaliBrain extends Plugin { this.focusSearchAfterInitiation = false; } } - diff --git a/src/graph/Node.ts b/src/graph/Node.ts index eb9f649..b9e2909 100644 --- a/src/graph/Node.ts +++ b/src/graph/Node.ts @@ -5,6 +5,7 @@ import { getTagStyle } from "src/utils/dataview"; import { Page } from "./Page"; import { ExcalidrawImageElement } from "@zsviczian/excalidraw/types/element/types"; import { isEmbedFileType } from "src/utils/fileUtils"; +import { getEmbeddableDimensions } from "src/utils/embeddableHelper"; export class Node { page: Page; @@ -59,6 +60,7 @@ export class Node { this.style = { ...this.settings.baseNodeStyle, ...x.isInferred?this.settings.inferredNodeStyle:{}, + ...x.page.isURL?this.settings.urlNodeStyle:{}, ...x.page.isVirtual?this.settings.virtualNodeStyle:{}, ...x.isCentral?this.settings.centralNodeStyle:{}, ...x.isSibling?this.settings.siblingNodeStyle:{}, @@ -87,20 +89,25 @@ export class Node { async renderEmbedded():Promise { const ea = this.ea; - const maxDimensions = {width: this.style.embedWidth, height: this.style.embedHeight}; - if(isEmbedFileType(this.page.file, ea)) { + let maxDimensions = {width: this.style.embedWidth, height: this.style.embedHeight}; + if((this.page.file && isEmbedFileType(this.page.file, ea)) || this.page.isURL) { + if(this.page.isURL) { + maxDimensions = getEmbeddableDimensions(this.page.url, maxDimensions); + } this.id = ea.addEmbeddable( this.center.x - maxDimensions.width/2, this.center.y - maxDimensions.height/2, maxDimensions.width, maxDimensions.height, - undefined, - this.page.file + this.page.isURL ? this.page.url : undefined, + this.page.isURL ? undefined : this.page.file ); - const box = ea.getElement(this.id) as any; - box.backgroundColor = this.style.backgroundColor; - box.strokeColor = this.style.borderColor; - box.strokeStyle = this.style.strokeStyle; + const embeddable = ea.getElement(this.id) as any; + //overriding the default link with the full filepath + embeddable.link = this.page.isURL ? this.page.url : `[[${this.page.file.path}]]`; + embeddable.backgroundColor = this.style.backgroundColor; + embeddable.strokeColor = this.style.borderColor; + embeddable.strokeStyle = this.style.strokeStyle; this.embeddedElementIds.push(this.id); return maxDimensions; } else { @@ -112,6 +119,8 @@ export class Node { false, ) const imgEl = ea.getElement(this.id) as Mutable; + //overriding the default link with the full filepath + imgEl.link = `[[${this.page.file.path}]]`; let width = imgEl.width; let height = imgEl.height; @@ -170,7 +179,7 @@ export class Node { } ); const box = ea.getElement(this.id) as any; - box.link = `[[${this.page.file?.path??this.page.path}]]`; + box.link = this.page.isURL ? this.page.url : `[[${this.page.file?.path??this.page.path}]]`; box.backgroundColor = this.style.backgroundColor; box.strokeColor = this.style.borderColor; box.strokeStyle = this.style.strokeStyle; diff --git a/src/graph/Page.ts b/src/graph/Page.ts index 1e32c14..17035fd 100644 --- a/src/graph/Page.ts +++ b/src/graph/Page.ts @@ -63,14 +63,15 @@ export class Page { public plugin: ExcaliBrain, public isFolder: boolean=false, public isTag: boolean=false, - public name?: string + public name?: string, + public url:string = null ) { if(!name) { this.name = file ? (file.extension === "md") ? file.basename : file.name - : getFilenameFromPath(path); + : Boolean(url) ? url : getFilenameFromPath(path); } this.mtime = file ? file.stat.mtime : null; this.neighbours = new Map(); @@ -194,6 +195,12 @@ export class Page { } public getTitle(): string { + if(this.isURL) { + if(this.plugin.settings.renderAlias && this.name && this.name !== "") { + return this.name; + } + return this.url; + } const aliases = (this.file && this.plugin.settings.renderAlias) ? (this.dvPage?.file?.aliases?.values??[]) : []; @@ -253,18 +260,23 @@ export class Page { this.addDVFieldLinksToPage(); this.neighbours.forEach(n=>n.target.addDVFieldLinksToPage()); - const { showVirtualNodes, showAttachments, showFolderNodes, showTagNodes, showPageNodes } = this.plugin.settings + const { showVirtualNodes, showAttachments, showFolderNodes, showTagNodes, showPageNodes, showURLNodes } = this.plugin.settings return Array.from(this.neighbours) .filter(x=> (showVirtualNodes || !x[1].target.isVirtual) && (showAttachments || !x[1].target.isAttachment) && (showFolderNodes || !x[1].target.isFolder) && (showTagNodes || !x[1].target.isTag) && - (showPageNodes || x[1].target.isFolder || x[1].target.isTag || x[1].target.isAttachment) - ) + (showPageNodes || x[1].target.isFolder || x[1].target.isTag || x[1].target.isAttachment || x[1].target.isURL) && + (showURLNodes || !x[1].target.isURL) + ) } public get isVirtual(): boolean { - return (this.file === null) && !this.isFolder && !this.isTag; + return (this.file === null) && !this.isFolder && !this.isTag && !this.isURL; + } + + public get isURL(): boolean { + return Boolean(this.url); } public get isAttachment(): boolean { @@ -439,6 +451,7 @@ export class Page { : null; }; + childrenCount():number { return this.getNeighbours() .reduce((prev,x) => { diff --git a/src/graph/Pages.ts b/src/graph/Pages.ts index a2de8cc..ff1eadd 100644 --- a/src/graph/Pages.ts +++ b/src/graph/Pages.ts @@ -77,22 +77,31 @@ export class Pages { //path case sensitivity issue child = this.pages.get(this.plugin.lowercasePathMap.get(childPath.toLowerCase())); } - if(this.plugin.settings.inferAllLinksAsFriends) { - child.addLeftFriend(parent,RelationType.INFERRED, LinkDirection.FROM); - parent.addLeftFriend(child,RelationType.INFERRED, LinkDirection.TO); - } else { - if(this.plugin.settings.inverseInfer) { //https://github.com/zsviczian/excalibrain/issues/78 - child.addChild(parent,RelationType.INFERRED, LinkDirection.FROM); - parent.addParent(child,RelationType.INFERRED, LinkDirection.TO); - } else { - child.addParent(parent,RelationType.INFERRED, LinkDirection.FROM); - parent.addChild(child,RelationType.INFERRED, LinkDirection.TO); - } - } + this.addInferredParentChild(parent,child); }) }); } + public addPageURLs() { + this.plugin.urlParser.fileToUrlMap.forEach((value, key)=>{ + const page=this.get(key.path); + if(!page) return; + value.forEach(url=>{ + let urlPage = this.get(url.url); + if(!urlPage) { + urlPage = new Page(this,url.url,null,this.plugin,false,false,url.alias,url.url); + this.add(url.url, urlPage); + } + this.addInferredParentChild(page,urlPage); + const originPage = this.get(url.origin); + if(originPage) { + urlPage.addParent(originPage,RelationType.INFERRED,LinkDirection.FROM); + originPage.addChild(urlPage,RelationType.INFERRED,LinkDirection.TO); + } + }) + }) + } + /** * @param page if undefined add unresolved links for all the pages */ diff --git a/src/graph/URLParser.ts b/src/graph/URLParser.ts index eb71b4a..c729c0e 100644 --- a/src/graph/URLParser.ts +++ b/src/graph/URLParser.ts @@ -8,11 +8,6 @@ export interface FileURL { } // Matches links in markdown format [label](url) -//export const linkRegex = /\[([^[\]]+)\]\(((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))\)/gi; -// Matches plain links -//export const plainLinkRegex = /((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/gi; -//export const plainLinkRegex = /((?:(?:ftp|https?|sftp|shttp|tftp):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/gi; - export const linkRegex = /(?:\[([^[\]]+)\]\()((?:(?:ftp|https?|sftp|shttp|tftp):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>"']|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))\)|\b()((?:(?:ftp|https?|sftp|shttp|tftp):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>"']|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))\b/gi; export class URLParser { @@ -125,10 +120,6 @@ export class URLParser { this.plugin.registerEvent(this.app.vault.on('create', modifyEventHandler)); this.plugin.registerEvent(this.app.vault.on('modify', modifyEventHandler)); this.plugin.registerEvent(this.app.vault.on('delete', deleteEventHandler)); - /*this.plugin.registerEvent(this.app.vault.on('rename', async (file:TFile) => { - this.fileToUrlMap.delete(file); - await this.parseFileURLs(file); - }));*/ } } diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 9117e89..a1b1692 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -101,6 +101,8 @@ export default { SHOW_COUNT_DESC: "Show the number of children, parents, friends next to the node gate", ALLOW_AUTOZOOM_NAME: "Autozoom", ALLOW_AUTOZOOM_DESC: "Toggle ON: Allow autozoom
    Toggle OFF: Disable autozoom", + MAX_AUTOZOOM_NAME: "Maximum autozoom level [%]", + MAX_AUTOZOOM_DESC: "Maximum zoom level to apply when autozoom is enabled. The higher the number the more zoomed in the graph will be.", ALLOW_AUTOFOCUS_ON_SEARCH_NAME: "Autofocus on search", ALLOW_AUTOFOCUS_ON_SEARCH_DESC: "Toggle ON: Allow autofocus on Search
    Toggle OFF: Disable autofocus", ALWAYS_ON_TOP_NAME: "Popout default 'always on top' behavior", @@ -142,6 +144,7 @@ export default { NODESTYLE_BASE: "Base node style", NODESTYLE_CENTRAL: "Style of central node", NODESTYLE_INFERRED: "Style of inferred nodes", + NODESTYLE_URL: "Style of web page nodes", NODESTYLE_VIRTUAL: "Style of virtual nodes", NODESTYLE_SIBLING: "Style of sibling nodes", NODESTYLE_ATTACHMENT: "Style of attachment nodes", @@ -183,7 +186,7 @@ export default { SEARCH_IN_VAULT: "Starred items will be listed in empty search.\nSearch for a file, a folder or a tag in your Vault.\nToggle folders and tags on/off to show in the list.", SHOW_HIDE_ATTACHMENTS: "Show/Hide attachments", SHOW_HIDE_VIRTUAL: "Show/Hide virtual nodes", - SHOW_HIDE_INFERRED: "Show/Hide inferred nodes", + SHOW_HIDE_INFERRED: "Show/Hide inferred relationships", SHOW_HIDE_ALIAS: "Show/Hide document alias", SHOW_HIDE_SIBLINGS: "Show/Hide siblings", SHOW_HIDE_EMBEDDEDCENTRAL: "Display central node as embedded frame", @@ -195,4 +198,4 @@ export default { //AddToOntologyModal ADD_TO_ONTOLOGY_MODAL_DESC: "Select the direction of the ontology. If one of the buttons is highlighted, then the field is already part of the ontology in that direction.", -} +} \ No newline at end of file diff --git a/styles.css b/styles.css index be8e9b7..1880aeb 100644 --- a/styles.css +++ b/styles.css @@ -46,7 +46,7 @@ } .excalibrain-toolspanel-wrapper { - z-index: 1; + z-index: 3; position: absolute; top: 10px; padding-left: 10px; @@ -56,7 +56,7 @@ } .excalibrain-history-wrapper { - z-index: 1; + z-index: 3; position: absolute; bottom: 0px; padding-left: 7rem; From 38ab3939dd5655292207757511e0dc7087ce0ba4 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Mon, 31 Jul 2023 23:36:53 +0200 Subject: [PATCH 10/12] hopefully all rollback issues have been resolved --- src/Scene.ts | 29 +- src/Scene2.ts | 985 --------------------------------------------- src/graph/Pages.ts | 3 + 3 files changed, 21 insertions(+), 996 deletions(-) delete mode 100644 src/Scene2.ts diff --git a/src/Scene.ts b/src/Scene.ts index 631a791..87b17c6 100644 --- a/src/Scene.ts +++ b/src/Scene.ts @@ -147,7 +147,7 @@ export class Scene { return; } - const isFile = !(page.isFolder || page.isTag || page.isVirtual); + const isFile = !(page.isFolder || page.isTag || page.isVirtual || page.isURL); if(isFile && !page.file) { this.blockUpdateTimer = false; @@ -169,10 +169,12 @@ export class Scene { keepOnTop(this.plugin.EA); const centralPage = this.getCentralPage(); - const isSameFileAsCurrent = centralPage && isFile && centralPage.file === page.file + const isSameFileAsCurrent = centralPage && + ((isFile && centralPage.file === page.file) || + (page.isURL && centralPage.isURL && centralPage.url === page.url)) // if the file hasn't changed don't update the graph - if(isSameFileAsCurrent && page.file.stat.mtime === centralPage.mtime) { + if(isSameFileAsCurrent && (page.isURL || (page.file.stat.mtime === centralPage.mtime))) { this.blockUpdateTimer = false; return; //don't reload the file if it has not changed } @@ -180,7 +182,7 @@ export class Scene { if(isFile && shouldOpenFile && !settings.embedCentralNode) { const centralLeaf = this.getCentralLeaf(); //@ts-ignore - if(!centralLeaf || !app.workspace.getLeafById(centralLeaf.id)) { + if(!centralLeaf || !this.app.workspace.getLeafById(centralLeaf.id)) { this.centralLeaf = this.ea.openFileInNewOrAdjacentLeaf(page.file); } else { centralLeaf.openFile(page.file, {active: false}); @@ -195,6 +197,11 @@ export class Scene { this.toolsPanel.rerender(); } + if(page.isURL && !settings.showURLNodes) { + settings.showURLNodes = true; + this.toolsPanel.rerender(); + } + if(page.isTag && !settings.showTagNodes) { settings.showTagNodes = true; this.toolsPanel.rerender(); @@ -278,6 +285,7 @@ export class Scene { ...settings.baseNodeStyle, ...settings.centralNodeStyle, }; + style.textColor = settings.baseNodeStyle.textColor; let counter = 0; ea.clear(); @@ -334,7 +342,7 @@ export class Scene { } const frame3 = async () => { if(this.plugin.settings.allowAutozoom) { - api.zoomToFit(null, 5, 0.15); + setTimeout(()=>api.zoomToFit(null, this.plugin.settings.maxZoom, 0.15),100); } ea.targetView.linksAlwaysOpenInANewPane = true; ea.targetView.allowFrameButtonsInViewMode = true; @@ -396,7 +404,7 @@ export class Scene { const ea = this.ea; retainCentralNode = retainCentralNode && Boolean(this.rootNode) && - settings.embedCentralNode && isEmbedFileType(centralPage.file,ea); + settings.embedCentralNode && ((centralPage.file && isEmbedFileType(centralPage.file,ea)) || centralPage.isURL); this.zoomToFitOnNextBrainLeafActivate = !ea.targetView.containerEl.isShown(); @@ -707,11 +715,10 @@ export class Scene { ea.elementsDict = newImagesDict; ea.addElementsToView(false,false); - ea.targetView.clearDirty(); //hack to prevent excalidraw from saving the changes - excalidrawAPI.updateScene({appState: {viewBackgroundColor: settings.backgroundColor}}); - if(settings.allowAutozoom) { - setTimeout(()=>excalidrawAPI.zoomToFit(ea.getViewElements(),5,0.15)); + ea.targetView.clearDirty(); //hack to prevent excalidraw from saving the changes + if(settings.allowAutozoom && !retainCentralNode) { + setTimeout(()=>excalidrawAPI.zoomToFit(ea.getViewElements(),settings.maxZoom,0.15),100); } this.toolsPanel.rerender(); @@ -781,7 +788,7 @@ export class Scene { if(this.zoomToFitOnNextBrainLeafActivate) { this.zoomToFitOnNextBrainLeafActivate = false; if(settings.allowAutozoom) { - this.ea.getExcalidrawAPI().zoomToFit(null, 5, 0.15); + this.ea.getExcalidrawAPI().zoomToFit(null, settings.maxZoom, 0.15); } } this.blockUpdateTimer = false; diff --git a/src/Scene2.ts b/src/Scene2.ts deleted file mode 100644 index 1c11a79..0000000 --- a/src/Scene2.ts +++ /dev/null @@ -1,985 +0,0 @@ -import { App, FileView, Notice, TextFileView, TFile, WorkspaceLeaf } from "obsidian"; -import { ExcalidrawAutomate } from "obsidian-excalidraw-plugin/lib/ExcalidrawAutomate"; -import { EMPTYBRAIN } from "./constants/emptyBrainFile"; -import { Layout } from "./graph/Layout"; -import { Links } from "./graph/Links"; -import { Node } from "./graph/Node"; -import ExcaliBrain from "./excalibrain-main"; -import { ExcaliBrainSettings } from "./Settings"; -import { ToolsPanel } from "./Components/ToolsPanel"; -import { Mutable, Neighbour, RelationType, Role } from "./types"; -import { HistoryPanel } from "./Components/HistoryPanel"; -import { WarningPrompt } from "./utils/Prompts"; -import { keepOnTop } from "./utils/utils"; -import { ExcalidrawElement } from "obsidian-excalidraw-plugin"; -import { isEmbedFileType } from "./utils/fileUtils"; -import { Page } from "./graph/Page"; -import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types" - -export class Scene { - ea: ExcalidrawAutomate; - plugin: ExcaliBrain; - app: App; - leaf: WorkspaceLeaf; - centralPagePath: string; //path of the page in the center of the graph - centralPageFile: TFile; - public centralLeaf: WorkspaceLeaf; //workspace leaf containing the central page - textSize: {width:number, height:number}; - nodeWidth: number; - nodeHeight: number; - public disregardLeafChange: boolean = false; - public terminated: boolean; - public nodesMap: Map = new Map(); - public links: Links; - private layouts: Layout[] = []; - private removeEH: Function; - private removeTimer: Function; - private removeOnCreate: Function; - private removeOnModify: Function; - private removeOnDelete: Function; - private removeOnRename: Function; - private blockUpdateTimer: boolean = false; - public toolsPanel: ToolsPanel; - private historyPanel: HistoryPanel; - public vaultFileChanged: boolean = false; - public pinLeaf: boolean = false; - public focusSearchAfterInitiation: boolean = true; - private zoomToFitOnNextBrainLeafActivate: boolean = false; //this addresses the issue caused in Obsidian 0.16.0 when the brain graph is rendered while the leaf is hidden because tab is not active - private rootNode: Node; - - constructor(plugin: ExcaliBrain, newLeaf: boolean, leaf?: WorkspaceLeaf) { - this.ea = plugin.EA; - this.plugin = plugin; - this.app = plugin.app; - this.leaf = leaf ?? app.workspace.getLeaf(newLeaf); - this.terminated = false; - this.links = new Links(plugin); - } - - public getCentralLeaf(): WorkspaceLeaf { - if(this.plugin.settings.embedCentralNode) { - return null; - } - return this.centralLeaf; - } - - public async initialize(focusSearchAfterInitiation: boolean) { - this.focusSearchAfterInitiation = focusSearchAfterInitiation; - await this.plugin.loadSettings(); - if(!this.leaf?.view) return; - this.toolsPanel = new ToolsPanel((this.leaf.view as TextFileView).contentEl.querySelector(".excalidraw"),this.plugin); - this.initializeScene(); - } - - /** - * Check if ExcaliBrain is currently active - * @returns boolean; true if active - */ - public isActive() { - //@ts-ignore - return !this.terminated && app.workspace.getLeafById(this.leaf?.id) - } - - /** - * Updates the current Scene applying changes in the Index - * @returns - */ - public async reRender(updateIndex:boolean = true) { - if(!this.isActive()) { - return; - } - - if(!this.centralPagePath) { - return; - } - - if(updateIndex) { - this.vaultFileChanged = false; - await this.plugin.createIndex(); //temporary - } - - keepOnTop(this.ea); - const centralPage = this.plugin.pages.get(this.centralPagePath); - if( - centralPage?.file && - !(centralPage.isFolder || centralPage.isTag || centralPage.isVirtual) && - !this.plugin.settings.embedCentralNode - ) { - if(!this.centralLeaf) { - this.ea.openFileInNewOrAdjacentLeaf(centralPage.file); - } else if ( - //@ts-ignore - this.centralLeaf.view?.file?.path !== centralPage.file.path - ) { - this.centralLeaf.openFile(centralPage.file, {active: false}); - } - } - await this.render(this.plugin.settings.embedCentralNode); - } - - private getCentralPage():Page { - //centralPagePath might no longer be valid in case the user changed the filename of the central page - //this is relevant only when the central page is embedded, since if the file is in another leaf the leaf.view.file will - //have the right new path - let centralPage = this.plugin.pages.get(this.centralPagePath) - if(!centralPage && this.centralPageFile) { - this.centralPagePath = this.centralPageFile.path; - centralPage = this.plugin.pages.get(this.centralPageFile.path); - } - return centralPage; - } - - /** - * Renders the ExcaliBrain graph for the file provided by its path - * @param path - * @returns - */ - public async renderGraphForPath(path: string, shouldOpenFile:boolean = true) { - if(!this.isActive()) { - return; - } - - this.blockUpdateTimer = true; //blocks the updateTimer - const settings = this.plugin.settings; - const page = this.plugin.pages.get(path); - if(!page) { - this.blockUpdateTimer = false; - return; - } - - const isFile = !(page.isFolder || page.isTag || page.isVirtual || page.isURL); - - if(isFile && !page.file) { - this.blockUpdateTimer = false; - return; - } - - //abort excalibrain if the file in the Obsidian view has changed - if(!this.ea.targetView?.file || this.ea.targetView.file.path !== settings.excalibrainFilepath) { - this.unloadScene(); - return; - } - - //don't render if the user is trying to render the excaliBrain file itself - if (isFile && (page.file.path === this.ea.targetView.file.path)) { //brainview drawing is the active leaf - this.blockUpdateTimer = false; - return; - } - - keepOnTop(this.plugin.EA); - - const centralPage = this.getCentralPage(); - const isSameFileAsCurrent = centralPage && - ((isFile && centralPage.file === page.file) || - (page.isURL && centralPage.isURL && centralPage.url === page.url)) - - // if the file hasn't changed don't update the graph - if(isSameFileAsCurrent && (page.isURL || (page.file.stat.mtime === centralPage.mtime))) { - this.blockUpdateTimer = false; - return; //don't reload the file if it has not changed - } - - if(isFile && shouldOpenFile && !settings.embedCentralNode) { - const centralLeaf = this.getCentralLeaf(); - //@ts-ignore - if(!centralLeaf || !this.app.workspace.getLeafById(centralLeaf.id)) { - this.centralLeaf = this.ea.openFileInNewOrAdjacentLeaf(page.file); - } else { - centralLeaf.openFile(page.file, {active: false}); - } - this.addToHistory(page.file.path); - } else { - this.addToHistory(page.path); - } - - if(page.isFolder && !settings.showFolderNodes) { - settings.showFolderNodes = true; - this.toolsPanel.rerender(); - } - - if(page.isURL && !settings.showURLNodes) { - settings.showURLNodes = true; - this.toolsPanel.rerender(); - } - - if(page.isTag && !settings.showTagNodes) { - settings.showTagNodes = true; - this.toolsPanel.rerender(); - } - - this.centralPagePath = path; - this.centralPageFile = page.file; - await this.render(isSameFileAsCurrent); - } - - async addToHistory(path: string) { - const nh = this.plugin.navigationHistory; - if(nh.last() === path) { - return; - } - const i = nh.indexOf(path); - if(i>-1) { - nh.splice(i,1); - } - if(nh.length>50) { - nh.shift(); - } - nh.push(path); - } - - public static async openExcalidrawLeaf(ea: ExcalidrawAutomate, settings: ExcaliBrainSettings, leaf: WorkspaceLeaf) { - let counter = 0; - - let file = app.vault.getAbstractFileByPath(settings.excalibrainFilepath); - if(file && !(file instanceof TFile)) { - new Notice(`Please check settings. ExcaliBrain path (${settings.excalibrainFilepath}) points to a folder, not a file`); - return; - } - if(!file) { - file = await app.vault.create(settings.excalibrainFilepath,EMPTYBRAIN); - //an ugly temporary hack waiting for metadataCache to index the new file - while(file instanceof TFile && !ea.isExcalidrawFile(file) && counter++<10) { - await sleep(50); - } - } - counter = 0; - if(file && file instanceof TFile && !ea.isExcalidrawFile(file)) { - (new WarningPrompt( - app, - "⚠ File Exists", - `${file.path} already exists in your Vault. Is it ok to overwrite this file? If not, change ExcaliBrain file path in plugin settings.`) - ).show(async (result: boolean) => { - if(result) { - await app.vault.modify(file as TFile,EMPTYBRAIN); - while(file instanceof TFile && !ea.isExcalidrawFile(file) && counter++<10) { - await sleep(50); - } - Scene.openExcalidrawLeaf(ea, settings, leaf); - } else { - new Notice(`Could not start ExcaliBrain. Please change the ExcaliBrain file path in plugin settings.`); - } - }); - return; - } - if(!leaf) { - leaf = app.workspace.getLeaf(false); - if(leaf.getViewState().type !== "empty") { - leaf = ea.getLeaf(leaf, "new-pane"); - } - } - if(settings.defaultAlwaysOnTop && leaf && ea.DEVICE.isDesktop) { - //@ts-ignore - const ownerWindow = leaf.view?.ownerWindow; - if(ownerWindow && (ownerWindow !== window) && !ownerWindow.electronWindow?.isMaximized()) { - ownerWindow.electronWindow.setAlwaysOnTop(true); - } - } - await leaf.openFile(file as TFile); - } - - public async initializeScene() { - this.disregardLeafChange = false; - const ea = this.ea; - const settings = this.plugin.settings; - const style = { - ...settings.baseNodeStyle, - ...settings.centralNodeStyle, - }; - style.textColor = settings.baseNodeStyle.textColor; - - let counter = 0; - ea.clear(); - ea.setView(this.leaf.view as any); - //delete existing elements from view. The actual delete will happen when addElementsToView is called - //I delete them this way to avoid the splash screen flashing up when the scene is cleared - ea.copyViewElementsToEAforEditing(ea.getViewElements()); - ea.getElements().forEach((el: Mutable)=>el.isDeleted=true); - - while(!ea.targetView.excalidrawAPI && counter++<10) { - await sleep(50); - } - if(!ea.targetView.excalidrawAPI) { - new Notice(`Error initializing Excalidraw view`); - return; - } - - const api = ea.getExcalidrawAPI(); - this.ea.registerThisAsViewEA(); - this.ea.targetView.semaphores.saving = true; //disable saving by setting this Excalidraw flag (not published API) - api.setMobileModeAllowed(false); //disable mobile view https://github.com/zsviczian/excalibrain/issues/9 - ea.style.fontFamily = style.fontFamily; - ea.style.fontSize = style.fontSize; - this.textSize = ea.measureText("m".repeat(style.maxLabelLength)); - this.nodeWidth = this.textSize.width + 2 * style.padding; - if(this.plugin.settings.compactView) { - this.nodeWidth = this.nodeWidth * 0.6; - } - this.nodeHeight = 2 * (this.textSize.height + 2 * style.padding); - - const frame1 = () => { - api.updateScene({ - appState: { - viewModeEnabled:true, - activeTool: { - lastActiveToolBeforeEraser: null, - locked: false, - type: "selection" - }, - theme: "light", - viewBackgroundColor: this.plugin.settings.backgroundColor - } - }); - } - const frame2 = () => { - ea.style.strokeColor = style.textColor; - ea.addText(0,0,"🚀 To get started\nselect a document using the search in the top left or\n" + - "open a document in another pane.\n\n" + - "✨ For the best experience enable 'Open in adjacent pane'\nin Excalidraw settings " + - "under 'Links and Transclusion'.\n\n⚠ ExcaliBrain may need to wait for " + - "DataView to initialize its index.\nThis can take up to a few minutes after starting Obsidian.", {textAlign:"center"}); - ea.addElementsToView(false,false); - ea.targetView.clearDirty(); //hack to prevent excalidraw from saving the changes - } - const frame3 = async () => { - if(this.plugin.settings.allowAutozoom) { - setTimeout(()=>api.zoomToFit(null, this.plugin.settings.maxZoom, 0.15),100); - } - ea.targetView.linksAlwaysOpenInANewPane = true; - ea.targetView.allowFrameButtonsInViewMode = true; - await this.addEventHandler(); - this.historyPanel = new HistoryPanel((this.leaf.view as TextFileView).contentEl.querySelector(".excalidraw"),this.plugin); - new Notice("ExcaliBrain On"); - } - frame1(); - frame2(); - frame3(); - } - - addNodes(x:{ - neighbours:Neighbour[], - layout:Layout, - isCentral:boolean, - isSibling:boolean, - friendGateOnLeft: boolean - }) { - x.neighbours.forEach(n => { - if(n.page.path === this.ea.targetView.file.path) { - return; - } - const node = new Node({ - ea: this.ea, - page: n.page, - isInferred: n.relationType === RelationType.INFERRED, - isCentral: x.isCentral, - isSibling: x.isSibling, - friendGateOnLeft: x.friendGateOnLeft - }); - this.nodesMap.set(n.page.path,node); - x.layout.nodes.push(node); - }); - } - - /** - * if retainCentralNode is true, the central node is not removed from the scene when the scene is rendered - * this will ensure that the embedded frame in the center is not reloaded - * @param retainCentralNode - * @returns - */ - private async render(retainCentralNode:boolean = false) { - if(this.historyPanel) { - this.historyPanel.rerender() - } - if(!this.centralPagePath) return; - const settings = this.plugin.settings; - const isCompactView = settings.compactView; - let centralPage = this.plugin.pages.get(this.centralPagePath); - if(!centralPage) { - //path case sensitivity issue - this.centralPagePath = this.plugin.lowercasePathMap.get(this.centralPagePath.toLowerCase()); - centralPage = this.plugin.pages.get(this.centralPagePath); - if(!centralPage) return; - this.centralPageFile = centralPage.file; - } - - const ea = this.ea; - retainCentralNode = - retainCentralNode && Boolean(this.rootNode) && - settings.embedCentralNode && ((centralPage.file && isEmbedFileType(centralPage.file,ea)) || centralPage.isURL); - - this.zoomToFitOnNextBrainLeafActivate = !ea.targetView.containerEl.isShown(); - - ea.clear(); - const excalidrawAPI = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI; - ea.copyViewElementsToEAforEditing(ea.getViewElements()); - //delete existing elements from view. The actual delete will happen when addElementsToView is called - //I delete them this way to avoid the splash screen flashing up when the scene is cleared - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1248#event-9940972555 - ea.getElements() - .filter((el: ExcalidrawElement)=>!retainCentralNode || !this.rootNode.embeddedElementIds.includes(el.id)) - .forEach((el: Mutable)=>el.isDeleted=true); - ea.style.verticalAlign = "middle"; - - //Extract URLs as child nodes - - //List nodes for the graph - const parents = centralPage.getParents() - .filter(x => - (x.page.path !== centralPage.path) && - !settings.excludeFilepaths.some(p => x.page.path.startsWith(p)) && - (!x.page.primaryStyleTag || !this.toolsPanel.linkTagFilter.selectedTags.has(x.page.primaryStyleTag))) - .slice(0,settings.maxItemCount); - const parentPaths = parents.map(x=>x.page.path); - - const children =centralPage.getChildren() - .filter(x => - (x.page.path !== centralPage.path) && - !settings.excludeFilepaths.some(p => x.page.path.startsWith(p)) && - (!x.page.primaryStyleTag || !this.toolsPanel.linkTagFilter.selectedTags.has(x.page.primaryStyleTag))) - .slice(0,settings.maxItemCount); - - const friends = centralPage.getLeftFriends().concat(centralPage.getPreviousFriends()) - .filter(x => - (x.page.path !== centralPage.path) && - !settings.excludeFilepaths.some(p => x.page.path.startsWith(p)) && - (!x.page.primaryStyleTag || !this.toolsPanel.linkTagFilter.selectedTags.has(x.page.primaryStyleTag))) - .slice(0,settings.maxItemCount); - - const nextFriends = centralPage.getRightFriends().concat(centralPage.getNextFriends()) - .filter(x => - (x.page.path !== centralPage.path) && - !settings.excludeFilepaths.some(p => x.page.path.startsWith(p)) && - (!x.page.primaryStyleTag || !this.toolsPanel.linkTagFilter.selectedTags.has(x.page.primaryStyleTag))) - .slice(0,settings.maxItemCount); - - const rawSiblings = centralPage - .getSiblings() - .filter(s => - //the node is not included already as a parent, child, or friend - !(parents.some(p=>p.page.path === s.page.path) || - children.some(c=>c.page.path === s.page.path) || - friends.some(f=>f.page.path === s.page.path) || - nextFriends.some(f=>f.page.path === s.page.path) || - //or not exluded via folder path in settings - settings.excludeFilepaths.some(p => s.page.path.startsWith(p)) - ) && - //it is not the current central page - (s.page.path !== centralPage.path)); - - const siblings = rawSiblings - .filter(s => - //Only display siblings for which the parents are actually displayed. - //There might be siblings whose parnets have been filtered from view - s.page.getParents().map(x=>x.page.path).some(y=>parentPaths.includes(y)) && - //filter based on primary tag - (!s.page.primaryStyleTag || !this.toolsPanel.linkTagFilter.selectedTags.has(s.page.primaryStyleTag))) - .slice(0,settings.maxItemCount); - - //------------------------------------------------------- - // Generate layout and nodes - this.nodesMap = new Map(); - this.links = new Links(this.plugin); - this.layouts = []; - const manyFriends = friends.length >= 10; - const manyNextFriends = nextFriends.length >= 10; - const baseStyle = settings.baseNodeStyle; - const siblingsCols = siblings.length >= 20 - ? 3 - : siblings.length >= 10 - ? 2 - : 1; - const childrenCols = isCompactView - ? (children.length <= 12 - ? [1, 1, 2, 3, 3, 3, 3, 2, 2, 3, 3, 2, 2][children.length] - : 3) - : (children.length <= 12 - ? [1, 1, 2, 3, 3, 3, 3, 4, 4, 5, 5, 4, 4][children.length] - : 5); - const parentCols = isCompactView - ? (parents.length < 2 - ? 1 - : 2) - : (parents.length < 5 - ? [1, 1, 2, 3, 2][parents.length] - : 3); - - - const isCenterEmbedded = - settings.embedCentralNode && - !centralPage.isVirtual && - !centralPage.isFolder && - !centralPage.isTag; - const centerEmbedWidth = settings.centerEmbedWidth; - const centerEmbedHeight = settings.centerEmbedHeight; - - const lCenter = new Layout({ - origoX: 0, - origoY: isCenterEmbedded - ? centerEmbedHeight - this.nodeHeight/2 - : 0, - top: null, - bottom: null, - columns: 1, - columnWidth: isCenterEmbedded - ? centerEmbedWidth - : this.nodeWidth, - rowHeight: isCenterEmbedded - ? centerEmbedHeight - : this.nodeHeight, - }); - this.layouts.push(lCenter); - - const lChildren = new Layout({ - origoX: 0, - origoY: isCenterEmbedded - ? centerEmbedHeight + 1.5 * this.nodeHeight - : 2.5 * this.nodeHeight, - top: isCenterEmbedded - ? centerEmbedHeight + this.nodeHeight - : 2 * this.nodeHeight, - bottom: null, - columns: childrenCols, - columnWidth: this.nodeWidth, - rowHeight: this.nodeHeight - }); - this.layouts.push(lChildren); - - const friendOrigoX = isCompactView && isCenterEmbedded - ? centerEmbedWidth/2 + this.nodeWidth - : Math.max( - (((manyNextFriends?1:0)+Math.max(childrenCols,parentCols)+1.9)/2.4) * this.nodeWidth, // (manyChildren ? -3 : -2) * this.nodeWidth, - isCenterEmbedded - ? centerEmbedWidth/2 + this.nodeWidth - : 0 - ); - - const lFriends = new Layout({ - origoX: -friendOrigoX, - origoY: isCenterEmbedded - ? centerEmbedHeight/2 - : 0, - top: null, - bottom: null, - columns: 1, - columnWidth: this.nodeWidth, - rowHeight: this.nodeHeight - }); - this.layouts.push(lFriends); - - const lNextFriends = new Layout({ - origoX: friendOrigoX, - origoY: isCenterEmbedded - ? centerEmbedHeight/2 - : 0, - top: null, - bottom: null, - columns: 1, - columnWidth: this.nodeWidth, - rowHeight: this.nodeHeight - }); - this.layouts.push(lNextFriends); - - const lParents = new Layout({ - origoX: 0, - origoY: -2.5 * this.nodeHeight, - top: null, - bottom: -2 * this.nodeHeight, - columns: parentCols, // 3, - columnWidth: this.nodeWidth, - rowHeight: this.nodeHeight - }); - this.layouts.push(lParents); - - const siblingsStyle = settings.siblingNodeStyle; - const siblingsPadding = siblingsStyle.padding??baseStyle.padding; - const siblingsLabelLength = siblingsStyle.maxLabelLength??baseStyle.maxLabelLength; - ea.style.fontFamily = siblingsStyle.fontFamily; - ea.style.fontSize = siblingsStyle.fontSize; - const siblingsTextSize = ea.measureText("m".repeat(siblingsLabelLength+3)); - const siblingsNodeWidth = siblingsTextSize.width + 3 * siblingsPadding; - const siblingsNodeHeight = 2 * (siblingsTextSize.height + 2 * siblingsPadding); - - const lSiblings = new Layout({ - origoX: this.nodeWidth * ((parentCols-1)/2 + (siblingsCols+1.5)/3), - origoY: -2.5 * this.nodeHeight, - top: null, - bottom: - this.nodeHeight/2, - columns: siblingsCols, - columnWidth: siblingsNodeWidth, - rowHeight: siblingsNodeHeight, - }) - this.layouts.push(lSiblings); - - this.rootNode = new Node({ - ea, - page: centralPage, - isInferred: false, - isCentral: true, - isSibling: false, - friendGateOnLeft: true, - isEmbeded: isCenterEmbedded, - embeddedElementIds: retainCentralNode ? this.rootNode?.embeddedElementIds : undefined, - }); - - this.nodesMap.set(centralPage.path,this.rootNode); - lCenter.nodes.push(this.rootNode); - - this.addNodes({ - neighbours: parents, - layout: lParents, - isCentral: false, - isSibling: false, - friendGateOnLeft: true - }); - - this.addNodes({ - neighbours: children, - layout: lChildren, - isCentral: false, - isSibling: false, - friendGateOnLeft: true - }); - - this.addNodes({ - neighbours: friends, - layout: lFriends, - isCentral: false, - isSibling: false, - friendGateOnLeft: false - }); - - this.addNodes({ - neighbours: nextFriends, - layout: lNextFriends, - isCentral: false, - isSibling: false, - friendGateOnLeft: true - }); - - if(settings.renderSiblings) { - this.addNodes({ - neighbours: siblings, - layout: lSiblings, - isCentral: false, - isSibling: true, - friendGateOnLeft: true - }); - } - - //------------------------------------------------------- - // Generate links for all displayed nodes - const addLinks = (nodeA: Node, neighbours:Neighbour[],role: Role) => { - neighbours.forEach(neighbour=>{ - const nodeB = this.nodesMap.get(neighbour.page.path); - if(!nodeB) { - return; - } - this.links.addLink( - nodeA, - nodeB, - role, - neighbour.relationType, - neighbour.typeDefinition, - neighbour.linkDirection, - ea, - settings - ) - }) - } - - Array.from(this.nodesMap.values()).forEach(nodeA => { - addLinks(nodeA, nodeA.page.getChildren(),Role.CHILD); - addLinks(nodeA, nodeA.page.getParents(),Role.PARENT); - addLinks(nodeA, nodeA.page.getLeftFriends(),Role.LEFT); - addLinks(nodeA, nodeA.page.getPreviousFriends(),Role.LEFT); - addLinks(nodeA, nodeA.page.getRightFriends(),Role.RIGHT); - addLinks(nodeA, nodeA.page.getNextFriends(),Role.RIGHT); - }); - - //------------------------------------------------------- - // Render - ea.style.opacity = 100; - await Promise.all(this.layouts.map(async (layout) => await layout.render())); - const nodeElements = ea.getElements(); - this.links.render(Array.from(this.toolsPanel.linkTagFilter.selectedLinks)); - - const linkElements = ea.getElements().filter(el=>!nodeElements.includes(el)); - - - //hack to send link elements behind node elements - const newImagesDict = linkElements.concat(nodeElements) - .reduce((dict:{[key:string]:any}, obj:ExcalidrawElement) => { - dict[obj.id] = obj; - return dict; - }, {}); - - ea.elementsDict = newImagesDict; - - ea.addElementsToView(false,false); - ea.targetView.clearDirty(); //hack to prevent excalidraw from saving the changes - - excalidrawAPI.updateScene({appState: {viewBackgroundColor: settings.backgroundColor}}); - if(settings.allowAutozoom && !retainCentralNode) { - setTimeout(()=>excalidrawAPI.zoomToFit(ea.getViewElements(),settings.maxZoom,0.15),100); - } - - this.toolsPanel.rerender(); - if(this.focusSearchAfterInitiation && settings.allowAutofocuOnSearch) { - this.toolsPanel.searchElement.focus(); - this.focusSearchAfterInitiation = false; - } - - this.blockUpdateTimer = false; - } - - public isCentralLeafStillThere():boolean { - const settings = this.plugin.settings; - //@ts-ignore - const noCentralLeaf = app.workspace.getLeafById(this.centralLeaf.id) === null ; - if(noCentralLeaf) { - return false; - } - //@ts-ignore - if (this.centralLeaf.view?.file?.path === settings.excalibrainFilepath) { - return false; - } - return true; - } - - private async brainEventHandler (leaf:WorkspaceLeaf, startup:boolean = false) { - const settings = this.plugin.settings; - - if(!this.ea.targetView?.file || this.ea.targetView.file.path !== settings.excalibrainFilepath) { - this.unloadScene(); - return; - } - if(this.disregardLeafChange) { - return; - } - - if(!startup && settings.embedCentralNode) { - return; - } - - this.blockUpdateTimer = true; - await sleep(100); - - //------------------------------------------------------- - //terminate event handler if view no longer exists or file has changed - - if(this.pinLeaf && !this.isCentralLeafStillThere()) { - this.pinLeaf = false; - this.toolsPanel.rerender(); - } - - if(this.pinLeaf && leaf !== this.centralLeaf) return; - - if(!(leaf?.view && (leaf.view instanceof FileView) && leaf.view.file)) { - this.blockUpdateTimer = false; - return; - } - - const rootFile = leaf.view.file; - - if (rootFile.path === this.ea.targetView.file.path) { //brainview drawing is the active leaf - if(this.vaultFileChanged) { - this.zoomToFitOnNextBrainLeafActivate = false; - await this.reRender(true); - } - if(this.zoomToFitOnNextBrainLeafActivate) { - this.zoomToFitOnNextBrainLeafActivate = false; - if(settings.allowAutozoom) { - this.ea.getExcalidrawAPI().zoomToFit(null, settings.maxZoom, 0.15); - } - } - this.blockUpdateTimer = false; - return; - } - - const centralPage = this.getCentralPage(); - if( - centralPage && - centralPage.path === rootFile.path && - rootFile.stat.mtime === centralPage.mtime - ) { - this.blockUpdateTimer = false; - return; //don't reload the file if it has not changed - } - - if(!this.plugin.pages.get(rootFile.path)) { - await this.plugin.createIndex(); - } - - this.addToHistory(rootFile.path); - this.centralPagePath = rootFile.path; - this.centralPageFile = rootFile; - this.centralLeaf = leaf; - this.render(); - } - - private async addEventHandler() { - const fileChangeHandler = () => { - this.vaultFileChanged = true; - } - - const beh = (leaf:WorkspaceLeaf)=>this.brainEventHandler(leaf); - this.app.workspace.on("active-leaf-change", beh); - this.removeEH = () => app.workspace.off("active-leaf-change",beh); - this.setTimer(); - this.app.vault.on("rename",fileChangeHandler); - this.removeOnRename = () => app.vault.off("rename",fileChangeHandler) - this.app.vault.on("modify",fileChangeHandler); - this.removeOnModify = () => app.vault.off("modify",fileChangeHandler) - this.app.vault.on("create",fileChangeHandler); - this.removeOnCreate = () => app.vault.off("create",fileChangeHandler) - this.app.vault.on("delete",fileChangeHandler); - this.removeOnDelete = () => app.vault.off("delete",fileChangeHandler) - - const leaves: WorkspaceLeaf[] = []; - app.workspace.iterateAllLeaves(l=>{ - if( (l.view instanceof FileView) && l.view.file && l.view.file.path !== this.ea.targetView.file.path) { - leaves.push(l); - } - }) - - await this.plugin.createIndex(); //temporary - - let leafToOpen = leaves[0]; - if(leaves.length>0) { - const lastFilePath = app.workspace.getLastOpenFiles()[0]; - if(lastFilePath && lastFilePath !== "") { - const leaf = leaves.filter(l=>(l.view as FileView)?.file.path === lastFilePath); - if(leaf.length>0) { - leafToOpen = leaf[0]; - } - } - keepOnTop(this.plugin.EA); - this.brainEventHandler(leafToOpen, true); - } else { - if(this.plugin.navigationHistory.length>0) { - const lastFilePath = this.plugin.navigationHistory.last(); - setTimeout(()=>this.renderGraphForPath(lastFilePath,true),100); - } - } - } - - setTimer() { - const updateTimer = async () => { - if(this.blockUpdateTimer) { - return; - } - if(this.vaultFileChanged) { - this.vaultFileChanged = false; - await this.plugin.createIndex(); - if(this.centralPagePath) { - const centralPage = this.getCentralPage(); - if(!centralPage) { - //@ts-ignore - if(this.centralLeaf && this.centralLeaf.view && this.centralLeaf.view.file) { - //@ts-ignore - this.centralPageFile = this.centralLeaf.view.file; - this.centralPagePath = this.centralPageFile.path; - } - } - } - this.render(true); - } - } - - if(this.removeTimer) { - this.removeTimer(); - this.removeTimer = undefined; - } - - const timer = setInterval(updateTimer,this.plugin.settings.indexUpdateInterval); - this.removeTimer = () => clearInterval(timer); - } - - - public unloadScene(saveSettings:boolean = true, silent: boolean = false) { - if(this.removeEH) { - this.removeEH(); - this.removeEH = undefined; - } - - if(this.removeTimer) { - this.removeTimer(); - this.removeTimer = undefined; - } - - if(this.removeOnRename) { - this.removeOnRename(); - this.removeOnRename = undefined; - } - - if(this.removeOnModify) { - this.removeOnModify(); - this.removeOnModify = undefined; - } - - if(this.removeOnCreate) { - this.removeOnCreate(); - this.removeOnCreate = undefined; - } - - if(this.removeOnDelete) { - this.removeOnDelete(); - this.removeOnDelete = undefined; - } - - if(this.ea.targetView && isBoolean(this.ea.targetView.linksAlwaysOpenInANewPane)) { - this.ea.targetView.linksAlwaysOpenInANewPane = false; - } - - if(this.ea.targetView && isBoolean(this.ea.targetView.allowFrameButtonsInViewMode)) { - this.ea.targetView.allowFrameButtonsInViewMode = false; - } - - if(this.ea.targetView && this.ea.targetView.excalidrawAPI) { - try { - this.ea.targetView.semaphores.saving = false; - this.ea.targetView.excalidrawAPI.setMobileModeAllowed(true); - this.ea.targetView.excalidrawAPI.updateScene({appState:{viewModeEnabled:false}}); - } catch {} - } - //@ts-ignore - if(this.ea.targetView && this.ea.targetView._loaded) { - try { - this.ea.deregisterThisAsViewEA(); - } catch {} - } - // timout is to make sure Obsidian is not being terminated when scene closes, - // becasue that can lead to crippled settings file - // if the plugin is still there after 400ms, it is safe to save the settings - if(saveSettings) { - setTimeout(async () => { - await this.plugin.loadSettings(); //only overwrite the navigation history, save other synchronized settings - this.plugin.settings.navigationHistory = [...this.plugin.navigationHistory]; - await this.plugin.saveSettings(); - },400); - } - this.toolsPanel?.terminate(); - this.toolsPanel = undefined; - this.historyPanel?.terminate(); - this.historyPanel = undefined; - this.ea.targetView = undefined; - this.leaf = undefined; - this.centralLeaf = undefined; - this.centralPagePath = undefined; - this.centralPageFile = undefined; - this.terminated = true; - //@ts-ignore - if(!this.app.plugins.plugins["obsidian-excalidraw-plugin"]) { - this.plugin.EA = null; - } - if(!silent) { - new Notice("Brain Graph Off"); - } - const mostRecentLeaf = this.app.workspace.getMostRecentLeaf(); - if(mostRecentLeaf) { - this.app.workspace.setActiveLeaf( - mostRecentLeaf, - { focus: true }, - ) - } - } -} \ No newline at end of file diff --git a/src/graph/Pages.ts b/src/graph/Pages.ts index ff1eadd..4ca5c6b 100644 --- a/src/graph/Pages.ts +++ b/src/graph/Pages.ts @@ -92,6 +92,9 @@ export class Pages { urlPage = new Page(this,url.url,null,this.plugin,false,false,url.alias,url.url); this.add(url.url, urlPage); } + if(url.alias !== "" && (urlPage.name === "" || urlPage.name === url.url)) { + urlPage.name = url.alias; + } this.addInferredParentChild(page,urlPage); const originPage = this.get(url.origin); if(originPage) { From 20a7a9fd2fe8aa92181915c2c4a8d4f92f7839da Mon Sep 17 00:00:00 2001 From: zsviczian Date: Tue, 1 Aug 2023 15:14:24 +0200 Subject: [PATCH 11/12] styling and placeholders for navigate buttons --- src/Components/ToolsPanel.ts | 65 +++++++++- src/Components/VerticalDivider.ts | 2 + src/lang/locale/en.ts | 3 + styles.css | 195 +++++++++++++++++------------- 4 files changed, 175 insertions(+), 90 deletions(-) create mode 100644 src/Components/VerticalDivider.ts diff --git a/src/Components/ToolsPanel.ts b/src/Components/ToolsPanel.ts index 7da69f8..fd95d5f 100644 --- a/src/Components/ToolsPanel.ts +++ b/src/Components/ToolsPanel.ts @@ -5,12 +5,12 @@ import ExcaliBrain from "src/excalibrain-main"; import { splitFolderAndFilename } from "src/utils/fileUtils"; import { PageSuggest } from "../Suggesters/PageSuggester"; import { LinkTagFilter } from "./LinkTagFilter"; -import { keepOnTop } from "src/utils/utils"; import { getIcon } from "obsidian"; +import { addVerticalDivider } from "./VerticalDivider"; export class ToolsPanel { private wrapperDiv: HTMLDivElement; - private buttons: ToggleButton[] = []; + private buttons: (ToggleButton|HTMLElement)[] = []; public linkTagFilter: LinkTagFilter; public searchElement: HTMLInputElement; @@ -108,6 +108,63 @@ export class ToolsPanel { ) ) + addVerticalDivider(buttonsWrapperDiv); + + //------------ + //Navigate back + //------------ + this.buttons.push( + new ToggleButton( + this.plugin, + ()=>false, + (val:boolean)=>{}, + buttonsWrapperDiv, + { + display: "<", + icon: getIcon("lucide-arrow-big-left").outerHTML, + tooltip: t("NAVIGATE_BACK") + }, + false + ) + ) + + //------------ + //Navigate forward + //------------ + this.buttons.push( + new ToggleButton( + this.plugin, + ()=>false, + (val:boolean)=>{}, + buttonsWrapperDiv, + { + display: ">", + icon: getIcon("lucide-arrow-big-right").outerHTML, + tooltip: t("NAVIGATE_FORWARD") + }, + false + ) + ) + + //------------ + //Refresh view + //------------ + this.buttons.push( + new ToggleButton( + this.plugin, + ()=>false, + (val:boolean)=>this.plugin.scene.pinLeaf = val, + buttonsWrapperDiv, + { + display: "🔄", + icon: getIcon("lucide-refresh-cw").outerHTML, + tooltip: t("REFRESH_VIEW") + }, + false + ) + ) + + addVerticalDivider(buttonsWrapperDiv); //------------ //Attachments //------------ @@ -290,7 +347,9 @@ export class ToolsPanel { } rerender() { - this.buttons.forEach(b=>b.setColor()); + this.buttons.forEach(b => { + if(b instanceof ToggleButton) b.setColor(); + }); this.linkTagFilter.render(); } diff --git a/src/Components/VerticalDivider.ts b/src/Components/VerticalDivider.ts new file mode 100644 index 0000000..6c83599 --- /dev/null +++ b/src/Components/VerticalDivider.ts @@ -0,0 +1,2 @@ +export const addVerticalDivider = (containerEl: HTMLElement):HTMLElement => containerEl.createDiv({ + cls: "excalibrain-toolspanel-divider"}); \ No newline at end of file diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index a1b1692..32581fe 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -195,6 +195,9 @@ export default { SHOW_HIDE_TAG: "Show/Hide tag nodes", SHOW_HIDE_PAGES: "Show/Hide page nodes (incl. defined, inferred, virtual and attachments)", PIN_LEAF: "Link ExcaliBrain to most recent active leaf", + NAVIGATE_BACK: "Navigate back", + NAVIGATE_FORWARD: "Navigate forward", + REFRESH_VIEW: "Refresh", //AddToOntologyModal ADD_TO_ONTOLOGY_MODAL_DESC: "Select the direction of the ontology. If one of the buttons is highlighted, then the field is already part of the ontology in that direction.", diff --git a/styles.css b/styles.css index 1880aeb..643e678 100644 --- a/styles.css +++ b/styles.css @@ -8,52 +8,80 @@ text-align: center; } -.excalibrain-settings-folding-L1 { - font-size: large; - font-weight: bold; - color: var(--text-title-h3); -} - -.excalibrain-settings-h1 { - color: var(--text-title-h1); -} - -.excalibrain-setting-style-section { - padding-left: 30px; - border-left: 10px solid var(--background-modifier-border); -} - -.excalibrain-settings-demoimg { - max-width: 400px; -} - -.excalibrain-setting-nameEl { - min-width: 10em; - max-width: 20em; -} - -.excalibrain-setting-descEl { - min-width: 10em; - max-width: 20em; -} - -.excalibrain-setting-controlEl { - width: 90%; +.excalibrain-contentEl div.Island, +.excalibrain-contentEl button.help-icon { + display:none; } .excalibrain-contentEl { + overflow: hidden !important; position: relative; } -.excalibrain-toolspanel-wrapper { - z-index: 3; - position: absolute; - top: 10px; - padding-left: 10px; - width: 100%; - padding-right: 10px; - pointer-events: none; -} +/* ----------- + TOOLS PANEL + ------------ */ + .excalibrain-toolspanel-divider { + width: 2px; + background-color: var(--default-border-color); + margin-left: 8px; + margin-right: 3px; + } + + .excalibrain-toolspanel-wrapper { + z-index: 3; + position: absolute; + top: 10px; + padding-left: 10px; + /* Set width to auto to fit its content */ + width: auto; + padding-right: 10px; + pointer-events: none; + } + + .excalibrain-dropdown-wrapper, + .excalibrain-searchinput { + /* Adjust flex properties to make them appear in separate rows */ + flex-wrap: wrap; + margin-bottom: 5px; /* Add a small margin to separate the rows */ + } + + .excalibrain-button { + vertical-align: middle; + padding-left: 5px; + padding-right: 5px; + margin-left: 5px !important; + margin-right: 0px !important; + width: 2.4em !important; + } + + .excalibrain-button.off { + background-color: var(--island-bg-color); + } + + .excalibrain-button.on { + background-color: var(--color-primary-darker); + } + + .excalibrain-searchinput { + width: 400px; + vertical-align: middle; + pointer-events: all; + } + + .excalibrain-buttons { + display: flex; /* Change display to flex */ + /* Adjust width to the content */ + width: auto; + margin-top: 5px; + pointer-events: all; + margin-left: -5px; + } + + +/* ----------- + HISTORY + ------------ */ .excalibrain-history-wrapper { z-index: 3; @@ -72,41 +100,55 @@ overflow-x: scroll; } -.excalibrain-button { - vertical-align: middle; - padding-left: 5px; - padding-right: 5px; - margin-left: 5px !important; - margin-right: 0px !important; - width: 2.4em !important; +.excalibrain-history-divider { + color: gold; + margin-left: 5px; + margin-right: 5px; + font-size: smaller; } -.excalibrain-button span { - text-align: center; +.excalibrain-history-item { + cursor: pointer; + color: silver; + font-size: smaller; } -.excalibrain-button.off { - background-color: var(--island-bg-color); +/* ----------- + SETTINGS + ------------ */ +.excalibrain-settings-folding-L1 { + font-size: large; + font-weight: bold; + color: var(--text-title-h3); } -.excalibrain-button.on { - background-color: var(--color-primary-darker); +.excalibrain-settings-h1 { + color: var(--text-title-h1); } -.excalibrain-searchinput { - width: 400px; - vertical-align: middle; - pointer-events: all; +.excalibrain-setting-style-section { + padding-left: 30px; + border-left: 10px solid var(--background-modifier-border); } -.excalibrain-buttons { - display: inline-flex; - width: 100%; - margin-top: 5px; - margin-left: -5px; - pointer-events: all; +.excalibrain-settings-demoimg { + max-width: 400px; +} + +.excalibrain-setting-nameEl { + min-width: 10em; + max-width: 20em; +} + +.excalibrain-setting-descEl { + min-width: 10em; + max-width: 20em; } +.excalibrain-setting-controlEl { + width: 90%; +} + .excalibrain-settings-colorlabel { padding-right: 5px; min-width: 3em; @@ -134,34 +176,13 @@ margin-right: 5px; } -.excalibrain-contentEl div.Island, -.excalibrain-contentEl button.help-icon { - display:none; -} - -.excalibrain-contentEl { - overflow: hidden !important; -} - -.excalibrain-history-divider { - color: gold; - margin-left: 5px; - margin-right: 5px; - font-size: smaller; -} - -.excalibrain-history-item { - cursor: pointer; - color: silver; - font-size: smaller; -} - .excalibrain-dropdown-wrapper { display: inline-flex; } -/*-----------*/ -/*MULTISELECT*/ +/* ----------- + MULTISELECT + ------------ */ .multiselect-container { padding-left: 5px; width:200px; From 87ff4fe4b613f87b4150b05754ca4cf66aadadfb Mon Sep 17 00:00:00 2001 From: zsviczian Date: Tue, 1 Aug 2023 15:40:41 +0200 Subject: [PATCH 12/12] further navigation button placeholders --- src/Components/ToolsPanel.ts | 54 ++++++++++++++++++++++++------------ src/Settings.ts | 2 ++ src/lang/locale/en.ts | 1 + 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/Components/ToolsPanel.ts b/src/Components/ToolsPanel.ts index fd95d5f..7221424 100644 --- a/src/Components/ToolsPanel.ts +++ b/src/Components/ToolsPanel.ts @@ -90,24 +90,6 @@ export class ToolsPanel { }, )); - //------------ - //Pin Leaf - //------------ - this.buttons.push( - new ToggleButton( - this.plugin, - ()=>this.plugin.scene.pinLeaf, - (val:boolean)=>this.plugin.scene.pinLeaf = val, - buttonsWrapperDiv, - { - display: "📌", - icon: getIcon("lucide-pin").outerHTML, - tooltip: t("PIN_LEAF") - }, - false - ) - ) - addVerticalDivider(buttonsWrapperDiv); //------------ @@ -164,6 +146,42 @@ export class ToolsPanel { ) ) + //------------ + //Pin Leaf + //------------ + this.buttons.push( + new ToggleButton( + this.plugin, + ()=>this.plugin.scene.pinLeaf, + (val:boolean)=>this.plugin.scene.pinLeaf = val, + buttonsWrapperDiv, + { + display: "📌", + icon: getIcon("lucide-pin").outerHTML, + tooltip: t("PIN_LEAF") + }, + false + ) + ) + + //------------ + //Automatically open central node in leaf + //------------ + this.buttons.push( + new ToggleButton( + this.plugin, + ()=>this.plugin.settings.autoOpenCentralDocument, + (val:boolean)=>this.plugin.settings.autoOpenCentralDocument = val, + buttonsWrapperDiv, + { + display: "🔌", + icon: ``, //getIcon("lucide-unplug").outerHTML, + tooltip: t("AUTO_OPEN_DOCUMENT") + }, + false + ) + ) + addVerticalDivider(buttonsWrapperDiv); //------------ //Attachments diff --git a/src/Settings.ts b/src/Settings.ts index 0e0c270..d73740f 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -32,6 +32,7 @@ export interface ExcaliBrainSettings { nodeTitleScript: string; backgroundColor: string; excludeFilepaths: string[]; + autoOpenCentralDocument: boolean; showInferredNodes: boolean; showAttachments: boolean; showURLNodes: boolean; @@ -91,6 +92,7 @@ export const DEFAULT_SETTINGS: ExcaliBrainSettings = { nodeTitleScript: "", backgroundColor: "#0c3e6aff", excludeFilepaths: [], + autoOpenCentralDocument: true, showInferredNodes: true, showAttachments: true, showURLNodes: true, diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 32581fe..bfa70aa 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -198,6 +198,7 @@ export default { NAVIGATE_BACK: "Navigate back", NAVIGATE_FORWARD: "Navigate forward", REFRESH_VIEW: "Refresh", + AUTO_OPEN_DOCUMENT: "Auto-open document in other leaf when navigating to graph-node and open graph-node when navigating to document in workspace leaf", //AddToOntologyModal ADD_TO_ONTOLOGY_MODAL_DESC: "Select the direction of the ontology. If one of the buttons is highlighted, then the field is already part of the ontology in that direction.",