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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ npm test

## Shortcuts

The toolbar **Shortcuts** button lets you replace command and grid-scrolling shortcuts or restore each shortcut to its default. Assigning an occupied key moves it to the new command and removes it from the previous command. Changes are applied only after choosing **Save** in the shortcut window; **Cancel** discards them.

- `Ctrl+O`: open file
- `Ctrl+S`: save
- `Ctrl+Shift+S`: save as
Expand All @@ -87,6 +89,9 @@ npm test
- `Ctrl+Plus`: zoom in
- `Ctrl+Minus`: zoom out
- `Ctrl+0`: reset zoom
- `PageUp` / `PageDown`: scroll one grid page vertically
- `Home` / `End`: scroll to the top or bottom of the grid
- `Shift+Home` / `Shift+End`: scroll to the left or right edge of the grid
- `Enter` / `F2`: edit cell
- `Escape`: cancel edit
- `Tab` / `Shift+Tab`: move horizontally after edit
Expand Down
3 changes: 2 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<button class="toggle-button" data-command="toggle-freeze-row">Freeze Row</button>
<button class="toggle-button" data-command="toggle-freeze-column">Freeze Column</button>
<button data-command="open-app-settings">Settings</button>
<button data-command="open-shortcut-settings">Shortcuts</button>
</section>
<section id="gridHost" class="grid-host" tabindex="0" aria-label="Virtual table editor">
<canvas id="gridCanvas"></canvas>
Expand Down Expand Up @@ -79,7 +80,7 @@
<div id="searchPanel" class="modal-backdrop search-backdrop hidden" role="dialog" aria-modal="true" aria-labelledby="searchTitle">
<div class="modal search-modal">
<h2 id="searchTitle">Find</h2>
<input id="searchInput" class="modal-input" placeholder="Search in current table" />
<input id="searchInput" class="modal-input" type="search" placeholder="Search in current table" autocomplete="off" spellcheck="false" />
<fieldset class="search-scope" aria-label="Search scope">
<label><input type="radio" name="searchScope" value="all" checked /> All cells</label>
<label><input type="radio" name="searchScope" value="column-titles" /> Column only</label>
Expand Down
13 changes: 12 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { createLegacyLintController } from "./ui/controllers/legacy-lint-control
import { createLspController } from "./ui/controllers/lsp-controller.js";
import { createSearchController } from "./ui/controllers/search-controller.js";
import { createSettingsController } from "./ui/controllers/settings-controller.js";
import { createShortcutSettingsController } from "./ui/controllers/shortcut-settings-controller.js";
import { createShellController } from "./ui/controllers/shell-controller.js";
const { state, savedTheme, savedGridFont, savedPanelState } = createInitialAppState({ storage: localStorage });
const {
Expand Down Expand Up @@ -235,6 +236,13 @@ const settingsController = createSettingsController({
showError,
escapeHtml
});
const shortcutSettingsController = createShortcutSettingsController({
state,
els,
storage: localStorage,
showToast,
escapeHtml
});
const documentController = createDocumentController({
state,
els,
Expand Down Expand Up @@ -331,6 +339,7 @@ const commandController = createCommandController({
toggleSidebar,
toggleTheme: settingsController.toggleTheme,
showAppSettings: settingsController.showAppSettings,
showShortcutSettings: shortcutSettingsController.showShortcutSettings,
showSettings: settingsController.showSettings,
goToDefinition: lspController.goToDefinition,
loadFixture: documentController.loadFixture,
Expand Down Expand Up @@ -388,6 +397,7 @@ const eventController = createAppEventController({
grid,
commands,
documentController,
hasOpenDocument,
searchController,
syncDockLayout,
wirePaneResizers,
Expand All @@ -410,7 +420,8 @@ const eventController = createAppEventController({
showPalette,
copySelection,
cutSelection,
pasteSelection
pasteSelection,
selectAll
});
renderChrome();
eventController.wireEvents();
Expand Down
113 changes: 113 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1609,6 +1609,119 @@ details:not([open]) .problem-file-header::before {
width: min(520px, calc(100vw - 32px));
}

.shortcut-modal {
width: min(720px, calc(100vw - 32px));
max-height: min(760px, calc(100vh - 32px));
display: flex;
flex-direction: column;
}

.shortcut-settings-hint {
margin: 4px 0 12px;
color: var(--muted);
font-size: 12px;
}

.shortcut-settings-list {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
border: 1px solid var(--line-soft);
border-radius: 7px;
background: var(--surface-1);
}

.shortcut-group h3 {
position: sticky;
top: 0;
z-index: 1;
margin: 0;
padding: 8px 12px;
border-bottom: 1px solid var(--line-soft);
background: var(--surface-3);
color: var(--text-strong);
font-size: 12px;
font-weight: 600;
}

.shortcut-row {
display: grid;
grid-template-columns: minmax(180px, 1fr) minmax(190px, 1.2fr) 64px;
align-items: center;
gap: 8px;
min-height: 42px;
padding: 5px 10px;
border-bottom: 1px solid var(--line-soft);
}

.shortcut-group:last-child .shortcut-row:last-child {
border-bottom: 0;
}

.shortcut-command-label {
min-width: 0;
overflow: hidden;
color: var(--text);
text-overflow: ellipsis;
white-space: nowrap;
}

.shortcut-capture,
.shortcut-default {
height: 30px;
border: 1px solid var(--button-border);
border-radius: 5px;
background: var(--button-bg);
color: var(--text);
}

.shortcut-capture {
padding: 0 10px;
overflow: hidden;
font-family: var(--grid-font);
text-overflow: ellipsis;
white-space: nowrap;
}

.shortcut-capture.recording {
border-color: var(--accent);
outline: 1px solid var(--accent);
color: var(--text-strong);
}

.shortcut-row-message {
grid-column: 1 / -1;
margin: -1px 0 3px;
color: var(--warningText, #d7a56d);
font-size: 12px;
line-height: 1.35;
}

.toolbar button:disabled {
cursor: default;
opacity: .45;
}

.shortcut-modal-actions {
align-items: center;
flex: 0 0 auto;
padding-top: 10px;
}

.shortcut-actions-spacer {
flex: 1;
}

@media (max-width: 620px) {
.shortcut-row {
grid-template-columns: 1fr 64px;
}

.shortcut-command-label {
grid-column: 1 / -1;
}
}

.settings-stack {
display: grid;
gap: 12px;
Expand Down
8 changes: 5 additions & 3 deletions src/ui/app-startup-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { initialSearchState } from "./search-policy.js";
import { normaliseGridFont } from "./app-settings-policy.js";
import { readJsonStorage } from "./app-runtime-utils.js";
import { loadShortcutBindings } from "./shortcut-policy.js";

export function createInitialAppState({ storage = localStorage } = {}) {
const savedTheme = storage.getItem("txteditor.theme") === "light" ? "light" : "dark";
Expand All @@ -29,7 +30,7 @@ export function createInitialAppState({ storage = localStorage } = {}) {
const savedLegacyLintSettings = normalizeLintSettings(readJsonStorage("txteditor.legacyLint.settings", createDefaultLintSettings()));
const savedDockLayout = normalizeDockLayout(readJsonStorage(DOCK_LAYOUT_KEY, DEFAULT_DOCK_LAYOUT));
const savedPanelState = panelStateFromStorage(storage, savedDockLayout);
const savedFreeze = readJsonStorage("txteditor.freeze", {});
const savedShortcuts = loadShortcutBindings(storage);
const state = {
docs: [],
active: 0,
Expand All @@ -43,15 +44,16 @@ export function createInitialAppState({ storage = localStorage } = {}) {
problemsWidth: savedPanelState.problemsWidth,
problemsHeight: savedPanelState.problemsHeight,
dockLayout: savedPanelState.dockLayout,
freezeRow: savedFreeze.row ?? false,
freezeColumn: savedFreeze.column ?? false,
freezeRow: false,
freezeColumn: false,
contextHit: null,
contextMenuActiveGroup: "",
contextMenuOpen: false,
theme: savedTheme,
gridFont: savedGridFont,
colorizeColumns: savedColorize,
vectorLspHover: savedVectorLspHover,
shortcuts: savedShortcuts,
lint: {
engine: savedLintEngine,
enabled: savedLintEnabled,
Expand Down
76 changes: 52 additions & 24 deletions src/ui/canvas-grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { boundedTableExtent, classifyGridHit, classifyPanePoint, classifyResizeH
import { GridMetrics } from "./grid-metrics.js";
import { cellBackground, cellTextColor, createGridRenderStats, initialColumnFitWidth, syncGridThemeFromStyle } from "./grid-render-policy.js";
import { applyColumnSelection, applyRowSelection, applySelectionForHit, hasFullColumnRange, hasFullRowRange, keyboardSelectionTarget } from "./grid-selection-policy.js";
import { applyGridScrollBounds, applyResizeDragState, centeredCellScrollState, centeredScrollOffset as centeredScrollOffsetPolicy, edgeCellScrollState } from "./grid-viewport-policy.js";
import { applyGridScrollBounds, applyResizeDragState, centeredCellScrollState, centeredScrollOffset as centeredScrollOffsetPolicy, clampedGridScrollOffsets, edgeCellScrollState, wheelScrollOffsets } from "./grid-viewport-policy.js";
import { drawGrid, drawGridActiveRowHeaderChrome, drawGridCell, drawGridDiagnosticMarker, drawGridRowHeader, fillGridText } from "./grid/grid-renderer.js";
import {
isGridHoverAllowed,
Expand Down Expand Up @@ -580,16 +580,6 @@ export class CanvasGrid {
onKeyDown(event) {
if (event.target === this.editor || this.editing) return;
const key = event.key;
if (event.ctrlKey && key === "=") return this.zoomByKey(event, 0.1);
if (event.ctrlKey && (key === "+" || key === "Add")) return this.zoomByKey(event, 0.1);
if (event.ctrlKey && (key === "-" || key === "Subtract")) return this.zoomByKey(event, -0.1);
if (event.ctrlKey && key === "0") return this.zoomReset(event);
if (event.ctrlKey && key.toLowerCase() === "a") {
this.selection.selectAll(this.doc.rowCount, this.doc.columnCount);
event.preventDefault();
this.draw();
return;
}
const editStart = keyboardEditStartAction(event);
if (editStart.action === "start-edit") {
event.preventDefault();
Expand Down Expand Up @@ -639,16 +629,6 @@ export class CanvasGrid {
return next;
}

zoomByKey(event, delta) {
event.preventDefault();
this.setZoom(this.doc.zoom + delta);
}

zoomReset(event) {
event.preventDefault();
this.setZoom(1);
}

setZoom(value) {
this.doc.zoom = clamp(Math.round(value * 10) / 10, 0.1, 8);
this.layout();
Expand Down Expand Up @@ -793,7 +773,7 @@ export class CanvasGrid {
if ("scrollTop" in scrollState && !options.preserveScrollTop) this.host.scrollTop = scrollState.scrollTop;
}

scrollCellToCenter(row, column) {
scrollCellToCenter(row, column, options = {}) {
const scrollState = centeredCellScrollState({
row,
column,
Expand All @@ -808,8 +788,56 @@ export class CanvasGrid {
scrollableWidth: this.scrollableColumnWidth(),
scrollableHeight: this.scrollableRowsHeight()
});
if ("scrollLeft" in scrollState) this.host.scrollLeft = scrollState.scrollLeft;
if ("scrollTop" in scrollState) this.host.scrollTop = scrollState.scrollTop;
if ("scrollLeft" in scrollState && !options.preserveScrollLeft) this.host.scrollLeft = scrollState.scrollLeft;
if ("scrollTop" in scrollState && !options.preserveScrollTop) this.host.scrollTop = scrollState.scrollTop;
}

scrollByWheel(event = {}) {
this.scrollToOffsets(wheelScrollOffsets(event, { scrollLeft: this.host.scrollLeft, scrollTop: this.host.scrollTop, lineSize: this.rowHeight, pageWidth: this.host.clientWidth, pageHeight: this.gridPageHeight() }));
}
scrollToTop() {
this.scrollToOffsets({ scrollTop: 0 });
}

scrollToBottom() {
this.scrollToOffsets({ scrollTop: Number.MAX_SAFE_INTEGER });
}

scrollToLeft() {
this.scrollToOffsets({ scrollLeft: 0 });
}

scrollToRight() {
this.scrollToOffsets({ scrollLeft: Number.MAX_SAFE_INTEGER });
}

scrollPageUp() {
this.scrollToOffsets({ scrollTop: this.host.scrollTop - this.gridPageHeight() });
}

scrollPageDown() {
this.scrollToOffsets({ scrollTop: this.host.scrollTop + this.gridPageHeight() });
}

gridPageHeight() {
return Math.max(1, this.host.clientHeight - this.headerHeight - this.frozenRowHeight());
}

scrollToOffsets({ scrollLeft = this.host.scrollLeft, scrollTop = this.host.scrollTop }) {
const next = clampedGridScrollOffsets({
scrollLeft,
scrollTop,
rowHeaderWidth: this.rowHeaderWidth,
headerHeight: this.headerHeight,
frozenColumnWidth: this.frozenColumnWidth(),
frozenRowHeight: this.frozenRowHeight(),
scrollableColumnWidth: this.scrollableColumnWidth(),
scrollableRowsHeight: this.scrollableRowsHeight(),
viewportWidth: this.host.clientWidth,
viewportHeight: this.host.clientHeight
});
this.host.scrollLeft = next.scrollLeft;
this.host.scrollTop = next.scrollTop;
}

statusText() {
Expand Down
3 changes: 3 additions & 0 deletions src/ui/command-registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const COMMAND_LABELS_BASE = [
["toggle-sidebar", "Toggle Explorer"],
["toggle-theme", "Toggle Light/Dark Mode"],
["open-app-settings", "Settings"],
["open-shortcut-settings", "Keyboard Shortcuts"],
["open-settings", "Lint Options"]
];

Expand All @@ -60,6 +61,7 @@ export const COMMANDS_AVAILABLE_WITHOUT_DOCUMENT = new Set([
"open-folder",
"open-settings",
"open-app-settings",
"open-shortcut-settings",
"toggle-sidebar",
"toggle-theme",
"toggle-colorize",
Expand Down Expand Up @@ -124,6 +126,7 @@ const COMMAND_ACTIONS = new Map([
["toggle-sidebar", { type: "handler", name: "toggleSidebar" }],
["toggle-theme", { type: "handler", name: "toggleTheme" }],
["open-app-settings", { type: "handler", name: "showAppSettings" }],
["open-shortcut-settings", { type: "handler", name: "showShortcutSettings" }],
["open-settings", { type: "handler", name: "showSettings" }],
["go-to-definition", { type: "handler", name: "goToDefinition" }]
]);
Expand Down
Loading