Skip to content

Commit

Permalink
New feature: "Focus Lock" for Obsidian 0.15.6+
Browse files Browse the repository at this point in the history
Don't like the sidebar panes stealing your cursor?  Focus lock lets you disable
(temporarily or permanently) Obsidian's new ability to activate sidbar panes.

Your choice is saved with the workspace, so each workspace can be configured
differently.  See the [README file](https://github.com/pjeby/pane-relief#focus-lock) for full details.
  • Loading branch information
pjeby committed Jul 14, 2022
1 parent 939907f commit 669ef63
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 319 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This plugin helps relieve the "pane" of managing lots of panes in [Obsidian.md](
- Commands to move between panes or windows, move panes around, jump to the Nth pane, etc.
- An intelligent pane maximizing command
- Optional [per-pane navigation buttons](#per-pane-navigation-buttons) and [pane numbering](#pane-numbering)
- [Focus lock](#focus-lock) for Obsidian 0.15.6+, to stop sidebar panes stealing focus (NEW in 0.2.1)

The overall goal of these features is to provide a more browser-like Obsidian experience for users that like using lots of panes, windows, and/or Hover Editors.

Expand Down Expand Up @@ -45,6 +46,12 @@ To see the full list of commands and view or change their key assignments, visit

As of version 0.1.6, Pane Relief also includes a "Maximize Active Pane" command that is compatible with the Hover Editor plugin and the popout windows of Obsidian 0.15.3+. If you were previously using the "Maximize Active Pane" plugin, you may wish to switch to disable that plugin and assign the hotkey to Pane Relief's version instead.

### Focus Lock

As of version 0.2.1, Pane Relief allows you to block sidebar panes from receiving focus (and thereby stealing keystrokes or opening links in the wrong pane(s)), using its focus lock function. If you are on Obsidian 0.15.6 or above, a clickable lock symbol appears in the status bar, and a keyboard command is also available to toggle the feature on and off (in case you want to edit a note in your sidebar, use keyboard navigation in the file explorer, etc.)

The toggle's current state is saved with your workspace, so it persists across Obsidian restarts, and if you're using Workspaces or Workspaces Plus, each workspace can have a different state. (Focus lock will default to "off" in new workspaces, so if you want it on in your current workspaces you will have to turn it on in each one the first time.)

## Installation

To install the plugin, open [Pane Relief](https://obsidian.md/plugins?id=pane-relief) in Obsidian's Community Plugins browser, then select "Install" and "Enable".
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "pane-relief",
"name": "Pane Relief",
"version": "0.2.0",
"version": "0.2.1",
"minAppVersion": "0.14.5",
"description": "Per-pane history, hotkeys for pane movement + navigation, and more",
"author": "PJ Eby",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"license": "ISC",
"devDependencies": {
"monkey-around": "^2.3.0",
"obsidian": "0.14.5",
"obsidian": "0.15.4",
"ophidian": "git://github.com/pjeby/ophidian.git"
}
}
68 changes: 18 additions & 50 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

124 changes: 64 additions & 60 deletions src/History.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Notice, TAbstractFile, ViewState, WorkspaceLeaf} from 'obsidian';
import {around} from "monkey-around";
import PaneRelief from "./pane-relief";
import {LayoutStorage, Service, windowEvent} from "ophidian";

const HIST_ATTR = "pane-relief:history-v1";
const SERIAL_PROP = "pane-relief:history-v1";
Expand Down Expand Up @@ -185,71 +185,75 @@ export class History {
}
}

export function installHistory(plugin: PaneRelief) {

// Monkeypatch: include history in leaf serialization (so it's persisted with the workspace)
// and check for popstate events (to suppress them)
plugin.register(around(WorkspaceLeaf.prototype, {
serialize(old) { return function serialize(){
const result = old.call(this);
if (this[HIST_ATTR]) result[SERIAL_PROP] = this[HIST_ATTR].serialize();
return result;
}},
setViewState(old) { return function setViewState(vs, es){
if (vs.popstate && window.event?.type === "popstate") {
return Promise.resolve();
export class HistoryManager extends Service {
onload() {
const store = this.use(LayoutStorage);

this.registerEvent(store.onSaveItem((item, state) => {
if (item instanceof WorkspaceLeaf && item[HIST_ATTR]) {
state[SERIAL_PROP] = item[HIST_ATTR].serialize();
}
return old.call(this, vs, es);
}}
}));

plugin.register(around(app.workspace, {
// Monkeypatch: load history during leaf load, if present
deserializeLayout(old) { return async function deserializeLayout(state, ...etc: any[]){
let result = await old.call(this, state, ...etc);
if (state.type === "leaf") {
if (!result) {
// Retry loading the pane as an empty
state.state.type = 'empty';
result = await old.call(this, state, ...etc);
if (!result) return result;
}
if (state[SERIAL_PROP]) result[HIST_ATTR] = new History(result, state[SERIAL_PROP]);
}));

this.registerEvent(store.onLoadItem((item, state) => {
if (item instanceof WorkspaceLeaf && state[SERIAL_PROP]) {
item[HIST_ATTR] = new History(item, state[SERIAL_PROP]);
}
return result;
}},
}));

// Monkeypatch: check for popstate events (to suppress them)
this.register(around(WorkspaceLeaf.prototype, {
setViewState(old) { return function setViewState(vs, es){
if (vs.popstate && window.event?.type === "popstate") {
return Promise.resolve();
}
return old.call(this, vs, es);
}}
}));

this.register(around(app.workspace, {
// Monkeypatch: keep Obsidian from pushing history in setActiveLeaf
setActiveLeaf(old) { return function setActiveLeaf(leaf, ...etc) {
const unsub = around(this, {
recordHistory(old) { return function (leaf: WorkspaceLeaf, _push: boolean, ...args: any[]) {
// Always update state in place
return old.call(this, leaf, false, ...args);
}; }
setActiveLeaf(old) { return function setActiveLeaf(leaf, ...etc) {
const unsub = around(this, {
recordHistory(old) { return function (leaf: WorkspaceLeaf, _push: boolean, ...args: any[]) {
// Always update state in place
return old.call(this, leaf, false, ...args);
}; }
});
try {
return old.call(this, leaf, ...etc);
} finally {
unsub();
}
}},
}));

function isSyntheticHistoryEvent(button: number) {
return !!windowEvent((_, event) => {
if (event.type === "mousedown" && (event as MouseEvent).button === button) {
event.preventDefault();
event.stopImmediatePropagation();
return true;
}
});
try {
return old.call(this, leaf, ...etc);
} finally {
unsub();
}
}},
}));

// Proxy the window history with a wrapper that delegates to the active leaf's History object,
const realHistory = window.history;
plugin.register(() => (window as any).history = realHistory);
Object.defineProperty(window, "history", { enumerable: true, configurable: true, writable: true, value: {
get state() { return History.current().state; },
get length() { return History.current().length; },
}

back() { if (!plugin.isSyntheticHistoryEvent(3)) this.go(-1); },
forward() { if (!plugin.isSyntheticHistoryEvent(4)) this.go( 1); },
go(by: number) { History.current().go(by); },
// Proxy the window history with a wrapper that delegates to the active leaf's History object,
const realHistory = window.history;
this.register(() => (window as any).history = realHistory);
Object.defineProperty(window, "history", { enumerable: true, configurable: true, writable: true, value: {
get state() { return History.current().state; },
get length() { return History.current().length; },

replaceState(state: PushState, title: string, url: string){ History.current().replaceState(state, title, url); },
pushState(state: PushState, title: string, url: string) { History.current().pushState(state, title, url); },
back() { if (!isSyntheticHistoryEvent(3)) this.go(-1); },
forward() { if (!isSyntheticHistoryEvent(4)) this.go( 1); },
go(by: number) { History.current().go(by); },

get scrollRestoration() { return realHistory.scrollRestoration; },
set scrollRestoration(val) { realHistory.scrollRestoration = val; },
}});
replaceState(state: PushState, title: string, url: string){ History.current().replaceState(state, title, url); },
pushState(state: PushState, title: string, url: string) { History.current().pushState(state, title, url); },

get scrollRestoration() { return realHistory.scrollRestoration; },
set scrollRestoration(val) { realHistory.scrollRestoration = val; },
}});
}
}
12 changes: 4 additions & 8 deletions src/Navigator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {Menu, Keymap, Component, WorkspaceLeaf, TFile, MenuItem} from 'obsidian';
import {domLeaves, History, HistoryEntry} from "./History";
import PaneRelief from './pane-relief';
import {PerWindowComponent} from './PerWindowComponent';
import {use} from "ophidian";
import {PerWindowComponent} from "ophidian";

declare module "obsidian" {
interface Menu {
Expand Down Expand Up @@ -64,8 +62,6 @@ const nonFileViews: Record<string, string[]> = {

export class Navigation extends PerWindowComponent {

plugin = this.use(PaneRelief);

back: Navigator
forward: Navigator
// Set to true while either menu is open, so we don't switch it out
Expand All @@ -85,7 +81,7 @@ export class Navigation extends PerWindowComponent {
leaves() {
const leaves = new Set<WorkspaceLeaf>();
const cb = (leaf: WorkspaceLeaf) => { leaves.add(leaf); };
app.workspace.iterateLeaves(cb, this.root);
app.workspace.iterateLeaves(cb, this.container);

// Support Hover Editors
const popovers = app.plugins.plugins["obsidian-hover-editor"]?.activePopovers;
Expand All @@ -99,7 +95,7 @@ export class Navigation extends PerWindowComponent {

latestLeaf() {
let leaf = app.workspace.activeLeaf;
if (leaf && this.plugin.nav.forLeaf(leaf) === this) return leaf;
if (leaf && this.use(Navigation).forLeaf(leaf) === this) return leaf;
return this.leaves().reduce((best, leaf)=>{ return (!best || best.activeTime < leaf.activeTime) ? leaf : best; }, null);
}

Expand Down Expand Up @@ -359,7 +355,7 @@ export function onElement<K extends keyof HTMLElementEventMap>(
return () => el.off(event, selector, callback, options);
}

function setTooltip(el: HTMLElement, text: string) {
export function setTooltip(el: HTMLElement, text: string) {
if (text) el.setAttribute("aria-label", text || undefined);
else el.removeAttribute("aria-label");
}
Loading

0 comments on commit 669ef63

Please sign in to comment.