Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/button blink optional #218

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/TextBuffer.md
Expand Up @@ -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
Expand All @@ -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.

Expand Down
193 changes: 184 additions & 9 deletions lib/TextBuffer.js
Expand Up @@ -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 ) ; }
}
Expand All @@ -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 ;
Expand Down Expand Up @@ -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 ) ;
Expand Down Expand Up @@ -1069,33 +1079,42 @@ 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 ) {
cell.attr = lastNonFillerCell.attr ;
}
}
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 ++ ;
Expand All @@ -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
} ) ;
}
} ;
Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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 ) ;
} ;


Expand Down
26 changes: 14 additions & 12 deletions lib/document/Button.js
Expand Up @@ -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 ; }

Expand Down Expand Up @@ -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 ) ;
}
} ;


Expand Down Expand Up @@ -374,4 +377,3 @@ userActions.submit = function( key ) {
if ( this.disabled || this.submitted ) { return ; }
this.submit( this.actionKeyBindings[ key ] ) ;
} ;

3 changes: 2 additions & 1 deletion lib/document/ColumnMenu.js
Expand Up @@ -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 ,
Expand Down Expand Up @@ -437,4 +439,3 @@ ColumnMenu.prototype.onParentResize = function() {
this.initPage() ;
this.draw() ;
} ;

11 changes: 8 additions & 3 deletions lib/document/EditableTextBox.js
Expand Up @@ -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() ;
Expand Down Expand Up @@ -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' ,
Expand Down
4 changes: 2 additions & 2 deletions lib/document/Element.js
Expand Up @@ -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 ) {
Expand Down
1 change: 1 addition & 0 deletions sample/document/buttons-test.js
Expand Up @@ -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' ,
Expand Down