@@ -23,6 +23,7 @@ import { ResizableBox } from 'react-resizable';
2323import ScriptConfirmationModal from '../../../components/ScriptConfirmationModal/ScriptConfirmationModal.react' ;
2424import styles from './Databrowser.scss' ;
2525import KeyboardShortcutsManager , { matchesShortcut } from 'lib/KeyboardShortcutsPreferences' ;
26+ import ServerConfigStorage from 'lib/ServerConfigStorage' ;
2627
2728import AggregationPanel from '../../../components/AggregationPanel/AggregationPanel' ;
2829import { 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