Skip to content

Commit 7ebd121

Browse files
authored
feat: Add support for reversing info panel auto-scroll direction while holding Command+Option keys (#3220)
1 parent d39b8d3 commit 7ebd121

4 files changed

Lines changed: 263 additions & 17 deletions

File tree

src/dashboard/Dashboard.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import DashboardSettings from './Settings/DashboardSettings/DashboardSettings.re
5757
import Security from './Settings/Security/Security.react';
5858
import KeyboardShortcutsSettings from './Settings/KeyboardShortcutsSettings.react';
5959
import CloudConfigSettings from './Settings/CloudConfigSettings.react';
60+
import DataBrowserSettings from './Settings/DataBrowserSettings.react';
6061
import semver from 'semver';
6162
import packageInfo from '../../package.json';
6263

@@ -239,6 +240,7 @@ export default class Dashboard extends React.Component {
239240
<Route path="security" element={<Security />} />
240241
<Route path="keyboard-shortcuts" element={<KeyboardShortcutsSettings />} />
241242
<Route path="cloud-config" element={<CloudConfigSettings />} />
243+
<Route path="data-browser" element={<DataBrowserSettings />} />
242244
<Route path="general" element={<GeneralSettings />} />
243245
<Route path="keys" element={<SecuritySettings />} />
244246
<Route path="users" element={<UsersSettings />} />

src/dashboard/DashboardView.react.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ export default class DashboardView extends React.Component {
226226
name: 'Dashboard',
227227
link: '/settings/dashboard',
228228
},
229+
{
230+
name: 'Data Browser',
231+
link: '/settings/data-browser',
232+
},
229233
{
230234
name: 'Cloud Config',
231235
link: '/settings/cloud-config',

src/dashboard/Data/Browser/DataBrowser.react.js

Lines changed: 102 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { ResizableBox } from 'react-resizable';
2323
import ScriptConfirmationModal from '../../../components/ScriptConfirmationModal/ScriptConfirmationModal.react';
2424
import styles from './Databrowser.scss';
2525
import KeyboardShortcutsManager, { matchesShortcut } from 'lib/KeyboardShortcutsPreferences';
26+
import ServerConfigStorage from 'lib/ServerConfigStorage';
2627

2728
import AggregationPanel from '../../../components/AggregationPanel/AggregationPanel';
2829
import { buildRelatedTextFieldsMenuItem } from '../../../lib/RelatedRecordsUtils';
@@ -199,6 +200,8 @@ export default class DataBrowser extends React.Component {
199200
mouseOverPanelHeader: false, // Whether the mouse is over the panel header row
200201
commandKeyPressed: false, // Whether the Command/Meta key is currently pressed
201202
optionKeyPressed: false, // Whether the Option/Alt key is currently pressed (pauses auto-scroll)
203+
reverseAutoScrollActive: false, // Whether Cmd+Option are both held (reverses auto-scroll direction)
204+
reverseAutoScrollSpeedFactor: 1, // Speed multiplier for reverse auto-scroll
202205
};
203206

204207
// Flag to skip panel clearing in componentDidUpdate during selective object refresh
@@ -265,6 +268,7 @@ export default class DataBrowser extends React.Component {
265268
this.handlePanelHeaderMouseLeave = this.handlePanelHeaderMouseLeave.bind(this);
266269
this.handleOptionKeyDown = this.handleOptionKeyDown.bind(this);
267270
this.handleOptionKeyUp = this.handleOptionKeyUp.bind(this);
271+
this.handleWindowBlur = this.handleWindowBlur.bind(this);
268272
this.handleMouseButtonDown = this.handleMouseButtonDown.bind(this);
269273
this.handleMouseButtonUp = this.handleMouseButtonUp.bind(this);
270274
this.saveOrderTimeout = null;
@@ -366,6 +370,7 @@ export default class DataBrowser extends React.Component {
366370
window.addEventListener('mouseup', this.handleMouseButtonUp);
367371
// Native context menu detection for auto-scroll pause
368372
this.nativeContextMenuTracker = this.setupNativeContextMenuDetection();
373+
window.addEventListener('blur', this.handleWindowBlur);
369374

370375
// Load keyboard shortcuts from server
371376
try {
@@ -376,6 +381,20 @@ export default class DataBrowser extends React.Component {
376381
console.warn('Failed to load keyboard shortcuts:', error);
377382
}
378383

384+
// Load data browser settings from server
385+
try {
386+
const serverStorage = new ServerConfigStorage(this.props.app);
387+
const panelSettings = await serverStorage.getConfig(
388+
'browser.panels.settings',
389+
this.props.app.applicationId
390+
);
391+
if (panelSettings !== null && typeof panelSettings === 'object' && typeof panelSettings.reverseAutoScrollSpeedFactor === 'number' && panelSettings.reverseAutoScrollSpeedFactor > 0) {
392+
this.setState({ reverseAutoScrollSpeedFactor: panelSettings.reverseAutoScrollSpeedFactor });
393+
}
394+
} catch (error) {
395+
console.warn('Failed to load data browser settings:', error);
396+
}
397+
379398
// Load graphs on initial mount
380399
try {
381400
const graphs = await this.graphPreferencesManager.getGraphs(
@@ -409,6 +428,7 @@ export default class DataBrowser extends React.Component {
409428
document.body.removeEventListener('keyup', this.handleOptionKeyUp);
410429
window.removeEventListener('mousedown', this.handleMouseButtonDown);
411430
window.removeEventListener('mouseup', this.handleMouseButtonUp);
431+
window.removeEventListener('blur', this.handleWindowBlur);
412432
if (this.nativeContextMenuTracker) {
413433
this.nativeContextMenuTracker.dispose();
414434
}
@@ -1581,17 +1601,29 @@ export default class DataBrowser extends React.Component {
15811601

15821602
handleAutoScrollKeyDown(e) {
15831603
// Command/Meta key = keyCode 91 (left) or 93 (right)
1584-
// Only track that Command key is held; don't stop auto-scroll or enter
1585-
// recording mode until the user actually scrolls (handled in handleAutoScrollWheel)
1586-
if ((e.keyCode === 91 || e.keyCode === 93) && this.state.autoScrollEnabled && this.state.isPanelVisible && !this.state.isRecordingAutoScroll) {
1587-
this.setState({ commandKeyPressed: true });
1604+
if ((e.keyCode === 91 || e.keyCode === 93) && this.state.autoScrollEnabled && this.state.isPanelVisible) {
1605+
if (this.state.optionKeyPressed && this.state.isAutoScrolling) {
1606+
// Option already held + Cmd pressed = activate reverse auto-scroll
1607+
// Clear optionKeyPressed to unblock auto-scroll
1608+
this.setState({
1609+
commandKeyPressed: true,
1610+
optionKeyPressed: false,
1611+
reverseAutoScrollActive: true,
1612+
});
1613+
} else if (!this.state.isRecordingAutoScroll) {
1614+
// Normal behavior: track Command key for potential scroll recording
1615+
this.setState({ commandKeyPressed: true });
1616+
}
15881617
}
15891618
}
15901619

15911620
handleAutoScrollKeyUp(e) {
15921621
// Command/Meta key = keyCode 91 (left) or 93 (right)
15931622
if (e.keyCode === 91 || e.keyCode === 93) {
1594-
if (this.state.isRecordingAutoScroll) {
1623+
if (this.state.reverseAutoScrollActive) {
1624+
// Deactivate reverse mode; skip recording-mode logic
1625+
this.setState({ commandKeyPressed: false, reverseAutoScrollActive: false });
1626+
} else if (this.state.isRecordingAutoScroll) {
15951627
const { recordedScrollDelta, recordingScrollStart, recordingScrollEnd } = this.state;
15961628

15971629
// Only start auto-scroll if we actually recorded some scrolling
@@ -1770,19 +1802,42 @@ export default class DataBrowser extends React.Component {
17701802
}, 50);
17711803
}
17721804

1805+
handleWindowBlur() {
1806+
// Reset all modifier key tracking state when the window loses focus,
1807+
// since keyup events won't fire while the window is not focused
1808+
if (this.state.commandKeyPressed || this.state.optionKeyPressed || this.state.reverseAutoScrollActive) {
1809+
this.setState({
1810+
commandKeyPressed: false,
1811+
optionKeyPressed: false,
1812+
reverseAutoScrollActive: false,
1813+
});
1814+
}
1815+
}
1816+
17731817
handleOptionKeyDown(e) {
17741818
// Option/Alt key = keyCode 18
1775-
// Track Option key state to pause auto-scroll while held
1776-
if (e.keyCode === 18 && !this.state.optionKeyPressed) {
1777-
this.setState({ optionKeyPressed: true });
1819+
if (e.keyCode === 18) {
1820+
if (this.state.commandKeyPressed && this.state.isAutoScrolling) {
1821+
// Cmd already held + Option pressed = activate reverse auto-scroll
1822+
// Don't set optionKeyPressed (which would block auto-scroll)
1823+
this.setState({ reverseAutoScrollActive: true });
1824+
} else if (!this.state.optionKeyPressed) {
1825+
// Normal behavior: track Option key to pause auto-scroll
1826+
this.setState({ optionKeyPressed: true });
1827+
}
17781828
}
17791829
}
17801830

17811831
handleOptionKeyUp(e) {
17821832
// Option/Alt key = keyCode 18
1783-
// Track Option key release to resume auto-scroll
1784-
if (e.keyCode === 18 && this.state.optionKeyPressed) {
1785-
this.setState({ optionKeyPressed: false });
1833+
if (e.keyCode === 18) {
1834+
if (this.state.reverseAutoScrollActive) {
1835+
// Deactivate reverse mode; don't trigger normal option-pause cleanup
1836+
this.setState({ reverseAutoScrollActive: false });
1837+
} else if (this.state.optionKeyPressed) {
1838+
// Normal behavior: release Option key pause
1839+
this.setState({ optionKeyPressed: false });
1840+
}
17861841
}
17871842
}
17881843

@@ -1836,6 +1891,7 @@ export default class DataBrowser extends React.Component {
18361891
recordedScrollDelta: 0,
18371892
recordingScrollStart: null,
18381893
recordingScrollEnd: null,
1894+
reverseAutoScrollActive: false,
18391895
});
18401896
}
18411897

@@ -1878,18 +1934,28 @@ export default class DataBrowser extends React.Component {
18781934
}
18791935

18801936
// Animate scroll smoothly using requestAnimationFrame
1881-
const scrollAmount = this.state.autoScrollAmount;
1937+
// Capture reverse state at step start so it stays consistent with scrollAmount
1938+
// throughout the animation (prevents jump if state changes mid-frame)
1939+
const isReversing = this.state.reverseAutoScrollActive;
1940+
let scrollAmount = this.state.autoScrollAmount;
1941+
if (isReversing) {
1942+
scrollAmount = -scrollAmount * this.state.reverseAutoScrollSpeedFactor;
1943+
}
18821944
// Animation duration: 300ms base, scaled by scroll amount (max 500ms)
18831945
const duration = Math.min(300 + Math.abs(scrollAmount) * 0.5, 500);
18841946
const startTime = performance.now();
18851947
const startScrollTop = container.scrollTop;
18861948

18871949
// Get starting positions for multi-panel sync
18881950
const panelStartPositions = [];
1951+
let maxPanelStartScrollTop = 0;
18891952
if (this.state.panelCount > 1 && this.state.syncPanelScroll) {
18901953
this.panelColumnRefs.forEach((ref) => {
18911954
if (ref && ref.current) {
18921955
panelStartPositions.push(ref.current.scrollTop);
1956+
if (ref.current.scrollTop > maxPanelStartScrollTop) {
1957+
maxPanelStartScrollTop = ref.current.scrollTop;
1958+
}
18931959
} else {
18941960
panelStartPositions.push(null);
18951961
}
@@ -1906,6 +1972,13 @@ export default class DataBrowser extends React.Component {
19061972
return;
19071973
}
19081974

1975+
// If reverse state changed mid-animation, restart the step immediately
1976+
// from the current scroll position with the correct direction
1977+
if (this.state.reverseAutoScrollActive !== isReversing) {
1978+
this.performAutoScrollStep();
1979+
return;
1980+
}
1981+
19091982
const elapsed = currentTime - startTime;
19101983
const progress = Math.min(elapsed / duration, 1);
19111984

@@ -1918,11 +1991,23 @@ export default class DataBrowser extends React.Component {
19181991

19191992
// Sync scroll to other panels
19201993
if (this.state.panelCount > 1 && this.state.syncPanelScroll) {
1921-
this.panelColumnRefs.forEach((ref, index) => {
1922-
if (ref && ref.current && panelStartPositions[index] !== null) {
1923-
ref.current.scrollTop = panelStartPositions[index] + (scrollAmount * easeOut);
1924-
}
1925-
});
1994+
if (isReversing) {
1995+
// During reverse auto-scroll, use the max scrollTop as the base for all panels
1996+
// so that shorter panels stay put until the longest panel catches up,
1997+
// matching the behavior of manual wheel scrolling (handleWrapperWheel)
1998+
const newPanelScrollTop = maxPanelStartScrollTop + (scrollAmount * easeOut);
1999+
this.panelColumnRefs.forEach((ref) => {
2000+
if (ref && ref.current) {
2001+
ref.current.scrollTop = newPanelScrollTop;
2002+
}
2003+
});
2004+
} else {
2005+
this.panelColumnRefs.forEach((ref, index) => {
2006+
if (ref && ref.current && panelStartPositions[index] !== null) {
2007+
ref.current.scrollTop = panelStartPositions[index] + (scrollAmount * easeOut);
2008+
}
2009+
});
2010+
}
19262011
}
19272012

19282013
if (progress < 1) {

0 commit comments

Comments
 (0)