Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,13 @@ Master YASGUI with these keyboard shortcuts for faster querying.
| `F9` | Switch between YASQE and YASR fullscreen |
| `Esc` | Exit fullscreen mode |

### YASGUI Tabs

| Shortcut | Action |
| ---------------------------------------- | --------------------------------------------- |
| `Ctrl+Alt+Tab` / `Cmd+Alt+Tab` | Switch to previously used YASGUI tab |
| `Ctrl+Alt+Shift+Tab` / `Cmd+Alt+Shift+Tab` | Switch to next tab in recently used order |

### General Editor

| Shortcut | Action |
Expand Down
9 changes: 9 additions & 0 deletions packages/yasgui/src/Tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,15 @@ export class Tab extends EventEmitter {
private handleKeyDown = (event: KeyboardEvent) => {
if (event.defaultPrevented) return;

// Ctrl+, → backward (less recently used), Ctrl+Alt+, → forward (more recently used)
// Ctrl+Tab / Ctrl+Shift+Tab cannot be used: the browser intercepts them before keydown fires
const isTabSwitchShortcut = (event.ctrlKey || event.metaKey) && !event.shiftKey && event.key === ",";
if (isTabSwitchShortcut) {
event.preventDefault();
this.yasgui.selectRecentlyUsedTab(event.altKey ? "forward" : "backward");
return;
}

const saveModalOpen = !!document.querySelector(".saveManagedQueryModalOverlay.open");
if (!saveModalOpen) {
const isSaveShortcut =
Expand Down
13 changes: 13 additions & 0 deletions packages/yasgui/src/TabSettingsModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2058,6 +2058,19 @@ export default class TabSettingsModal {
{ keys: ["Esc"], description: "Exit fullscreen mode" },
],
},
{
category: "YASGUI Tabs",
shortcuts: [
{
keys: ["Ctrl+Alt+Tab", "Cmd+Alt+Tab"],
description: "Switch to previously used YASGUI tab",
},
{
keys: ["Ctrl+Alt+Shift+Tab", "Cmd+Alt+Shift+Tab"],
description: "Switch to next YASGUI tab in recently used order",
},
],
},
];

shortcutsData.forEach((section) => {
Expand Down
50 changes: 50 additions & 0 deletions packages/yasgui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { addClass, removeClass } from "@matdata/yasgui-utils";
import GeoPlugin from "yasgui-geo-tg";
import GraphPlugin from "@matdata/yasgui-graph-plugin";
import TablePlugin from "@matdata/yasgui-table-plugin";
import { moveTabIdToFront, removeTabId } from "./tabNavigationHistory";
import "@matdata/yasgui-graph-plugin/dist/yasgui-graph-plugin.min.css";
import "@matdata/yasgui-table-plugin/dist/yasgui-table-plugin.min.css";
import { ThemeManager, Theme } from "./ThemeManager";
Expand Down Expand Up @@ -155,6 +156,9 @@ export class Yasgui extends EventEmitter {
public persistentConfig: PersistentConfig;
public themeManager: ThemeManager;
public queryBrowser: QueryBrowser;
private recentTabIds: string[] = [];
private navigationSnapshot: string[] | null = null;
private navigationCursor = 0;
public static Tab = Tab;
constructor(parent: HTMLElement, config: PartialConfig) {
super();
Expand Down Expand Up @@ -185,6 +189,9 @@ export class Yasgui extends EventEmitter {
this.tabPanelsEl = document.createElement("div");

this.queryBrowser = new QueryBrowser(this);
this.on("tabClose", (_yasgui, tab) => {
this.removeTabFromRecentHistory(tab.getId());
});

this.rootEl.appendChild(this.tabElements.drawTabsList());
this.rootEl.appendChild(this.tabPanelsEl);
Expand Down Expand Up @@ -227,6 +234,7 @@ export class Yasgui extends EventEmitter {
const newTab = this.addTab(true);
this.persistentConfig.setActive(newTab.getId());
this.emit("tabChange", this, newTab);
this.recordTabInRecentHistory(newTab.getId());
} else {
for (const tabId of tabs) {
this._tabs[tabId] = new Tab(this, this.persistentConfig.getTab(tabId));
Expand All @@ -237,6 +245,7 @@ export class Yasgui extends EventEmitter {
const activeTabId = this.persistentConfig.getActiveId();
if (activeTabId) {
this.markTabSelected(activeTabId);
this.recordTabInRecentHistory(activeTabId);
if (executeIdAfterInit && executeIdAfterInit === activeTabId) {
(this.getTab(activeTabId) as Tab).query().catch(() => {});
}
Expand Down Expand Up @@ -300,13 +309,53 @@ export class Yasgui extends EventEmitter {
const tab = this.getTab();
if (tab && tab.getId() !== tabId) {
if (this.markTabSelected(tabId)) {
this.recordTabInRecentHistory(tabId);
//emit
this.emit("tabSelect", this, tabId);
this.persistentConfig.setActive(tabId);
}
}
return tab;
}
private recordTabInRecentHistory(tabId: string) {
// Any non-navigation selection commits the navigation and ends navigation mode.
this.navigationSnapshot = null;
this.recentTabIds = moveTabIdToFront(this.recentTabIds, tabId);
}
private removeTabFromRecentHistory(tabId: string) {
this.recentTabIds = removeTabId(this.recentTabIds, tabId);
}
public selectRecentlyUsedTab(direction: "backward" | "forward" = "backward"): void {
const activeTab = this.getTab();
if (!activeTab) return;
const activeTabId = activeTab.getId();

// Initialize (or re-initialize) the navigation snapshot when starting a new navigation
// sequence or when the active tab no longer matches the cursor position.
// Snapshotting the history order lets repeated presses cycle through all tabs
// instead of ping-ponging between the two most-recently-used ones.
if (this.navigationSnapshot === null || this.navigationSnapshot[this.navigationCursor] !== activeTabId) {
this.navigationSnapshot = [...this.recentTabIds];
const activeIdx = this.navigationSnapshot.indexOf(activeTabId);
this.navigationCursor = activeIdx >= 0 ? activeIdx : 0;
}

if (direction === "backward") {
this.navigationCursor = Math.min(this.navigationCursor + 1, this.navigationSnapshot.length - 1);
} else {
this.navigationCursor = Math.max(this.navigationCursor - 1, 0);
}

const nextTabId = this.navigationSnapshot[this.navigationCursor];
if (!nextTabId || nextTabId === activeTabId) return;

// Select the tab without updating recentTabIds. The history is committed when the
// user performs any non-navigation action that calls recordTabInRecentHistory.
if (this.markTabSelected(nextTabId)) {
this.emit("tabSelect", this, nextTabId);
this.persistentConfig.setActive(nextTabId);
}
}
/**
* Checks if two persistent tab configuration are the same based.
* It isnt a strict equality, as falsy values (e.g. a header that isnt set in one tabjson) isnt taken into consideration
Expand Down Expand Up @@ -417,6 +466,7 @@ export class Yasgui extends EventEmitter {
if (setActive) {
this.persistentConfig.setActive(tabId);
this._tabs[tabId].show();
this.recordTabInRecentHistory(tabId);
}
return this._tabs[tabId];
}
Expand Down
17 changes: 17 additions & 0 deletions packages/yasgui/src/tabNavigationHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const moveTabIdToFront = (tabIds: string[], tabId: string): string[] => {
const idsWithoutTab = tabIds.filter((id) => id !== tabId);
return [tabId, ...idsWithoutTab];
};

export const removeTabId = (tabIds: string[], tabId: string): string[] => tabIds.filter((id) => id !== tabId);

export const getRecentlyUsedTabId = (
tabIds: string[],
activeTabId: string,
direction: "backward" | "forward" = "backward",
): string | undefined => {
const recentlyUsedIds = moveTabIdToFront(tabIds, activeTabId);
const candidateTabIds = recentlyUsedIds.filter((id) => id !== activeTabId);
if (!candidateTabIds.length) return undefined;
return direction === "backward" ? candidateTabIds[0] : candidateTabIds[candidateTabIds.length - 1];
};
30 changes: 30 additions & 0 deletions test/unit/yasgui-tab-navigation-history-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as chai from "chai";
import { describe, it } from "mocha";

import { getRecentlyUsedTabId, moveTabIdToFront, removeTabId } from "../../packages/yasgui/src/tabNavigationHistory.js";

const expect = chai.expect;

describe("Yasgui tab navigation history", () => {
it("moves selected tab to the front of recent history", () => {
expect(moveTabIdToFront(["tab-1", "tab-2", "tab-3"], "tab-2")).to.deep.equal(["tab-2", "tab-1", "tab-3"]);
});

it("returns previously used tab when navigating backward", () => {
const recentTabIds = ["tab-1", "tab-2", "tab-3"];
expect(getRecentlyUsedTabId(recentTabIds, "tab-1", "backward")).to.equal("tab-2");
});

it("returns oldest tab when navigating forward through recent history", () => {
const recentTabIds = ["tab-1", "tab-2", "tab-3"];
expect(getRecentlyUsedTabId(recentTabIds, "tab-1", "forward")).to.equal("tab-3");
});

it("returns undefined when there is no other tab in history", () => {
expect(getRecentlyUsedTabId(["tab-1"], "tab-1", "backward")).to.equal(undefined);
});

it("removes closed tabs from history", () => {
expect(removeTabId(["tab-1", "tab-2", "tab-3"], "tab-2")).to.deep.equal(["tab-1", "tab-3"]);
});
});
Loading