4 Commits
Author SHA1 Message Date
bdeshi 57fccedcdd note about keeping historical buttons 2026-08-06 02:48:31 +06:00
bdeshi 5932bbfa9d look up order in a map 2026-08-02 22:41:38 +06:00
bdeshi 3ac82877f7 two concurrent saves could land out of order 2026-08-02 22:01:00 +06:00
bdeshi d3fb5b4383 let register() handle the teardown 2026-08-02 20:03:55 +06:00
3 changed files with 38 additions and 29 deletions
+35 -27
View File
@@ -4,32 +4,23 @@ module.exports = class RibbonClasp extends Plugin {
async onload() { async onload() {
this.settings = Object.assign({ order: [] }, await this.loadData()); this.settings = Object.assign({ order: [] }, await this.loadData());
this.enforcing = false; this.enforcing = false;
this.alive = true;
this.register(() => { this.alive = false; });
// icons can attach within milliseconds of each other on startup // icons can attach within milliseconds of each other on startup
// (resetTimer, or it fires 200ms after the first one instead of the last) // (resetTimer, or it fires 200ms after the first one instead of the last)
this.debouncedEnforce = debounce(() => this.enforceOrder(), 200, true); this.debouncedEnforce = debounce(() => this.enforceOrder(), 200, true);
// disconnecting the observer doesn't stop an already-scheduled call
this.register(() => this.debouncedEnforce.cancel());
this.app.workspace.onLayoutReady(() => { this.app.workspace.onLayoutReady(() => {
// not tied to our lifecycle; still fires if we were disabled during boot // not tied to our lifecycle; still fires if we were disabled during boot
if (this.unloaded) return; if (!this.alive) return;
this.patchRibbon(); this.patchRibbon();
this.enforceOrder(); this.enforceOrder();
this.observeRibbon(); this.observeRibbon();
}); });
} }
onunload() {
this.unloaded = true;
this.observer?.disconnect();
// disconnect doesn't stop an already-scheduled call
this.debouncedEnforce?.cancel();
const ribbon = this.getRibbon();
// don't restore over someone else's patch, just go inert instead
if (ribbon && ribbon.onChange === this.wrapper) {
ribbon.onChange = this.originalOnChange;
}
}
getRibbon() { getRibbon() {
return this.app.workspace.leftRibbon ?? null; return this.app.workspace.leftRibbon ?? null;
} }
@@ -44,8 +35,9 @@ module.exports = class RibbonClasp extends Plugin {
const containerEl = this.getRibbonContainerEl(ribbon); const containerEl = this.getRibbonContainerEl(ribbon);
if (!containerEl) return; if (!containerEl) return;
this.observer = new MutationObserver(() => this.debouncedEnforce()); const observer = new MutationObserver(() => this.debouncedEnforce());
this.observer.observe(containerEl, { childList: true }); observer.observe(containerEl, { childList: true });
this.register(() => observer.disconnect());
} }
// hook the ribbon's own onChange so drags and hide/show toggles update // hook the ribbon's own onChange so drags and hide/show toggles update
@@ -57,13 +49,18 @@ module.exports = class RibbonClasp extends Plugin {
const original = ribbon.onChange.bind(ribbon); const original = ribbon.onChange.bind(ribbon);
this.originalOnChange = original; this.originalOnChange = original;
this.wrapper = (save) => { const wrapper = (save) => {
original(save); original(save);
if (save && !this.enforcing && !this.unloaded) { if (save && !this.enforcing && this.alive) {
this.recordOrder(ribbon); this.recordOrder(ribbon);
} }
}; };
ribbon.onChange = this.wrapper; ribbon.onChange = wrapper;
this.register(() => {
// don't restore over someone else's patch, just go inert instead
if (ribbon.onChange === wrapper) ribbon.onChange = original;
});
} }
enforceOrder() { enforceOrder() {
@@ -76,13 +73,12 @@ module.exports = class RibbonClasp extends Plugin {
return; return;
} }
// anything we haven't seen before sorts to the bottom, in the order it came in
const rank = new Map(order.map((id, i) => [id, i]));
const sorted = [...ribbon.items].sort((a, b) => { const sorted = [...ribbon.items].sort((a, b) => {
const ia = order.indexOf(a.id); const ia = rank.get(a.id) ?? Infinity;
const ib = order.indexOf(b.id); const ib = rank.get(b.id) ?? Infinity;
if (ia === -1 && ib === -1) return 0; return ia === ib ? 0 : ia - ib;
if (ia === -1) return 1;
if (ib === -1) return -1;
return ia - ib;
}); });
const changed = sorted.some((item, i) => ribbon.items[i] !== item); const changed = sorted.some((item, i) => ribbon.items[i] !== item);
@@ -100,13 +96,25 @@ module.exports = class RibbonClasp extends Plugin {
this.mergeNewItems(ribbon); this.mergeNewItems(ribbon);
} }
// a drag saves, and the dom change it makes wakes the observer up, which can
// save again before the first one has finished writing. queue them instead.
save() {
this.writing = Promise.resolve(this.writing)
.catch(() => {})
.then(() => this.saveData(this.settings));
return this.writing;
}
recordOrder(ribbon) { recordOrder(ribbon) {
const ids = ribbon.items.map((item) => item.id); const ids = ribbon.items.map((item) => item.id);
if (this.sameOrder(ids, this.settings.order)) return; if (this.sameOrder(ids, this.settings.order)) return;
this.settings.order = ids; this.settings.order = ids;
this.saveData(this.settings); this.save();
} }
// ids of plugins that are gone stay in the list on purpose -- nothing prunes
// them, so reinstalling one puts its icon back where it used to be
//
// only append icons we haven't seen before instead of overwriting the // only append icons we haven't seen before instead of overwriting the
// whole order -- enforceOrder() runs during boot too, before every // whole order -- enforceOrder() runs during boot too, before every
// plugin has attached its icon yet, and a full overwrite there was // plugin has attached its icon yet, and a full overwrite there was
@@ -116,7 +124,7 @@ module.exports = class RibbonClasp extends Plugin {
const newIds = ribbon.items.map((item) => item.id).filter((id) => !known.has(id)); const newIds = ribbon.items.map((item) => item.id).filter((id) => !known.has(id));
if (newIds.length === 0) return; if (newIds.length === 0) return;
this.settings.order = [...this.settings.order, ...newIds]; this.settings.order = [...this.settings.order, ...newIds];
this.saveData(this.settings); this.save();
} }
sameOrder(a, b) { sameOrder(a, b) {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "ribbon-clasp", "id": "ribbon-clasp",
"name": "Ribbon Clasp", "name": "Ribbon Clasp",
"version": "1.0.0", "version": "1.1.0",
"minAppVersion": "1.1.0", "minAppVersion": "1.1.0",
"description": "Stops the left ribbon icons from reshuffling themselves.", "description": "Stops the left ribbon icons from reshuffling themselves.",
"author": "bdeshi", "author": "bdeshi",
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"1.0.0": "1.1.0" "1.0.0": "1.1.0",
"1.1.0": "1.1.0"
} }