lexgui 0.7.6 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -347,6 +347,19 @@ class CodeEditor {
347
347
  this.skipTabs = options.skipTabs ?? false;
348
348
  this.useFileExplorer = ( options.fileExplorer ?? false ) && !this.skipTabs;
349
349
  this.useAutoComplete = options.autocomplete ?? true;
350
+ this.allowClosingTabs = options.allowClosingTabs ?? true;
351
+ this.allowLoadingFiles = options.allowLoadingFiles ?? true;
352
+ this.highlight = options.highlight ?? 'Plain Text';
353
+ this.newTabOptions = options.newTabOptions;
354
+ this.customSuggestions = options.customSuggestions ?? [];
355
+
356
+ // Editor callbacks
357
+ this.onSave = options.onSave ?? options.onsave; // LEGACY onsave
358
+ this.onRun = options.onRun ?? options.onrun; // LEGACY onrun
359
+ this.onCtrlSpace = options.onCtrlSpace;
360
+ this.onCreateStatusPanel = options.onCreateStatusPanel;
361
+ this.onContextMenu = options.onContextMenu;
362
+ this.onNewTab = options.onNewTab;
350
363
 
351
364
  // File explorer
352
365
  if( this.useFileExplorer )
@@ -377,8 +390,7 @@ class CodeEditor {
377
390
  this.loadTab( event.node.id );
378
391
  break;
379
392
  case LX.TreeEvent.NODE_DELETED:
380
- this.tabs.delete( event.node.id );
381
- delete this.loadedTabs[ event.node.id ];
393
+ this.closeTab( event.node.id );
382
394
  break;
383
395
  // case LX.TreeEvent.NODE_CONTEXTMENU:
384
396
  // LX.addContextMenu( event.multiple ? "Selected Nodes" : event.node.id, event.value, m => {
@@ -547,36 +559,44 @@ class CodeEditor {
547
559
  return;
548
560
  }
549
561
 
550
- this.setScrollBarValue( 'vertical' );
562
+ // Vertical scroll
563
+ {
564
+ this.setScrollBarValue( 'vertical' );
551
565
 
552
- const scrollTop = this.getScrollTop();
566
+ const scrollTop = this.getScrollTop();
553
567
 
554
- // Scroll down...
555
- if( scrollTop > lastScrollTopValue )
556
- {
557
- if( this.visibleLinesViewport.y < (this.code.lines.length - 1) )
568
+ // Scroll down...
569
+ if( scrollTop > lastScrollTopValue )
558
570
  {
559
- const totalLinesInViewport = ( ( this.codeScroller.offsetHeight ) / this.lineHeight )|0;
560
- const scrollDownBoundary =
561
- ( Math.max( this.visibleLinesViewport.y - totalLinesInViewport, 0 ) - 1 ) * this.lineHeight;
562
-
563
- if( scrollTop >= scrollDownBoundary )
571
+ if( this.visibleLinesViewport.y < (this.code.lines.length - 1) )
564
572
  {
565
- this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
573
+ const totalLinesInViewport = ( ( this.codeScroller.offsetHeight ) / this.lineHeight )|0;
574
+ const scrollDownBoundary =
575
+ ( Math.max( this.visibleLinesViewport.y - totalLinesInViewport, 0 ) - 1 ) * this.lineHeight;
576
+
577
+ if( scrollTop >= scrollDownBoundary )
578
+ {
579
+ this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
580
+ }
566
581
  }
567
582
  }
568
- }
569
- // Scroll up...
570
- else
571
- {
572
- const scrollUpBoundary = parseInt( this.code.style.top );
573
- if( scrollTop < scrollUpBoundary )
583
+ // Scroll up...
584
+ else
574
585
  {
575
- this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
586
+ const scrollUpBoundary = parseInt( this.code.style.top );
587
+ if( scrollTop < scrollUpBoundary )
588
+ {
589
+ this.processLines( CodeEditor.UPDATE_VISIBLE_LINES );
590
+ }
576
591
  }
592
+
593
+ lastScrollTopValue = scrollTop;
577
594
  }
578
595
 
579
- lastScrollTopValue = scrollTop;
596
+ // Horizontal scroll
597
+ {
598
+ this.setScrollBarValue( 'horizontal' );
599
+ }
580
600
  });
581
601
 
582
602
  this.codeScroller.addEventListener( 'wheel', e => {
@@ -697,9 +717,6 @@ class CodeEditor {
697
717
 
698
718
  // Code
699
719
 
700
- this.highlight = options.highlight ?? 'Plain Text';
701
- this.onsave = options.onsave ?? ((code) => { console.log( code, "save" ) });
702
- this.onrun = options.onrun ?? ((code) => { this.runScript(code) });
703
720
  this.actions = {};
704
721
  this.cursorBlinkRate = 550;
705
722
  this.tabSpaces = 4;
@@ -784,14 +801,14 @@ class CodeEditor {
784
801
  var numCharsDeleted = 1;
785
802
 
786
803
  // Delete full word
787
- if( e.shiftKey )
804
+ if( e.ctrlKey )
788
805
  {
789
806
  const [word, from, to] = this.getWordAtPos( cursor, -1 );
790
807
 
791
808
  if( word.length > 1 )
792
809
  {
793
810
  deleteFromPosition = from;
794
- numCharsDeleted = word.length;
811
+ numCharsDeleted = word.length - ( to - cursor.position );
795
812
  }
796
813
  }
797
814
 
@@ -966,7 +983,7 @@ class CodeEditor {
966
983
 
967
984
  if( e.ctrlKey )
968
985
  {
969
- this.onrun( this.getText() );
986
+ this.onRun( this.getText() );
970
987
  return;
971
988
  }
972
989
 
@@ -1149,7 +1166,7 @@ class CodeEditor {
1149
1166
  if( e.metaKey ) // Apple devices (Command)
1150
1167
  {
1151
1168
  e.preventDefault();
1152
- this.actions[ 'End' ].callback( ln, cursor );
1169
+ this.actions[ 'End' ].callback( ln, cursor, e );
1153
1170
  }
1154
1171
  else if( e.ctrlKey ) // Next word
1155
1172
  {
@@ -1290,7 +1307,7 @@ class CodeEditor {
1290
1307
  for( let url of options.files )
1291
1308
  {
1292
1309
  const finalUrl = url.constructor === Array ? url[ 0 ] : url;
1293
- const finalFileName = url.constructor === Array ? url[ 1 ] : url;
1310
+ const finalFileName = url.constructor === Array ? url[ 1 ] : undefined;
1294
1311
 
1295
1312
  await this.loadFile( finalUrl, { filename: finalFileName, async: loadAsync, callback: ( name, text ) => {
1296
1313
  filesLoaded++;
@@ -1300,17 +1317,21 @@ class CodeEditor {
1300
1317
 
1301
1318
  if( options.onFilesLoaded )
1302
1319
  {
1303
- options.onFilesLoaded( this.loadedTabs, numFiles );
1320
+ options.onFilesLoaded( this, this.loadedTabs, numFiles );
1304
1321
  }
1305
1322
  }
1306
1323
  }});
1307
1324
  }
1308
1325
  }
1309
- else
1326
+ else if( options.defaultTab ?? true )
1310
1327
  {
1311
1328
  this.addTab( options.name || "untitled", true, options.title, { language: options.highlight ?? "Plain Text" } );
1312
1329
  onLoadAll();
1313
1330
  }
1331
+ else
1332
+ {
1333
+ onLoadAll();
1334
+ }
1314
1335
  }
1315
1336
 
1316
1337
  static getInstances()
@@ -1711,6 +1732,11 @@ class CodeEditor {
1711
1732
 
1712
1733
  let panel = new LX.Panel({ className: "lexcodetabinfo flex flex-row", height: "auto" });
1713
1734
 
1735
+ if( this.onCreateStatusPanel )
1736
+ {
1737
+ this.onCreateStatusPanel( panel, this );
1738
+ }
1739
+
1714
1740
  let leftStatusPanel = new LX.Panel( { id: "FontSizeZoomStatusComponent", height: "auto" } );
1715
1741
  leftStatusPanel.sameLine();
1716
1742
 
@@ -1856,10 +1882,18 @@ class CodeEditor {
1856
1882
 
1857
1883
  this.processFocus( false );
1858
1884
 
1859
- new LX.DropdownMenu( e.target, [
1885
+ if( this.onNewTab )
1886
+ {
1887
+ this.onNewTab( e );
1888
+ return;
1889
+ }
1890
+
1891
+ const dmOptions = this.newTabOptions ?? [
1860
1892
  { name: "Create file", icon: "FilePlus", callback: this._onCreateNewFile.bind( this ) },
1861
- { name: "Load file", icon: "FileUp", callback: this.loadTabFromFile.bind( this ) },
1862
- ], { side: "bottom", align: "start" });
1893
+ { name: "Load file", icon: "FileUp", disabled: !this.allowLoadingFiles, callback: this.loadTabFromFile.bind( this ) }
1894
+ ];
1895
+
1896
+ new LX.DropdownMenu( e.target, dmOptions, { side: "bottom", align: "start" });
1863
1897
  }
1864
1898
 
1865
1899
  _onCreateNewFile() {
@@ -1869,10 +1903,14 @@ class CodeEditor {
1869
1903
  if( this.onCreateFile )
1870
1904
  {
1871
1905
  options = this.onCreateFile( this );
1906
+ if( !options ) // Skip adding new file
1907
+ {
1908
+ return;
1909
+ }
1872
1910
  }
1873
1911
 
1874
1912
  const name = options.name ?? "unnamed.js";
1875
- this.addTab( name, true, name, { language: options.language ?? "JavaScript" } );
1913
+ this.addTab( name, true, name, { indexOffset: options.indexOffset, language: options.language ?? "JavaScript" } );
1876
1914
  }
1877
1915
 
1878
1916
  _onSelectTab( isNewTabButton, event, name ) {
@@ -1921,19 +1959,19 @@ class CodeEditor {
1921
1959
  }
1922
1960
 
1923
1961
  new LX.DropdownMenu( event.target, [
1924
- { name: "Close", kbd: "MWB", callback: () => { this.tabs.delete( name ) } },
1925
- { name: "Close Others", callback: () => {
1962
+ { name: "Close", kbd: "MWB", disabled: !this.allowClosingTabs, callback: () => { this.closeTab( name ) } },
1963
+ { name: "Close Others", disabled: !this.allowClosingTabs, callback: () => {
1926
1964
  for( const [ key, data ] of Object.entries( this.tabs.tabs ) )
1927
1965
  {
1928
1966
  if( key === '+' || key === name ) continue;
1929
- this.tabs.delete( key )
1967
+ this.closeTab( key )
1930
1968
  }
1931
1969
  } },
1932
- { name: "Close All", callback: () => {
1970
+ { name: "Close All", disabled: !this.allowClosingTabs, callback: () => {
1933
1971
  for( const [ key, data ] of Object.entries( this.tabs.tabs ) )
1934
1972
  {
1935
1973
  if( key === '+' ) continue;
1936
- this.tabs.delete( key )
1974
+ this.closeTab( key )
1937
1975
  }
1938
1976
  } },
1939
1977
  null,
@@ -2015,7 +2053,8 @@ class CodeEditor {
2015
2053
  icon: tabIcon,
2016
2054
  onSelect: this._onSelectTab.bind( this, isNewTabButton ),
2017
2055
  onContextMenu: this._onContextMenuTab.bind( this, isNewTabButton ),
2018
- allowDelete: true
2056
+ allowDelete: this.allowClosingTabs,
2057
+ indexOffset: options.indexOffset
2019
2058
  } );
2020
2059
  }
2021
2060
 
@@ -2038,6 +2077,12 @@ class CodeEditor {
2038
2077
  this.mustProcessLines = true;
2039
2078
  }
2040
2079
 
2080
+ if( options.codeLines )
2081
+ {
2082
+ code.lines = options.codeLines;
2083
+ this.mustProcessLines = true;
2084
+ }
2085
+
2041
2086
  this._processLinesIfNecessary();
2042
2087
 
2043
2088
  this._updateDataInfoPanel( "@tab-name", name );
@@ -2155,7 +2200,7 @@ class CodeEditor {
2155
2200
  icon: tabIcon,
2156
2201
  onSelect: this._onSelectTab.bind( this, isNewTabButton ),
2157
2202
  onContextMenu: this._onContextMenuTab.bind( this, isNewTabButton ),
2158
- allowDelete: true
2203
+ allowDelete: this.allowClosingTabs
2159
2204
  });
2160
2205
 
2161
2206
  // Move into the sizer..
@@ -2173,6 +2218,11 @@ class CodeEditor {
2173
2218
 
2174
2219
  closeTab( name, eraseAll ) {
2175
2220
 
2221
+ if( !this.allowClosingTabs )
2222
+ {
2223
+ return;
2224
+ }
2225
+
2176
2226
  this.tabs.delete( name );
2177
2227
 
2178
2228
  if( eraseAll )
@@ -2290,22 +2340,65 @@ class CodeEditor {
2290
2340
  e.preventDefault();
2291
2341
 
2292
2342
  if( !this.canOpenContextMenu )
2343
+ {
2293
2344
  return;
2345
+ }
2294
2346
 
2295
2347
  LX.addContextMenu( null, e, m => {
2296
2348
  m.add( "Copy", () => { this._copyContent( cursor ); } );
2349
+
2297
2350
  if( !this.disableEdition )
2298
2351
  {
2299
2352
  m.add( "Cut", () => { this._cutContent( cursor ); } );
2300
2353
  m.add( "Paste", () => { this._pasteContent( cursor ); } );
2354
+ }
2355
+
2356
+ if( !this.onContextMenu )
2357
+ {
2358
+ return;
2359
+ }
2360
+
2361
+ let content = null;
2362
+
2363
+ if( cursor.selection )
2364
+ {
2365
+ // Some selections don't depend on mouse up..
2366
+ if( cursor.selection ) cursor.selection.invertIfNecessary();
2367
+
2368
+ const separator = "_NEWLINE_";
2369
+ let code = this.code.lines.join( separator );
2370
+
2371
+ // Get linear start index
2372
+ let index = 0;
2373
+
2374
+ for( let i = 0; i <= cursor.selection.fromY; i++ )
2375
+ {
2376
+ index += ( i == cursor.selection.fromY ? cursor.selection.fromX : this.code.lines[ i ].length );
2377
+ }
2378
+
2379
+ index += cursor.selection.fromY * separator.length;
2380
+ const num_chars = cursor.selection.chars + ( cursor.selection.toY - cursor.selection.fromY ) * separator.length;
2381
+ const text = code.substr( index, num_chars );
2382
+ content = text.split( separator ).join('\n');
2383
+ }
2384
+
2385
+ const options = this.onContextMenu( this, content, e );
2386
+ if( options.length )
2387
+ {
2301
2388
  m.add( "" );
2302
- m.add( "Format/JSON", () => {
2303
- let json = this.toJSONFormat( this.getText() );
2304
- if( !json )
2305
- return;
2306
- this.code.lines = json.split( "\n" );
2307
- this.processLines();
2308
- } );
2389
+
2390
+ for( const o of options )
2391
+ {
2392
+ m.add( o.path, { disabled: o.disabled, callback: o.callback } );
2393
+ }
2394
+
2395
+ // m.add( "Format/JSON", () => {
2396
+ // let json = this.toJSONFormat( this.getText() );
2397
+ // if( !json )
2398
+ // return;
2399
+ // this.code.lines = json.split( "\n" );
2400
+ // this.processLines();
2401
+ // } );
2309
2402
  }
2310
2403
  });
2311
2404
 
@@ -2638,10 +2731,17 @@ class CodeEditor {
2638
2731
  e.preventDefault();
2639
2732
  this.selectAll();
2640
2733
  return true;
2734
+ case 'b': // k+b comment block
2735
+ e.preventDefault();
2736
+ if( this.state.keyChain == 'k' ) {
2737
+ this._commentLines( cursor, true );
2738
+ return true;
2739
+ }
2740
+ return false;
2641
2741
  case 'c': // k+c, comment line
2642
2742
  e.preventDefault();
2643
2743
  if( this.state.keyChain == 'k' ) {
2644
- this._commentLines();
2744
+ this._commentLines( cursor );
2645
2745
  return true;
2646
2746
  }
2647
2747
  return false;
@@ -2663,12 +2763,12 @@ class CodeEditor {
2663
2763
  return true;
2664
2764
  case 's': // save
2665
2765
  e.preventDefault();
2666
- this.onsave( this.getText() );
2766
+ this.onSave( this.getText() );
2667
2767
  return true;
2668
2768
  case 'u': // k+u, uncomment line
2669
2769
  e.preventDefault();
2670
2770
  if( this.state.keyChain == 'k' ) {
2671
- this._uncommentLines();
2771
+ this._uncommentLines( cursor );
2672
2772
  return true;
2673
2773
  }
2674
2774
  return false;
@@ -2688,6 +2788,13 @@ class CodeEditor {
2688
2788
  e.preventDefault();
2689
2789
  this._decreaseFontSize();
2690
2790
  return true;
2791
+ case ' ': // custom event
2792
+ if( this.onCtrlSpace )
2793
+ {
2794
+ e.preventDefault();
2795
+ this.onCtrlSpace( cursor );
2796
+ return true;
2797
+ }
2691
2798
  }
2692
2799
  }
2693
2800
 
@@ -2981,32 +3088,77 @@ class CodeEditor {
2981
3088
  this.hideAutoCompleteBox();
2982
3089
  }
2983
3090
 
2984
- _commentLines() {
3091
+ _commentLines( cursor, useCommentBlock ) {
3092
+
3093
+ const lang = CodeEditor.languages[ this.highlight ];
2985
3094
 
2986
3095
  this.state.keyChain = null;
2987
3096
 
3097
+ cursor = cursor ?? this.getCurrentCursor();
3098
+
2988
3099
  if( cursor.selection )
2989
3100
  {
2990
- var cursor = this.getCurrentCursor();
3101
+ if( !( ( useCommentBlock ? lang.blockComments : lang.singleLineComments ) ?? true ) )
3102
+ {
3103
+ return;
3104
+ }
3105
+
2991
3106
  this._addUndoStep( cursor, true );
2992
3107
 
2993
- const selectedLines = this.code.lines.slice( cursor.selection.fromY, cursor.selection.toY );
3108
+ const selectedLines = this.code.lines.slice( cursor.selection.fromY, cursor.selection.toY + 1 );
2994
3109
  const minIdx = Math.min(...selectedLines.map( v => {
2995
3110
  var idx = firstNonspaceIndex( v );
2996
3111
  return idx < 0 ? 1e10 : idx;
2997
3112
  } ));
2998
3113
 
2999
- for( var i = cursor.selection.fromY; i <= cursor.selection.toY; ++i )
3114
+ if( useCommentBlock )
3115
+ {
3116
+ const tokens = ( lang.blockCommentsTokens ?? this.defaultBlockCommentTokens );
3117
+
3118
+ const fromString = this.code.lines[ cursor.selection.fromY ];
3119
+ let fromIdx = firstNonspaceIndex( fromString );
3120
+ if( fromIdx == -1 )
3121
+ {
3122
+ fromIdx = 0;
3123
+ }
3124
+
3125
+ this.code.lines[ cursor.selection.fromY ] = [
3126
+ fromString.substring( 0, fromIdx ),
3127
+ tokens[ 0 ] + " ",
3128
+ fromString.substring( fromIdx )
3129
+ ].join( '' );
3130
+
3131
+ this.code.lines[ cursor.selection.toY ] += " " + tokens[ 1 ];
3132
+
3133
+ cursor.selection.fromX += ( tokens[ 0 ].length + 1 );
3134
+ this._processSelection( cursor );
3135
+ }
3136
+ else
3000
3137
  {
3001
- this._commentLine( cursor, i, minIdx );
3138
+ for( let i = cursor.selection.fromY; i <= cursor.selection.toY; ++i )
3139
+ {
3140
+ this._commentLine( cursor, i, minIdx, false );
3141
+ }
3142
+
3143
+ const token = ( lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken ) + ' ';
3144
+ this.cursorToString( cursor, token );
3145
+
3146
+ cursor.selection.fromX += token.length;
3147
+ cursor.selection.toX += token.length;
3148
+ this._processSelection( cursor );
3002
3149
  }
3003
3150
  }
3004
3151
  else
3005
3152
  {
3006
- for( let cursor of this.cursors.children )
3153
+ if( !( lang.singleLineComments ?? true ) )
3007
3154
  {
3008
- this._addUndoStep( cursor, true );
3009
- this._commentLine( cursor, cursor.line );
3155
+ return;
3156
+ }
3157
+
3158
+ for( const cr of this.cursors.children )
3159
+ {
3160
+ this._addUndoStep( cr, true );
3161
+ this._commentLine( cr, cr.line );
3010
3162
  }
3011
3163
  }
3012
3164
 
@@ -3014,33 +3166,37 @@ class CodeEditor {
3014
3166
  this._hideActiveLine();
3015
3167
  }
3016
3168
 
3017
- _commentLine( cursor, line, minNonspaceIdx ) {
3169
+ _commentLine( cursor, line, minNonspaceIdx, updateCursor = true ) {
3018
3170
 
3019
3171
  const lang = CodeEditor.languages[ this.highlight ];
3020
-
3021
- if( !( lang.singleLineComments ?? true ))
3172
+ if( !( lang.singleLineComments ?? true ) )
3022
3173
  return;
3023
3174
 
3024
3175
  const token = ( lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken ) + ' ';
3025
3176
  const string = this.code.lines[ line ];
3026
3177
 
3027
3178
  let idx = firstNonspaceIndex( string );
3028
- if( idx > -1 )
3179
+ if( idx == -1 )
3029
3180
  {
3030
- // Update idx using min of the selected lines (if necessary..)
3031
- idx = minNonspaceIdx ?? idx;
3181
+ return;
3182
+ }
3032
3183
 
3033
- this.code.lines[ line ] = [
3034
- string.substring( 0, idx ),
3035
- token,
3036
- string.substring( idx )
3037
- ].join( '' );
3184
+ // Update idx using min of the selected lines (if necessary..)
3185
+ idx = minNonspaceIdx ?? idx;
3038
3186
 
3187
+ this.code.lines[ line ] = [
3188
+ string.substring( 0, idx ),
3189
+ token,
3190
+ string.substring( idx )
3191
+ ].join( '' );
3192
+
3193
+ if( updateCursor )
3194
+ {
3039
3195
  this.cursorToString( cursor, token );
3040
3196
  }
3041
3197
  }
3042
3198
 
3043
- _uncommentLines() {
3199
+ _uncommentLines( cursor ) {
3044
3200
 
3045
3201
  this.state.keyChain = null;
3046
3202
 
@@ -4619,6 +4775,8 @@ class CodeEditor {
4619
4775
 
4620
4776
  if( state.selection )
4621
4777
  {
4778
+ this.endSelection();
4779
+
4622
4780
  this.startSelection( cursor );
4623
4781
 
4624
4782
  cursor.selection.load( state.selection );
@@ -4899,7 +5057,10 @@ class CodeEditor {
4899
5057
  }
4900
5058
  else
4901
5059
  {
4902
- this.codeScroller.scrollLeft += value;
5060
+ if( value )
5061
+ {
5062
+ this.codeScroller.scrollLeft += value;
5063
+ }
4903
5064
 
4904
5065
  const scrollBarWidth = this.hScrollbar.thumb.parentElement.offsetWidth;
4905
5066
  const scrollThumbWidth = this.hScrollbar.thumb.offsetWidth;
@@ -5109,6 +5270,9 @@ class CodeEditor {
5109
5270
  suggestions = suggestions.concat( otherValues.slice( 0, -1 ) );
5110
5271
  }
5111
5272
 
5273
+ // Add custom suggestions...
5274
+ suggestions = suggestions.concat( this.customSuggestions );
5275
+
5112
5276
  // Remove 1/2 char words and duplicates...
5113
5277
  suggestions = Array.from( new Set( suggestions )).filter( s => s.length > 2 && s.toLowerCase().includes( word.toLowerCase() ) );
5114
5278
 
@@ -193,7 +193,6 @@ class Timeline {
193
193
  this.setTime(value);
194
194
  }, {
195
195
  units: "s",
196
- signal: "@on_set_time_" + this.uniqueID,
197
196
  step: 0.01, min: 0, precision: 3,
198
197
  skipSlider: true,
199
198
  skipReset: true,
@@ -205,7 +204,6 @@ class Timeline {
205
204
  }, {
206
205
  units: "s",
207
206
  step: 0.01, min: 0,
208
- signal: "@on_set_duration_" + this.uniqueID,
209
207
  skipReset: true,
210
208
  nameWidth: "auto"
211
209
  });
@@ -261,7 +259,6 @@ class Timeline {
261
259
  */
262
260
  updateLeftPanel( ) {
263
261
 
264
- const scrollTop = this.trackTreesPanel ? this.trackTreesPanel.root.scrollTop : 0;
265
262
  this.leftPanel.clear();
266
263
 
267
264
  const panel = this.leftPanel;
@@ -326,7 +323,7 @@ class Timeline {
326
323
  }
327
324
  });
328
325
 
329
- this.trackTreesPanel.root.scrollTop = scrollTop;
326
+ this.trackTreesPanel.root.scrollTop = this.currentScrollInPixels;
330
327
 
331
328
  if( this.leftPanel.parent.root.classList.contains("hidden") || !this.root.parentElement ){
332
329
  return;
@@ -694,7 +691,7 @@ class Timeline {
694
691
  this.duration = this.animationClip.duration = v;
695
692
 
696
693
  if(updateHeader) {
697
- LX.emit( "@on_set_duration_" + this.uniqueID, + this.duration.toFixed(2)); // skipcallback = true
694
+ this.header.components["Duration"].set( +this.duration.toFixed(2), true ); // skipcallback = true
698
695
  }
699
696
 
700
697
  if( this.onSetDuration && !skipCallback )
@@ -703,7 +700,7 @@ class Timeline {
703
700
 
704
701
  setTime(time, skipCallback = false ){
705
702
  this.currentTime = Math.max(0,Math.min(time,this.duration));
706
- LX.emit( "@on_set_time_" + this.uniqueID, +this.currentTime.toFixed(2)); // skipcallback = true
703
+ this.header.components["Current Time"].set( +this.currentTime.toFixed(2), true ); // skipcallback = true
707
704
 
708
705
  if(this.onSetTime && !skipCallback)
709
706
  this.onSetTime(this.currentTime);
@@ -733,6 +730,41 @@ class Timeline {
733
730
  this.visualOriginTime += this.currentTime - this.xToTime(xCurrentTime);
734
731
  }
735
732
 
733
+ /**
734
+ * @method setScroll
735
+ * not delta from last state, but full scroll amount.
736
+ * @param {Number} scrollY either pixels or [0,1]
737
+ * @param {Bool} normalized if true, scrollY is in range[0,1] being 1 fully scrolled. Otherwised scrollY represents pixels
738
+ * @returns
739
+ */
740
+
741
+ setScroll( scrollY, normalized = true ){
742
+ if ( !this.trackTreesPanel ){
743
+ this.currentScroll = 0;
744
+ this.currentScrollInPixels = 0;
745
+ return;
746
+ }
747
+
748
+ const r = this.trackTreesPanel.root;
749
+ if (r.scrollHeight > r.clientHeight){
750
+ if ( normalized ){
751
+ this.currentScroll = scrollY;
752
+ this.currentScrollInPixels = scrollY * (r.scrollHeight - r.clientHeight);
753
+ }else{
754
+ this.currentScroll = scrollY / (r.scrollHeight - r.clientHeight);
755
+ this.currentScrollInPixels = scrollY;
756
+ }
757
+ }
758
+ else{
759
+ this.currentScroll = 0;
760
+ this.currentScrollInPixels = 0;
761
+ }
762
+
763
+ // automatically calls event.
764
+ this.trackTreesPanel.root.scrollTop = this.currentScrollInPixels;
765
+
766
+ }
767
+
736
768
  /**
737
769
  * @method processMouse
738
770
  * @param {*} e
@@ -1567,9 +1599,7 @@ class KeyFramesTimeline extends Timeline {
1567
1599
  */
1568
1600
  changeSelectedItems( itemsToAdd = null, itemsToRemove = null, skipCallback = false ) {
1569
1601
 
1570
- // TODO: maybe make this un-functions more general
1571
- this.deselectAllKeyFrames();
1572
- this.unHoverAll();
1602
+ this.deselectAllElements();
1573
1603
 
1574
1604
  const tracks = this.animationClip.tracks;
1575
1605
  const tracksPerGroup = this.animationClip.tracksPerGroup;
@@ -3219,9 +3249,7 @@ class ClipsTimeline extends Timeline {
3219
3249
  */
3220
3250
  changeSelectedItems( ) {
3221
3251
 
3222
- // TODO: maybe make this un-functions more general
3223
- this.deselectAllClips();
3224
- this.unHoverAll();
3252
+ this.deselectAllElements();
3225
3253
 
3226
3254
  this.selectedItems = this.animationClip.tracks.slice();
3227
3255