diff --git a/doc/TextBuffer.md b/doc/TextBuffer.md index cf19dbad..aa3bfdbb 100644 --- a/doc/TextBuffer.md +++ b/doc/TextBuffer.md @@ -488,6 +488,7 @@ It moves the cursor to the end of the line and joins the current line with the f * options `Object` where: * finalCall `boolean` call the callback one more time at the end of the buffer with an empty string + * fillerCopyAttr `boolean` if set, during the iteration it always copies attribute of a non-filler cell to all filler cells after it * callback `Function( cellData )`, where: * cellData `Object` where: * offset `integer` the offset/position of the current cell in the raw/serialized text @@ -497,6 +498,7 @@ It moves the cursor to the end of the line and joins the current line with the f * attr `integer` the attributes of the current cell in the bit flags mode, use [ScreenBuffer.attr2object()](ScreenBuffer.md#ref.ScreenBuffer.attr2object) to convert it if necessary * misc `Object` userland meta-data for the current cell + * cell `Object` the Cell instance, avoid modifying it unless knowing what you are doing It iterates over the whole *textBuffer*, using the *callback* for each cell. diff --git a/lib/TextBuffer.js b/lib/TextBuffer.js index b4aff45f..fc3d0a09 100644 --- a/lib/TextBuffer.js +++ b/lib/TextBuffer.js @@ -89,6 +89,7 @@ function TextBuffer( options = {} ) { this.buffer = [ [] ] ; this.stateMachine = options.stateMachine || null ; + this.stateMachineCheckpointDistance = options.stateMachineCheckpointDistance || 100 ; // Min distance (in cell) between to checkpoint if ( options.hidden ) { this.setHidden( options.hidden ) ; } } @@ -105,12 +106,13 @@ TextBuffer.prototype.parseMarkup = string.markupMethod.bind( misc.markupOptions // Special: if positive or 0, it's the width of the char, if -1 it's an anti-filler, if -2 it's a filler -function Cell( char = ' ' , special = 1 , attr = null , misc_ = null ) { +function Cell( char = ' ' , special = 1 , attr = null , misc_ = null , checkpoint = null ) { this.char = char ; this.width = special >= 0 ? special : -special - 1 ; this.filler = special < 0 ; // note: antiFiller ARE filler this.attr = attr ; this.misc = misc_ ; + this.checkpoint = checkpoint ; // <-- state-machine checkpoint, null=no checkpoint, any value = state (type is third-party) } TextBuffer.Cell = Cell ; @@ -779,6 +781,14 @@ TextBuffer.prototype.setAttrCodeRegion = function( attr , region = WHOLE_BUFFER_ +// TODOC +TextBuffer.prototype.setCheckpointAt = function( checkpoint , x , y ) { + if ( ! this.buffer[ y ] || ! this.buffer[ y ][ x ] ) { return ; } + this.buffer[ y ][ x ].checkpoint = checkpoint ; +} ; + + + TextBuffer.prototype.isInSelection = function( x = this.cx , y = this.cy ) { if ( ! this.selectionRegion ) { return false ; } return this.isInRegion( this.selectionRegion , x , y ) ; @@ -1069,19 +1079,23 @@ TextBuffer.prototype.getMiscAt = function( x , y ) { TextBuffer.prototype.iterate = function( options , callback ) { - var x , y , yMax , cell , lastNonFillerCell , offset = 0 , length ; + var x , y , yMax , cell , lastNonFillerCell , length , + offset = 0 , + startX = options.x ?? 0 , + startY = options.y ?? 0 , + done = false ; if ( typeof options === 'function' ) { callback = options ; options = {} ; } else if ( ! options || typeof options !== 'object' ) { options = {} ; } if ( ! this.buffer.length ) { return ; } - for ( y = 0 , yMax = this.buffer.length ; y < yMax ; y ++ ) { + for ( y = startY , yMax = this.buffer.length ; y < yMax ; y ++ ) { if ( this.buffer[ y ] ) { length = this.buffer[ y ].length ; lastNonFillerCell = null ; - for ( x = 0 ; x < length ; x ++ ) { + for ( x = y === startY ? startX : 0 ; x < length ; x ++ ) { cell = this.buffer[ y ][ x ] ; if ( cell.filler ) { if ( options.fillerCopyAttr && lastNonFillerCell ) { @@ -1089,13 +1103,18 @@ TextBuffer.prototype.iterate = function( options , callback ) { } } else { - callback( { + // We check if we are done only here, not after the callback, because the 'fillerCopyAttr' option + // should do some extra work on filler-cells even if we had previously finished... + if ( done ) { return ; } + + done = callback( { offset: offset , x: x , y: y , text: cell.char , attr: cell.attr , - misc: cell.misc + misc: cell.misc , + cell } ) ; offset ++ ; @@ -1107,14 +1126,15 @@ TextBuffer.prototype.iterate = function( options , callback ) { // Call the callback one last time at the end of the buffer, with an empty string. // Useful for 'Ne' (Neon) state machine. - if ( options.finalCall ) { + if ( ! done && options.finalCall ) { callback( { offset: offset + 1 , x: null , y: y , text: '' , attr: null , - misc: null + misc: null , + cell: null } ) ; } } ; @@ -1832,6 +1852,59 @@ TextBuffer.prototype.deleteLine = function( getDeleted = false ) { +// TODOC +// Return an object with {x,y,cell}, containing the first cell matching the filter, or null if nothing was found. +// Also work backward if endX,endY are before startX,startY. +TextBuffer.prototype.findCell = function( cellFilterFn , startX = 0 , startY = 0 , endX = null , endY = null ) { + if ( ! this.buffer.length ) { return ; } + + var x , y , cell , endX_ , startX_ , + reverse = endY !== null && ( endY < startY || ( endY === startY && endX !== null && endX < startX ) ) ; + + if ( ! reverse ) { + // Forward search + endY = endY !== null ? Math.min( endY , this.buffer.length - 1 ) : + this.buffer.length - 1 ; + + for ( y = startY ; y <= endY ; y ++ ) { + if ( this.buffer[ y ] ) { + startX_ = y === startY ? Math.min( startX , this.buffer[ y ].length - 1 ) : + 0 ; + endX_ = y === endY && endX !== null ? Math.min( endX , this.buffer[ y ].length - 1 ) : + this.buffer[ y ].length - 1 ; + + for ( x = startX_ ; x <= endX_ ; x ++ ) { + cell = this.buffer[ y ][ x ] ; + if ( cellFilterFn( cell ) ) { return { x , y , cell } ; } + } + } + } + } + else { + // Backward search + startY = Math.min( startY , this.buffer.length - 1 ) ; + endY = Math.min( endY , this.buffer.length - 1 ) ; + + for ( y = startY ; y >= endY ; y -- ) { + if ( this.buffer[ y ] ) { + startX_ = y === startY ? Math.min( startX , this.buffer[ y ].length - 1 ) : + this.buffer[ y ].length - 1 ; + endX_ = y === endY && endX !== null ? Math.min( endX , this.buffer[ y ].length - 1 ) : + 0 ; + + for ( x = startX_ ; x >= endX_ ; x -- ) { + cell = this.buffer[ y ][ x ] ; + if ( cellFilterFn( cell ) ) { return { x , y , cell } ; } + } + } + } + } + + return null ; +} ; + + + // TODOC // Return a region where the searchString is found TextBuffer.prototype.findNext = function( searchString , startPosition , reverse ) { @@ -2203,10 +2276,112 @@ TextBuffer.prototype.runStateMachine = function() { this.stateMachine.reset() ; + var checkpointDistance = 0 ; + + // DEBUG: + var potentialCheckpointCount = 0 , checkpointCount = 0 ; + this.iterate( { finalCall: true , fillerCopyAttr: true } , context => { context.textBuffer = this ; - this.stateMachine.pushEvent( context.text , context ) ; + var isCheckpoint = this.stateMachine.pushEvent( context.text , context ) ; + + // Final call? + if ( ! context.cell ) { return ; } + + // DEBUG: + if ( isCheckpoint ) { potentialCheckpointCount ++ ; } + + if ( isCheckpoint && checkpointDistance >= this.stateMachineCheckpointDistance ) { + let state = this.stateMachine.saveState() ; + context.cell.checkpoint = state ; + checkpointDistance = 0 ; + + // DEBUG: + checkpointCount ++ ; + } + else { + context.cell.checkpoint = null ; + } + + checkpointDistance ++ ; } ) ; + + console.error( "Checkpoint count:" , checkpointCount , potentialCheckpointCount ) ; +} ; + + + +TextBuffer.prototype.runStateMachineLocally = function( fromX , fromY ) { + if ( ! this.stateMachine ) { return ; } + + var iterateOptions , previousCheckpoint , + checkpointDistance = 0 , + startX = 0 , + startY = 0 ; + + // DEBUG: + var potentialCheckpointCount = 0 , checkpointCount = 0 ; + + // First, find a cell with a checkpoint + previousCheckpoint = this.findCell( cell => cell.checkpoint , fromX , fromY , 0 , 0 ) ; + + if ( previousCheckpoint ) { + startX = previousCheckpoint.x ; + startY = previousCheckpoint.y ; + this.stateMachine.restoreState( previousCheckpoint.cell.checkpoint ) ; + console.error( ">> Restore previous checkpoint at:" , startX , startY , '(' , fromX , fromY , ')' ) ; + } + else { + this.stateMachine.reset() ; + console.error( ">> Can't find a restore point (" , fromX , fromY , ')' ) ; + } + + iterateOptions = { + x: startX , + y: startY , + finalCall: true , + fillerCopyAttr: true + } ; + + this.iterate( iterateOptions , context => { + context.textBuffer = this ; + var isCheckpoint = this.stateMachine.pushEvent( context.text , context ) ; + + // Final call? + if ( ! context.cell ) { return ; } + + // DEBUG: + if ( isCheckpoint ) { potentialCheckpointCount ++ ; } + + if ( isCheckpoint ) { + if ( + context.cell.checkpoint + // Have we passed the local point? + && context.y > fromY || ( context.y === fromY && context.x > fromX ) + && this.stateMachine.isStateEqualTo( context.cell.checkpoint ) + ) { + // We found a state saved on a cell, which is after local modification, and that is equal to the current state: + // we don't have to continue further more, there will be no hilighting modification. + console.error( ">>>> Found an equal checkpoint after!" , context.x , context.y ) ; + return true ; + } + + if ( checkpointDistance >= this.stateMachineCheckpointDistance ) { + context.cell.checkpoint = this.stateMachine.saveState() ; + checkpointDistance = 0 ; + + // DEBUG: + checkpointCount ++ ; + } + } + else { + context.cell.checkpoint = null ; + } + + checkpointDistance ++ ; + } ) ; + + console.error( "Local checkpoint count:" , checkpointCount , potentialCheckpointCount ) ; } ; diff --git a/lib/document/Button.js b/lib/document/Button.js index d0583477..a5db3245 100644 --- a/lib/document/Button.js +++ b/lib/document/Button.js @@ -108,6 +108,8 @@ function Button( options ) { this.onMiddleClick = this.onMiddleClick.bind( this ) ; this.onHover = this.onHover.bind( this ) ; + this.disableBlink = options.disableBlink || false ; + if ( options.keyBindings ) { this.keyBindings = options.keyBindings ; } if ( options.actionKeyBindings ) { this.actionKeyBindings = options.actionKeyBindings ; } @@ -260,18 +262,19 @@ Button.prototype.drawSelfCursor = function() { // Blink effect, when the button is submitted Button.prototype.blink = function( special = null , animationCountdown = 4 ) { - if ( animationCountdown ) { - if ( animationCountdown % 2 ) { this.attr = this.focusAttr ; } - else { this.attr = this.blurAttr ; } + if ( !this.disableBlink && animationCountdown) { + + if ( animationCountdown % 2 ) { this.attr = this.focusAttr ; } + else { this.attr = this.blurAttr ; } + + this.draw() ; + setTimeout( () => this.blink( special , animationCountdown - 1 ) , 80 ) ; + } else { + this.updateStatus() ; + this.draw() ; + this.emit( 'blinked' , this.value , special , this ) ; + } - this.draw() ; - setTimeout( () => this.blink( special , animationCountdown - 1 ) , 80 ) ; - } - else { - this.updateStatus() ; - this.draw() ; - this.emit( 'blinked' , this.value , special , this ) ; - } } ; @@ -374,4 +377,3 @@ userActions.submit = function( key ) { if ( this.disabled || this.submitted ) { return ; } this.submit( this.actionKeyBindings[ key ] ) ; } ; - diff --git a/lib/document/ColumnMenu.js b/lib/document/ColumnMenu.js index 637d0b32..9971b139 100644 --- a/lib/document/ColumnMenu.js +++ b/lib/document/ColumnMenu.js @@ -396,6 +396,8 @@ ColumnMenu.prototype.initPage = function( page = this.page ) { paddingHasMarkup: this.paddingHasMarkup , + disableBlink : def.disableBlink || false, + keyBindings: isToggle ? this.toggleButtonKeyBindings : this.buttonKeyBindings , actionKeyBindings: isToggle ? this.toggleButtonActionKeyBindings : this.buttonActionKeyBindings , shortcuts: def.shortcuts , @@ -437,4 +439,3 @@ ColumnMenu.prototype.onParentResize = function() { this.initPage() ; this.draw() ; } ; - diff --git a/lib/document/EditableTextBox.js b/lib/document/EditableTextBox.js index 423875c9..8dcce259 100644 --- a/lib/document/EditableTextBox.js +++ b/lib/document/EditableTextBox.js @@ -230,9 +230,14 @@ EditableTextBox.prototype.setValue = function( value , dontDraw ) { // Called when something was edited, usually requiring to run state machine, auto-scroll and draw. // Usually, editionUpdateDebounced is called instead. // Sync, but return a promise (needed for Promise.debounceUpdate()) -EditableTextBox.prototype.editionUpdate = function() { +EditableTextBox.prototype.editionUpdate = function( startX = null , startY = null ) { if ( this.stateMachine ) { - this.textBuffer.runStateMachine() ; + if ( startX !== null && startY !== null ) { + this.textBuffer.runStateMachineLocally( startX , startY ) ; + } + else { + this.textBuffer.runStateMachine() ; + } } this.autoScrollAndDraw() ; @@ -319,7 +324,7 @@ userActions.character = function( key ) { var count = this.textBuffer.insert( key , this.textAttr ) ; - this.editionUpdateDebounced() ; + this.editionUpdateDebounced( x , y ) ; this.emit( 'change' , { type: 'insert' , diff --git a/lib/document/Element.js b/lib/document/Element.js index 30de9447..3e395c22 100644 --- a/lib/document/Element.js +++ b/lib/document/Element.js @@ -789,9 +789,9 @@ Element.prototype.bindKey = function( key , action ) { this.keyBindings[ key ] = // TODOC Element.prototype.getKeyBinding = function( key ) { return this.keyBindings[ key ] ?? null ; } ; // TODOC -Element.prototype.getKeyBindings = function( key ) { return Object.assign( {} , this.keyBindings ) ; } ; +Element.prototype.getAllKeyBindings = function( key ) { return Object.assign( {} , this.keyBindings ) ; } ; // TODOC -Element.prototype.getActionBinding = function( action , ui = false ) { +Element.prototype.getActionBindings = function( action , ui = false ) { var keys = [] ; for ( let key in this.keyBindings ) { diff --git a/sample/document/buttons-test.js b/sample/document/buttons-test.js index 15c06cd3..52d9ae4e 100755 --- a/sample/document/buttons-test.js +++ b/sample/document/buttons-test.js @@ -46,6 +46,7 @@ var button1 = new termkit.Button( { //content: '> button#1' , content: '> ^[fg:*royal-blue]button#1' , //content: '> ^[fg:*coquelicot]button#1' , + //disableBlink: true, focusAttr: { bgColor: '@light-gray' } , contentHasMarkup: true , value: 'b1' , diff --git a/sample/document/column-menu-test.js b/sample/document/column-menu-test.js index e81ca04f..802b55fa 100755 --- a/sample/document/column-menu-test.js +++ b/sample/document/column-menu-test.js @@ -47,6 +47,7 @@ var columnMenu = new termkit.ColumnMenu( { width: 20 , pageMaxHeight: 5 , //height: 5 , + //disableBlink: true, blurLeftPadding: '^; ' , focusLeftPadding: '^;^R> ' , disabledLeftPadding: '^; ' ,