diff --git a/package-lock.json b/package-lock.json index 064ddac..6434ae7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "eslint-config-prettier": "8.3.0", "eslint-plugin-prettier": "^4.0.0", "obsidian": "^0.14.6", - "obsidian-excalidraw-plugin": "1.6.26-10", + "obsidian-excalidraw-plugin": "1.6.33", "prettier": "^2.5.1", "rollup": "^2.70.1", "rollup-plugin-terser": "^7.0.2", @@ -4287,9 +4287,9 @@ "dev": true }, "node_modules/@zsviczian/excalidraw": { - "version": "0.11.0-obsidian-16", - "resolved": "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.11.0-obsidian-16.tgz", - "integrity": "sha512-KVCWC7T31tXo6xfXY6AnGsDEl1j7BVwh3eSwoyn4MTS5UbhD5X0rwB8F6Yl1bdxKbrmnqQV98IKLsdgtfuHoVQ==", + "version": "0.11.0-obsidian-20", + "resolved": "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.11.0-obsidian-20.tgz", + "integrity": "sha512-9r/F1q1K/uUFrDeJ81MrzD85deB77HioNkdVRU05IeAUSk7cSImvbKrtrCYefdx2jyICb5j/dY9/PKH8G/hbrA==", "dev": true, "dependencies": { "dotenv": "10.0.0" @@ -12547,13 +12547,13 @@ "integrity": "sha512-xAEnNCT3w2Tg6MA7ly6QqYJvEoY1tm9iIjJ3yMKK9JPlWuRHAMoe5iETwQnx3M9TVbFMfsrBgWKR+IsmswwNjg==" }, "node_modules/obsidian-excalidraw-plugin": { - "version": "1.6.26-10", - "resolved": "https://registry.npmjs.org/obsidian-excalidraw-plugin/-/obsidian-excalidraw-plugin-1.6.26-10.tgz", - "integrity": "sha512-CJml0ibqB/kLx8xXKld89TCANaKZhPxVCZg4/CWZL5ANuLOPtOE4S+GywHVE4yor7ruV0QmQgumpoNWOte0C0A==", + "version": "1.6.33", + "resolved": "https://registry.npmjs.org/obsidian-excalidraw-plugin/-/obsidian-excalidraw-plugin-1.6.33.tgz", + "integrity": "sha512-SBTpXcDyfkDa1F91l3TKPcVVbxU6MlQ2kNSUW3cT0kv4q8kh6yFcBynJ0ZGL0F31tRGz042rlkFaY80/0GdDEw==", "dev": true, "dependencies": { "@types/lz-string": "^1.3.34", - "@zsviczian/excalidraw": "0.11.0-obsidian-16", + "@zsviczian/excalidraw": "0.11.0-obsidian-20", "clsx": "1.1.1", "lz-string": "^1.4.4", "monkey-around": "^2.3.0", @@ -20827,9 +20827,9 @@ "dev": true }, "@zsviczian/excalidraw": { - "version": "0.11.0-obsidian-16", - "resolved": "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.11.0-obsidian-16.tgz", - "integrity": "sha512-KVCWC7T31tXo6xfXY6AnGsDEl1j7BVwh3eSwoyn4MTS5UbhD5X0rwB8F6Yl1bdxKbrmnqQV98IKLsdgtfuHoVQ==", + "version": "0.11.0-obsidian-20", + "resolved": "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.11.0-obsidian-20.tgz", + "integrity": "sha512-9r/F1q1K/uUFrDeJ81MrzD85deB77HioNkdVRU05IeAUSk7cSImvbKrtrCYefdx2jyICb5j/dY9/PKH8G/hbrA==", "dev": true, "requires": { "dotenv": "10.0.0" @@ -26992,13 +26992,13 @@ } }, "obsidian-excalidraw-plugin": { - "version": "1.6.26-10", - "resolved": "https://registry.npmjs.org/obsidian-excalidraw-plugin/-/obsidian-excalidraw-plugin-1.6.26-10.tgz", - "integrity": "sha512-CJml0ibqB/kLx8xXKld89TCANaKZhPxVCZg4/CWZL5ANuLOPtOE4S+GywHVE4yor7ruV0QmQgumpoNWOte0C0A==", + "version": "1.6.33", + "resolved": "https://registry.npmjs.org/obsidian-excalidraw-plugin/-/obsidian-excalidraw-plugin-1.6.33.tgz", + "integrity": "sha512-SBTpXcDyfkDa1F91l3TKPcVVbxU6MlQ2kNSUW3cT0kv4q8kh6yFcBynJ0ZGL0F31tRGz042rlkFaY80/0GdDEw==", "dev": true, "requires": { "@types/lz-string": "^1.3.34", - "@zsviczian/excalidraw": "0.11.0-obsidian-16", + "@zsviczian/excalidraw": "0.11.0-obsidian-20", "clsx": "1.1.1", "lz-string": "^1.4.4", "monkey-around": "^2.3.0", diff --git a/package.json b/package.json index f71380e..7f042d2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "ts-multiselect": "^0.0.10" }, "devDependencies": { - "obsidian-excalidraw-plugin": "1.6.26-10", + "obsidian-excalidraw-plugin": "1.6.33", "@babel/core": "^7.16.12", "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", diff --git a/src/Components/ToolsPanel.ts b/src/Components/ToolsPanel.ts index cd895eb..d124c1d 100644 --- a/src/Components/ToolsPanel.ts +++ b/src/Components/ToolsPanel.ts @@ -11,6 +11,7 @@ export class ToolsPanel { private wrapperDiv: HTMLDivElement; private buttons: ToggleButton[] = []; public linkTagFilter: LinkTagFilter; + public searchElement: HTMLInputElement; constructor( private contentEl: HTMLElement, @@ -45,8 +46,10 @@ export class ToolsPanel { new PageSuggest( this.plugin.app, inputEl, - this.plugin + this.plugin, + contentEl ); + this.searchElement = inputEl; //------- //Filter diff --git a/src/Scene.ts b/src/Scene.ts index f8d4ba6..b152006 100644 --- a/src/Scene.ts +++ b/src/Scene.ts @@ -35,11 +35,12 @@ export class Scene { private removeOnDelete: Function; private removeOnRename: Function; private blockUpdateTimer: boolean = false; - private toolsPanel: ToolsPanel; + public toolsPanel: ToolsPanel; private historyPanel: HistoryPanel; private vaultFileChanged: boolean = false; public pinLeaf: boolean = false; - + public focusSearchAfterInitiation: boolean = true; + constructor(plugin: ExcaliBrain, newLeaf: boolean, leaf?: WorkspaceLeaf) { this.ea = plugin.EA; this.plugin = plugin; @@ -49,10 +50,11 @@ export class Scene { this.links = new Links(plugin); } - public async initialize() { + public async initialize(focusSearchAfterInitiation: boolean) { + this.focusSearchAfterInitiation = focusSearchAfterInitiation; await this.plugin.loadSettings(); - await this.initializeScene(); this.toolsPanel = new ToolsPanel((this.leaf.view as TextFileView).contentEl,this.plugin); + await this.initializeScene(); this.historyPanel = new HistoryPanel((this.leaf.view as TextFileView).contentEl,this.plugin); } @@ -207,7 +209,7 @@ export class Scene { }); return; } - await (leaf ?? app.workspace.getLeaf(true)).openFile(file as TFile); + await (leaf ?? app.workspace.getLeaf(true)).openFile(file as TFile); } public async initializeScene() { @@ -256,7 +258,7 @@ export class Scene { "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"}); await ea.addElementsToView(); - ea.getExcalidrawAPI().zoomToFit(null, 5, 0.1); + ea.getExcalidrawAPI().zoomToFit(null, 5, 0.15); ea.targetView.linksAlwaysOpenInANewPane = true; @@ -482,17 +484,20 @@ export class Scene { // Render this.ea.style.opacity = 100; this.layouts.forEach(layout => layout.render()); + const nodeElements = this.ea.getElements(); this.links.render(Array.from(this.toolsPanel.linkTagFilter.selectedLinks)); - const elements = this.ea.getElements(); + const linkElements = this.ea.getElements().filter(el=>!nodeElements.includes(el)); this.ea.getExcalidrawAPI().updateScene({ - elements: elements.filter( - el=>el.type==="arrow" - ).concat(elements.filter(el=>el.type!=="arrow")) + elements: linkElements.concat(nodeElements) //send link elements behind node elements }) - this.ea.getExcalidrawAPI().zoomToFit(null,5,0.1); + this.ea.getExcalidrawAPI().zoomToFit(null,5,0.15); this.toolsPanel.rerender(); + if(this.focusSearchAfterInitiation) { + this.toolsPanel.searchElement.focus(); + this.focusSearchAfterInitiation = false; + } this.blockUpdateTimer = false; } diff --git a/src/Settings.ts b/src/Settings.ts index fb5690f..437b05c 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -18,11 +18,12 @@ import { Node } from "./graph/Node"; import { svgToBase64 } from "./utils/utils"; import { Arrowhead } from "@zsviczian/excalidraw/types/element/types"; import { Link } from "./graph/Link"; -import { PREDEFINED_LINK_STYLES } from "./constants/constants"; +import { DEFAULT_LINK_STYLE, DEFAULT_NODE_STYLE, PREDEFINED_LINK_STYLES } from "./constants/constants"; export interface ExcaliBrainSettings { excalibrainFilepath: string; hierarchy: Hierarchy; + inferAllLinksAsFriends: boolean; renderAlias: boolean; backgroundColor: string; excludeFilepaths: string[]; @@ -59,6 +60,7 @@ export const DEFAULT_SETTINGS: ExcaliBrainSettings = { children: ["Children", "Child", "down", "d"], friends: ["Friends", "Friend", "Jump", "Jumps", "j"] }, + inferAllLinksAsFriends: false, renderAlias: true, backgroundColor: "#0c3e6aff", excludeFilepaths: [], @@ -70,26 +72,7 @@ export const DEFAULT_SETTINGS: ExcaliBrainSettings = { showPageNodes: true, maxItemCount: 30, renderSiblings: true, - baseNodeStyle: { - prefix: "", - backgroundColor: "#00000066", - fillStyle: "solid", - textColor: "#ffffffff", - borderColor: "#00000000", - fontSize: 20, - fontFamily: 3, - maxLabelLength: 30, - roughness: 0, - strokeShaprness: "round", - strokeWidth: 1, - strokeStyle: "solid", - padding: 10, - gateRadius: 5, - gateOffset: 15, - gateStrokeColor: "#ffffffff", - gateBackgroundColor: "#ffffffff", - gateFillStyle: "solid" - }, + baseNodeStyle: DEFAULT_NODE_STYLE, centralNodeStyle: { fontSize: 30, backgroundColor: "#C49A13FF", @@ -124,14 +107,7 @@ export const DEFAULT_SETTINGS: ExcaliBrainSettings = { }, tagNodeStyles: {}, tagStyleList: [], - baseLinkStyle: { - strokeColor: "#696969FF", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 0, - startArrowHead: "none", - endArrowHead: "none", - }, + baseLinkStyle: DEFAULT_LINK_STYLE, inferredLinkStyle: { strokeStyle: "dashed", }, @@ -488,6 +464,68 @@ export class ExcaliBrainSettingTab extends PluginSettingTab { setDisabled(allowOverride && !toggleComponent.getValue()); } + toggle( + containerEl: HTMLElement, + name: string, + description: string, + getValue:()=>boolean, + setValue:(val:boolean)=>void, + deleteValue:()=>void, + allowOverride: boolean, + defaultValue: boolean, + ) { + let toggleComponent: ToggleComponent; + let valueComponent: ToggleComponent; + + const setting = new Setting(containerEl).setName(name); + + const setDisabled = (isDisabled:boolean) => { + if(isDisabled) { + setting.settingEl.addClass(HIDE_DISABLED_CLASS); + } else { + setting.settingEl.removeClass(HIDE_DISABLED_CLASS); + } + valueComponent.setDisabled(isDisabled); + valueComponent.toggleEl.style.opacity = isDisabled ? "0.3" : "1"; + } + + if(allowOverride) { + setting.addToggle(toggle => { + toggleComponent = toggle; + toggle.toggleEl.addClass("excalibrain-settings-toggle"); + toggle + .setValue(typeof getValue() !== "undefined") + .setTooltip(t("NODESTYLE_INCLUDE_TOGGLE")) + .onChange(value => { + this.dirty = true; + if(!value) { + setDisabled(true); + deleteValue(); + return; + } + setValue(valueComponent.getValue()) + setDisabled(false); + }) + }) + } + + setting.addToggle((toggle) => { + valueComponent = toggle; + toggle + .setValue(getValue()??defaultValue) + .onChange(async (value) => { + setValue(value); + this.dirty = true; + }) + }); + + if(description) { + setting.setDesc(fragWithHTML(description)); + } + + setDisabled(allowOverride && !toggleComponent.getValue()); + } + dropdownpicker( containerEl: HTMLElement, name: string, @@ -949,6 +987,24 @@ export class ExcaliBrainSettingTab extends PluginSettingTab { inheritedStyle.strokeWidth, ) + this.dropdownpicker( + containerEl, + t("LINKSTYLE_ROUGHNESS"), + null, + {0:"Architect",1:"Artist",2:"Cartoonist"}, + () => setting.roughness?.toString(), + (val) => { + setting.roughness = parseInt(val); + this.updateLinkDemoImg(); + }, + ()=> { + delete setting.roughness; + this.updateLinkDemoImg(); + }, + allowOverride, + inheritedStyle.roughness.toString(), + ) + this.dropdownpicker( containerEl, t("LINKSTYLE_STROKE"), @@ -1002,6 +1058,78 @@ export class ExcaliBrainSettingTab extends PluginSettingTab { allowOverride, inheritedStyle.endArrowHead, ) + + this.toggle( + containerEl, + t("LINKSTYLE_SHOWLABEL"), + null, + () => setting.showLabel, + (val) => { + setting.showLabel = val; + this.updateLinkDemoImg(); + }, + () => { + delete setting.showLabel; + this.updateLinkDemoImg(); + }, + allowOverride, + inheritedStyle.showLabel + ) + + this.colorpicker( + containerEl, + t("NODESTYLE_TEXTCOLOR"), + null, + ()=>setting.textColor, + val=> { + setting.textColor=val; + this.updateLinkDemoImg(); + }, + ()=> { + delete setting.textColor; + this.updateLinkDemoImg(); + }, + allowOverride, + inheritedStyle.textColor, + ); + + + this.numberslider( + containerEl, + t("LINKSTYLE_FONTSIZE"), + null, + {min:6,max:30,step:3}, + () => setting.fontSize, + (val) => { + setting.fontSize = val; + this.updateLinkDemoImg(); + }, + ()=> { + delete setting.fontSize; + this.updateLinkDemoImg(); + }, + allowOverride, + inheritedStyle.fontSize, + ) + + this.dropdownpicker( + containerEl, + t("LINKSTYLE_FONTFAMILY"), + null, + {1:"Hand-drawn",2:"Normal",3:"Code",4:"Fourth (custom) Font"}, + () => setting.fontFamily?.toString(), + (val) => { + setting.fontFamily = parseInt(val); + this.updateLinkDemoImg(); + }, + ()=> { + delete setting.fontFamily; + this.updateLinkDemoImg(); + }, + allowOverride, + inheritedStyle.fontFamily.toString(), + ) + } async display() { @@ -1085,7 +1213,7 @@ export class ExcaliBrainSettingTab extends PluginSettingTab { const hierarchyDesc = this.containerEl.createEl("p", {}); hierarchyDesc.innerHTML = t("HIERARCHY_DESC"); - let onHierarchyChange: Function = ()=>{}; + let onHierarchyChange: Function = ()=>{}; new Setting(containerEl) .setName(t("PARENTS_NAME")) .addText((text)=> { @@ -1131,6 +1259,18 @@ export class ExcaliBrainSettingTab extends PluginSettingTab { }) }) + new Setting(containerEl) + .setName(t("INFER_NAME")) + .setDesc(fragWithHTML(t("INFER_DESC"))) + .addToggle(toggle => + toggle + .setValue(this.plugin.settings.inferAllLinksAsFriends) + .onChange(value => { + this.plugin.settings.inferAllLinksAsFriends = value; + this.dirty = true; + }) + ) + this.containerEl.createEl("h1", { cls: "excalibrain-settings-h1", text: t("DISPLAY_HEAD") @@ -1138,7 +1278,7 @@ export class ExcaliBrainSettingTab extends PluginSettingTab { const filepathList = new Setting(containerEl) .setName(t("EXCLUDE_PATHLIST_NAME")) - .setDesc(t("EXCLUDE_PATHLIST_DESC")) + .setDesc(fragWithHTML(t("EXCLUDE_PATHLIST_DESC"))) .addTextArea((text)=> { text.inputEl.style.height = "100px"; text.inputEl.style.width = "100%"; diff --git a/src/Suggesters/PageSuggester.ts b/src/Suggesters/PageSuggester.ts index ae540c5..c8a6da0 100644 --- a/src/Suggesters/PageSuggester.ts +++ b/src/Suggesters/PageSuggester.ts @@ -12,11 +12,12 @@ export enum FileSuggestMode { export class PageSuggest extends TextInputSuggest { constructor( - public app: App, - public inputEl: HTMLInputElement, + app: App, + inputEl: HTMLInputElement, private plugin: ExcaliBrain, + containerEl: HTMLElement ) { - super(app, inputEl); + super(app, inputEl, containerEl); } getSuggestions(inputStr: string): Page[] { @@ -27,7 +28,6 @@ export class PageSuggest extends TextInputSuggest { const lowerInputStr = inputStr.toLowerCase(); //first filter on the name of the file const exactMatchesBasename = this.plugin.pages?.getPages().filter(p=> - !p.isVirtual && (!p.file || (this.plugin.settings.showAttachments || p.file.extension === "md") && (!this.plugin.settings.excludeFilepaths.some(ep=>p.path.startsWith(ep))) @@ -45,7 +45,7 @@ export class PageSuggest extends TextInputSuggest { //extend query with matches based on filepath const exactMatches = exactMatchesBasename.concat( this.plugin.pages?.getPages().filter(p=> - !p.isVirtual && !exactMatchesBasename.contains(p) && + !exactMatchesBasename.contains(p) && (!p.file || (this.plugin.settings.showAttachments || p.file.extension === "md") && (!this.plugin.settings.excludeFilepaths.some(ep=>p.path.startsWith(ep))) diff --git a/src/Suggesters/Suggest.ts b/src/Suggesters/Suggest.ts index a5d2d5a..a92d3cf 100644 --- a/src/Suggesters/Suggest.ts +++ b/src/Suggesters/Suggest.ts @@ -110,20 +110,20 @@ class Suggest { } export abstract class TextInputSuggest implements ISuggestOwner { - protected app: App; - protected inputEl: HTMLInputElement | HTMLTextAreaElement; - private popper: PopperInstance; private scope: Scope; private suggestEl: HTMLElement; private suggest: Suggest; + - constructor(app: App, inputEl: HTMLInputElement | HTMLTextAreaElement) { - this.app = app; - this.inputEl = inputEl; + constructor( + public app: App, + public inputEl: HTMLInputElement | HTMLTextAreaElement, + public containerEl: HTMLElement + ) { this.scope = new Scope(); - this.suggestEl = createDiv("suggestion-container"); + this.suggestEl = containerEl.createDiv("suggestion-container"); const suggestion = this.suggestEl.createDiv("suggestion"); this.suggest = new Suggest(this, suggestion, this.scope); @@ -153,7 +153,7 @@ export abstract class TextInputSuggest implements ISuggestOwner { if (suggestions.length > 0) { this.suggest.setSuggestions(suggestions); // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.open((this.app).dom.appContainerEl, this.inputEl); + this.open(this.containerEl, this.inputEl); //(this.app).dom.appContainerEl } else { this.close(); } diff --git a/src/Types.ts b/src/Types.ts index a551cde..2bbf49b 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -79,6 +79,10 @@ export type LinkStyle = { roughness?: number, startArrowHead?: Arrowhead|"none", endArrowHead?: Arrowhead|"none", + showLabel?: boolean, + fontSize?: number, + fontFamily?: number, + textColor?: string } export type LinkStyleData = { diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 0f876e5..214ebc1 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -1,5 +1,41 @@ +import { NodeStyle, LinkStyle } from "../Types"; + export const APPNAME = "ExcaliBrain"; export const PLUGIN_NAME = "excalibrain" export const MINEXCALIDRAWVERSION = "1.6.28" export const PREDEFINED_LINK_STYLES = ["base","inferred","file-tree","tag-tree"]; -export const SUGGEST_LIMIT = 30; \ No newline at end of file +export const SUGGEST_LIMIT = 30; + +export const DEFAULT_LINK_STYLE:LinkStyle = { + strokeColor: "#696969FF", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 0, + startArrowHead: "none", + endArrowHead: "none", + showLabel: false, + fontSize: 10, + fontFamily: 3, + textColor: "#ffffffff" +} + +export const DEFAULT_NODE_STYLE:NodeStyle = { + prefix: "", + backgroundColor: "#00000066", + fillStyle: "solid", + textColor: "#ffffffff", + borderColor: "#00000000", + fontSize: 20, + fontFamily: 3, + maxLabelLength: 30, + roughness: 0, + strokeShaprness: "round", + strokeWidth: 1, + strokeStyle: "solid", + padding: 10, + gateRadius: 5, + gateOffset: 15, + gateStrokeColor: "#ffffffff", + gateBackgroundColor: "#ffffffff", + gateFillStyle: "solid" +} \ No newline at end of file diff --git a/src/graph/Link.ts b/src/graph/Link.ts index 58d0b98..f3c35b7 100644 --- a/src/graph/Link.ts +++ b/src/graph/Link.ts @@ -25,6 +25,20 @@ export class Link { if(hlist) { hlist.forEach(h=>{ if(!plugin.hierarchyLinkStylesExtended[h]) { + switch(h) { + case "file-tree": + linkstyle = { + ...linkstyle, + ...plugin.settings.folderLinkStyle + }; + break; + case "tag-tree": + linkstyle = { + ...linkstyle, + ...plugin.settings.tagLinkStyle + }; + break; + } return; } linkstyle = { @@ -42,11 +56,11 @@ export class Link { }; } - render(hide: boolean) { const ea = this.ea; const style = this.style; ea.style.strokeStyle = style.strokeStyle; + ea.style.roughness = style.roughness; ea.style.strokeColor = style.strokeColor; ea.style.strokeWidth = style.strokeWidth; ea.style.opacity = hide ? 10 : 100; @@ -66,7 +80,7 @@ export class Link { gateBId = this.nodeB.friendGateId; break; } - ea.connectObjects( + const id = ea.connectObjects( gateAId, null, gateBId, @@ -76,5 +90,11 @@ export class Link { endArrowHead: style.endArrowHead === "none" ? null : style.endArrowHead, } ) + if(style.showLabel && this.hierarchyDefinition) { + ea.style.fontSize = style.fontSize; + ea.style.fontFamily = style.fontFamily; + ea.style.strokeColor = style.textColor; + ea.addLabelToLine(id,this.hierarchyDefinition); + } } } \ No newline at end of file diff --git a/src/graph/Page.ts b/src/graph/Page.ts index 2cf3732..3ee7cf9 100644 --- a/src/graph/Page.ts +++ b/src/graph/Page.ts @@ -1,6 +1,5 @@ import { TFile } from "obsidian"; import ExcaliBrain from "src/main"; -import { ExcaliBrainSettings } from "src/Settings"; import { LinkDirection, Neighbour, Relation, RelationType } from "src/Types"; import { getFilenameFromPath } from "src/utils/fileUtils"; @@ -12,22 +11,6 @@ const DEFAULT_RELATION:Relation = { direction: null } -const getRelationVector = (r:Relation):{ - pi: boolean, - pd: boolean, - ci: boolean, - cd: boolean, - fd: boolean -} => { - return { - pi: r.isParent && r.parentType === RelationType.INFERRED, - pd: r.isParent && r.parentType === RelationType.DEFINED, - ci: r.isChild && r.childType === RelationType.INFERRED, - cd: r.isChild && r.childType === RelationType.DEFINED, - fd: r.isFriend - } -} - const concat = (s1: string, s2: string): string => { return s1 && s2 ? (s1 +", " + s2) @@ -93,6 +76,23 @@ export class Page { : this.name } + private getRelationVector (r:Relation):{ + pi: boolean, + pd: boolean, + ci: boolean, + cd: boolean, + fd: boolean + } { + return { + pi: r.isParent && r.parentType === RelationType.INFERRED, + pd: r.isParent && r.parentType === RelationType.DEFINED, + ci: r.isChild && r.childType === RelationType.INFERRED, + cd: r.isChild && r.childType === RelationType.DEFINED, + fd: (!this.plugin.settings.inferAllLinksAsFriends && r.isFriend) || + (this.plugin.settings.inferAllLinksAsFriends && r.isFriend && !(r.parentType === RelationType.DEFINED || r.childType === RelationType.DEFINED)) + } + } + private getNeighbours(): [string, Relation][] { const { showVirtualNodes, showAttachments, showFolderNodes, showTagNodes, showPageNodes } = this.plugin.settings return Array.from(this.neighbours) @@ -194,7 +194,7 @@ export class Page { //see: getRelationLogic.excalidraw //----------------------------------------------- isChild = (relation: Relation):RelationType => { - const {pi,pd,ci,cd,fd} = getRelationVector(relation); + const {pi,pd,ci,cd,fd} = this.getRelationVector(relation); return (cd && !pd && !fd) ? RelationType.DEFINED : (!pi && !pd && ci && !cd && !fd) @@ -226,7 +226,7 @@ export class Page { } isParent (relation: Relation):RelationType { - const {pi,pd,ci,cd,fd} = getRelationVector(relation); + const {pi,pd,ci,cd,fd} = this.getRelationVector(relation); return (!cd && pd && !fd) ? RelationType.DEFINED : (pi && !pd && !ci && !cd && !fd) @@ -260,7 +260,7 @@ export class Page { } isFriend (relation: Relation):RelationType { - const {pi,pd,ci,cd,fd} = getRelationVector(relation); + const {pi,pd,ci,cd,fd} = this.getRelationVector(relation); return fd ? RelationType.DEFINED : (pi && !pd && ci && !cd && !fd) @@ -287,11 +287,11 @@ export class Page { return { page: x[1].target, relationType: x[1].friendType ?? - (x[1].parentType === RelationType.DEFINED && x[1].childType === RelationType.DEFINED) + ((x[1].parentType === RelationType.DEFINED && x[1].childType === RelationType.DEFINED) //case H ? RelationType.DEFINED //case I - : RelationType.INFERRED, + : RelationType.INFERRED), typeDefinition: x[1].friendTypeDefinition, linkDirection: x[1].direction } diff --git a/src/graph/Pages.ts b/src/graph/Pages.ts index e2274e2..d9bdcde 100644 --- a/src/graph/Pages.ts +++ b/src/graph/Pages.ts @@ -85,8 +85,13 @@ export class Pages { const parent = this.pages.get(parentPath); Object.keys(resolvedLinks[parentPath]).forEach(childPath=>{ const child = this.pages.get(childPath); - child.addParent(parent,RelationType.INFERRED, LinkDirection.TO); - parent.addChild(child,RelationType.INFERRED, LinkDirection.FROM); + if(this.plugin.settings.inferAllLinksAsFriends) { + child.addFriend(parent,RelationType.INFERRED, LinkDirection.FROM); + parent.addFriend(child,RelationType.INFERRED, LinkDirection.TO); + } else { + child.addParent(parent,RelationType.INFERRED, LinkDirection.FROM); + parent.addChild(child,RelationType.INFERRED, LinkDirection.TO); + } }) }); } @@ -109,8 +114,13 @@ export class Pages { } Object.keys(unresolvedLinks[parentPath]).forEach(childPath=>{ const newPage = this.get(childPath) ?? new Page(childPath,null,this.plugin); - newPage.addParent(parent,RelationType.INFERRED, LinkDirection.TO); - parent.addChild(newPage,RelationType.INFERRED, LinkDirection.FROM); + if(this.plugin.settings.inferAllLinksAsFriends) { + newPage.addFriend(parent,RelationType.INFERRED, LinkDirection.FROM); + parent.addFriend(newPage,RelationType.INFERRED, LinkDirection.TO); + } else { + newPage.addParent(parent,RelationType.INFERRED, LinkDirection.FROM); + parent.addChild(newPage,RelationType.INFERRED, LinkDirection.TO); + } this.add(childPath,newPage); }) }); @@ -130,8 +140,8 @@ export class Pages { tag = "tag:" + tag.substring(1); const parent = this.pages.get(tag); if(!parent) return; - page.addParent(parent,RelationType.DEFINED,LinkDirection.TO,"tag-tree"); - parent.addChild(page,RelationType.DEFINED,LinkDirection.FROM,"tag-tree"); + page.addParent(parent,RelationType.DEFINED,LinkDirection.FROM,"tag-tree"); + parent.addChild(page,RelationType.DEFINED,LinkDirection.TO,"tag-tree"); }) const parentFields = this.plugin.hierarchyLowerCase.parents; @@ -141,8 +151,8 @@ export class Pages { log(`Unexpected: ${page.file.path} references ${item.link} in DV, but it was not found in app.metadataCache. The page was skipped.`); return; } - page.addParent(referencedPage,RelationType.DEFINED,LinkDirection.FROM, item.field); - referencedPage.addChild(page,RelationType.DEFINED,LinkDirection.TO, item.field); + page.addParent(referencedPage,RelationType.DEFINED,LinkDirection.TO, item.field); + referencedPage.addChild(page,RelationType.DEFINED,LinkDirection.FROM, item.field); }); const childFields = this.plugin.hierarchyLowerCase.children; getDVFieldLinksForPage(this.plugin,dvPage,childFields).forEach(item=>{ @@ -151,8 +161,8 @@ export class Pages { log(`Unexpected: ${page.file.path} references ${item.link} in DV, but it was not found in app.metadataCache. The page was skipped.`); return; } - page.addChild(referencedPage,RelationType.DEFINED,LinkDirection.FROM, item.field); - referencedPage.addParent(page,RelationType.DEFINED,LinkDirection.TO, item.field); + page.addChild(referencedPage,RelationType.DEFINED,LinkDirection.TO, item.field); + referencedPage.addParent(page,RelationType.DEFINED,LinkDirection.FROM, item.field); }); const friendFields = this.plugin.hierarchyLowerCase.friends; getDVFieldLinksForPage(this.plugin,dvPage,friendFields).forEach(item=>{ @@ -161,8 +171,8 @@ export class Pages { log(`Unexpected: ${page.file.path} references ${item.link} in DV, but it was not found in app.metadataCache. The page was skipped.`); return; } - page.addFriend(referencedPage,RelationType.DEFINED,LinkDirection.FROM,item.field); - referencedPage.addFriend(page,RelationType.DEFINED,LinkDirection.TO, item.field); + page.addFriend(referencedPage,RelationType.DEFINED,LinkDirection.TO,item.field); + referencedPage.addFriend(page,RelationType.DEFINED,LinkDirection.FROM, item.field); }); } } \ No newline at end of file diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 84a8d80..16f1c5c 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -12,6 +12,12 @@ export default { "the automatically generated ExcaliBrain graph.", HIERARCHY_HEAD: "Hierarchy", HIERARCHY_DESC: "Enter the Dataview field names separated by comma (,) that you will use to define link directions in your graph.", + INFER_NAME: "Infer all implicit relationships as Friend", + INFER_DESC: "Toggle On: All implicit links in the document are interpreted as FRIENDS.
" + + "Toggle Off: The following logic is used:
    " + + "
  • A forward link is inferred as a CHILD
  • " + + "
  • A backlink is inferred as a PARENT
  • " + + "
  • If files mutually link to each other, they are FRIENDS
", PARENTS_NAME: "Parents", CHILDREN_NAME: "Children", FRIENDS_NAME: "Friends", @@ -84,6 +90,9 @@ export default { LINKSTYLE_ROUGHNESS: "Roughness", LINKSTYLE_ARROWSTART: "Start arrow head", LINKSTYLE_ARROWEND: "End arrow head", + LINKSTYLE_SHOWLABEL: "Show label on link", + LINKSTYLE_FONTSIZE: "Label font size", + LINKSTYLE_FONTFAMILY: "Label font family", LINKSTYLE_BASE: "Base link style", LINKSTYLE_INFERRED: "Style of inferred link", LINKSTYLE_FOLDER: "Style of folder link", @@ -92,7 +101,11 @@ export default { DATAVIEW_NOT_FOUND: `Dataview plugin not found. Please install or enable Dataview then try restarting ${APPNAME}.`, EXCALIDRAW_NOT_FOUND: `Excalidraw plugin not found. Please install or enable Excalidraw then try restarting ${APPNAME}.`, EXCALIDRAW_MINAPP_VERSION: `ExcaliBrain requires Excalidraw ${MINEXCALIDRAWVERSION} or higher. Please upgrade Excalidraw then try restarting ${APPNAME}.`, - COMMAND_START: "Open ExcaliBrain", + COMMAND_START: "ExcaliBrain Normal", + COMMAND_START_HOVER: "ExcaliBrain Hover-Editor", + //COMMAND_SEARCH: "Search", + COMMAND_STOP: "Stop ExcaliBrain", + HOVER_EDITOR_ERROR: "I am sorry. Something went wrong. Most likely there was a version update to Hover Editor which I haven't addressed properly in ExcaliBrain. Normally I should get this fixed within few days", //ToolsPanel OPEN_DRAWING: "Save snapshot for editing", 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.", diff --git a/src/main.ts b/src/main.ts index 6a81ca6..57e9514 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,16 +4,15 @@ import { DEFAULT_SETTINGS, ExcaliBrainSettings, ExcaliBrainSettingTab } from './ import { errorlog } from './utils/utils'; import { getAPI } from "obsidian-dataview" import { t } from './lang/helpers'; -import { MINEXCALIDRAWVERSION, PLUGIN_NAME, PREDEFINED_LINK_STYLES } from './constants/constants'; +import { DEFAULT_LINK_STYLE, DEFAULT_NODE_STYLE, MINEXCALIDRAWVERSION, PLUGIN_NAME, PREDEFINED_LINK_STYLES } from './constants/constants'; import { DvAPIInterface } from 'obsidian-dataview/lib/typings/api'; import { Pages } from './graph/Pages'; import { getEA } from "obsidian-excalidraw-plugin"; -import { ExcalidrawAutomate } from 'obsidian-excalidraw-plugin/lib/ExcalidrawAutomate'; +import { ExcalidrawAutomate, search } from 'obsidian-excalidraw-plugin/lib/ExcalidrawAutomate'; import { Scene } from './Scene'; import { LinkStyles, NodeStyles, LinkStyle, RelationType, LinkDirection } from './Types'; import { WarningPrompt } from './utils/Prompts'; - declare module "obsidian" { interface App { plugins: { @@ -39,6 +38,7 @@ export default class ExcaliBrain extends Plugin { private disregardLeafChangeTimer: NodeJS.Timeout; private pluginLoaded: boolean = false; public starred: Page[] = []; + private focusSearchAfterInitiation:boolean = false; constructor(app: App, manifest: PluginManifest) { super(app, manifest); @@ -128,15 +128,15 @@ export default class ExcaliBrain extends Plugin { if(f instanceof TFolder) { const child = new Page("folder:"+f.path, null, this, true, false, f.name); this.pages.add("folder:"+f.path,child); - child.addParent(parent,RelationType.DEFINED,LinkDirection.TO,"file-tree"); - parent.addChild(child,RelationType.DEFINED,LinkDirection.FROM,"file-tree"); + child.addParent(parent,RelationType.DEFINED,LinkDirection.FROM,"file-tree"); + parent.addChild(child,RelationType.DEFINED,LinkDirection.TO,"file-tree"); addFolderChildren(f,child); return; } else { const child = new Page(f.path,f as TFile,this); this.pages.add(f.path,child); - child.addParent(parent,RelationType.DEFINED,LinkDirection.TO,"file-tree"); - parent.addChild(child,RelationType.DEFINED,LinkDirection.FROM,"file-tree"); + child.addParent(parent,RelationType.DEFINED,LinkDirection.FROM,"file-tree"); + parent.addChild(child,RelationType.DEFINED,LinkDirection.TO,"file-tree"); } }) } @@ -163,8 +163,8 @@ export default class ExcaliBrain extends Plugin { tagPages.push(child); if(idx>0) { const parent = tagPages[idx-1]; - child.addParent(parent,RelationType.DEFINED,LinkDirection.TO,"tag-tree"); - parent.addChild(child,RelationType.DEFINED,LinkDirection.FROM,"tag-tree"); + child.addParent(parent,RelationType.DEFINED,LinkDirection.FROM,"tag-tree"); + parent.addChild(child,RelationType.DEFINED,LinkDirection.TO,"tag-tree"); } }) }) @@ -225,27 +225,124 @@ export default class ExcaliBrain extends Plugin { return true; } + private revealBrainLeaf() { + if(!this.scene || this.scene.terminated) { + return; + } + app.workspace.revealLeaf(this.scene.leaf); + //@ts-ignore + const hoverEditor = app.plugins.getPlugin("obsidian-hover-editor"); + if(hoverEditor) { + const activeEditor = hoverEditor.activePopovers.filter((he:any) => he.leaves()[0] === this.scene.leaf)[0]; + if(activeEditor) { + if(this.scene.leaf.view.containerEl.offsetHeight === 0) { + activeEditor.titleEl.querySelector("a.popover-action.mod-minimize").click(); + } + } + } + const searchElement = this.scene.toolsPanel?.searchElement; + searchElement?.focus(); + } + private registerCommands() { this.addCommand({ id: "excalibrain-start", name: t("COMMAND_START"), - callback: () => { + callback: async () => { if(!this.excalidrawAvailable()) return; + if(this.scene && !this.scene.terminated) { + this.revealBrainLeaf(); + return; + } + const leaf = this.getBrainLeaf(); + if(leaf) { + this.scene = new Scene(this,true,leaf); + this.scene.initialize(true); + this.revealBrainLeaf(); + return; + } + this.focusSearchAfterInitiation = true; + await Scene.openExcalidrawLeaf(window.ExcalidrawAutomate,this.settings,leaf); + }, + }); + + this.addCommand({ + id: "excalibrain-stop-brain", + name: t("COMMAND_STOP"), + checkCallback: (checking: boolean) => { + if(checking) { + return(this.scene && !this.scene.terminated); + } if(this.scene && !this.scene.terminated) { this.scene.unloadScene(); this.scene = null; - } else { - const leaf = this.getBrainLeaf(); - //@ts-ignore - if(leaf && leaf.view && leaf.view.file && leaf.view.file.path == this.settings.excalibrainFilepath) { - this.scene = new Scene(this,true,leaf); - this.scene.initialize(); - return; + return; + } + } + }) + + this.addCommand({ + id: "excalibrain-open-hover", + name: t("COMMAND_START_HOVER"), + checkCallback: (checking: boolean) => { + //@ts-ignore + const hoverEditor = app.plugins.getPlugin("obsidian-hover-editor"); + if(checking) { + return hoverEditor; + } + if(!this.excalidrawAvailable()) return; + if(this.scene && !this.scene.terminated) { + this.revealBrainLeaf(); + return; + } + try { + //getBrainLeaf will only return one leaf. If there are multiple leaves open, some in hover editors other docked, the + //current logic might miss the open hover editor. However, this is likely an uncommon scenario, thus no + //value in making the logic more sophisticated. + const brainLeaf = this.getBrainLeaf(); + if(brainLeaf) { + const activeEditor = hoverEditor.activePopovers.filter((he:any) => he.leaves()[0] === brainLeaf)[0]; + if(activeEditor) { + app.workspace.revealLeaf(brainLeaf); + if(brainLeaf.view.containerEl.offsetHeight === 0) { //if hover editor is minimized + activeEditor.titleEl.querySelector("a.popover-action.mod-maximize").click(); + } + this.scene = new Scene(this,true,brainLeaf); + this.scene.initialize(true); + return; + } } - Scene.openExcalidrawLeaf(window.ExcalidrawAutomate,this.settings,leaf); + const leaf = hoverEditor.spawnPopover(undefined,()=>{ + this.app.workspace.setActiveLeaf(leaf, false, true); + const activeEditor = hoverEditor.activePopovers.filter((he:any) => he.leaves()[0] === leaf)[0]; + if(!activeEditor) { + new Notice(t("HOVER_EDITOR_ERROR"), 6000); + return false; + } + //@ts-ignore + setTimeout(()=>app.commands.commands["obsidian-hover-editor:snap-active-popover-to-viewport"].checkCallback(false)); + this.focusSearchAfterInitiation = true; + Scene.openExcalidrawLeaf(window.ExcalidrawAutomate,this.settings,leaf); + }); + } catch(e) { + new Notice(t("HOVER_EDITOR_ERROR"), 6000); } - }, - }); + } + }) + + /* + this.addCommand({ + id: "excalibrain-search", + name: t("COMMAND_SEARCH"), + checkCallback: (checking: boolean) => { + if(checking) { + return this.scene && !this.scene.terminated; + } + this.revealBrainLeaf(); + const searchElement = this.scene.toolsPanel?.searchElement; + searchElement?.focus(); + } + })*/ } getBrainLeaf():WorkspaceLeaf { @@ -351,7 +448,15 @@ export default class ExcaliBrain extends Plugin { async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - + this.settings.baseLinkStyle = { + ...DEFAULT_LINK_STYLE, + ...this.settings.baseLinkStyle, + }; + this.settings.baseNodeStyle = { + ...DEFAULT_NODE_STYLE, + ...this.settings.baseNodeStyle, + }; + this.hierarchyLowerCase.parents = []; this.settings.hierarchy.parents.forEach(f=>this.hierarchyLowerCase.parents.push(f.toLowerCase().replaceAll(" ","-"))) this.hierarchyLowerCase.children = []; @@ -505,7 +610,8 @@ export default class ExcaliBrain extends Plugin { return; } this.scene = new Scene(this,true,leaf) - this.scene.initialize(); + this.scene.initialize(this.focusSearchAfterInitiation); + this.focusSearchAfterInitiation = false; } /* private registerDataviewEventHandlers() {