diff --git a/lib/document/BaseMenu.js b/lib/document/BaseMenu.js index 5eaf020a..08471604 100644 --- a/lib/document/BaseMenu.js +++ b/lib/document/BaseMenu.js @@ -32,6 +32,8 @@ +const tree = require( 'tree-kit' ) ; + const Element = require( './Element.js' ) ; const Button = require( './Button.js' ) ; @@ -58,6 +60,30 @@ function BaseMenu( options = {} ) { this.page = 0 ; this.maxPage = 0 ; + // Submenu + this.hasSubmenu = !! options.submenu ; + this.submenu = null ; // A child (column) menu + this.submenuOptions = null ; + + if ( this.hasSubmenu ) { + // Use tree-kit because 'options' comes from an Object.create() and has almost no owned properties + this.submenuOptions = tree.extend( null , {} , options , { + // Things to clear or to force + internal: true , + parent: null , + items: null , + x: undefined , outputX: undefined , + y: undefined , outputY: undefined , + //width: undefined , outputWidth: undefined , + //height: undefined , outputHeight: undefined , + //submenu: false + } ) ; + + if ( options.submenu && typeof options.submenu === 'object' ) { + Object.assign( this.submenuOptions , options.submenu ) ; + } + } + this.onButtonSubmit = this.onButtonSubmit.bind( this ) ; this.onButtonToggle = this.onButtonToggle.bind( this ) ; this.onButtonFocus = this.onButtonFocus.bind( this ) ; @@ -298,14 +324,22 @@ BaseMenu.prototype.onButtonSubmit = function( buttonValue , action , button ) { this.nextPage() ; break ; default : + if ( this.hasSubmenu && this.submenuOptions.openOn === 'submit' ) { this.openSubmenu( button.value , button ) ; } this.emit( 'submit' , buttonValue , action , this ) ; } } ; -BaseMenu.prototype.onButtonBlinked = function( ... args ) { - this.emit( 'blinked' , ... args ) ; +BaseMenu.prototype.onButtonBlinked = function( buttonValue , action , button ) { + switch ( button.internalRole ) { + case 'previousPage' : + case 'nextPage' : + break ; + default : + if ( this.hasSubmenu && this.submenuOptions.openOn === 'blinked' ) { this.openSubmenu( button.value , button ) ; } + this.emit( 'blinked' , buttonValue , action , this ) ; + } } ; @@ -313,10 +347,10 @@ BaseMenu.prototype.onButtonBlinked = function( ... args ) { BaseMenu.prototype.onButtonFocus = function( focus , type , button ) { switch ( button.internalRole ) { case 'previousPage' : - break ; case 'nextPage' : break ; default : + if ( this.hasSubmenu && this.submenuOptions.openOn === 'focus' ) { this.openSubmenu( button.value , button ) ; } this.emit( 'itemFocus' , button.value , focus , button ) ; } } ; @@ -332,5 +366,6 @@ BaseMenu.prototype.toggleButtonKeyBindings = {} ; BaseMenu.prototype.toggleButtonActionKeyBindings = {} ; BaseMenu.prototype.initPage = function() {} ; BaseMenu.prototype.onButtonToggle = function() {} ; +BaseMenu.prototype.submenu = function() {} ; BaseMenu.prototype.childUseParentKeyValue = false ; diff --git a/lib/document/ColumnMenu.js b/lib/document/ColumnMenu.js index df51a67b..f215302a 100644 --- a/lib/document/ColumnMenu.js +++ b/lib/document/ColumnMenu.js @@ -42,6 +42,7 @@ function ColumnMenu( options ) { options = ! options ? {} : options.internal ? options : Object.create( options ) ; options.internal = true ; + this.onSubmenuSubmit = this.onSubmenuSubmit.bind( this ) ; this.onParentResize = this.onParentResize.bind( this ) ; // Overwritten by Element() when .autoWidth is set @@ -133,7 +134,8 @@ ColumnMenu.prototype.keyBindings = { END: 'lastPage' , // ENTER: 'submit' , // KP_ENTER: 'submit' , - ALT_ENTER: 'submit' + ALT_ENTER: 'submit' , + ESC: 'parent' } ; ColumnMenu.prototype.buttonKeyBindings = { @@ -411,8 +413,8 @@ ColumnMenu.prototype.initPage = function( page = this.page ) { } ) ; this.buttons[ index ].on( 'submit' , this.onButtonSubmit ) ; - this.buttons[ index ].on( 'focus' , this.onButtonFocus ) ; this.buttons[ index ].on( 'blinked' , this.onButtonBlinked ) ; + this.buttons[ index ].on( 'focus' , this.onButtonFocus ) ; if ( isToggle ) { this.buttons[ index ].on( 'toggle' , this.onButtonToggle ) ; @@ -428,6 +430,82 @@ ColumnMenu.prototype.initPage = function( page = this.page ) { +// Userland: .submenu( itemValue ) +// Internal: .submenu( itemValue , button ) +ColumnMenu.prototype.openSubmenu = function( itemValue , button = null ) { + var x , y , width , height , + itemDef = button ? this.pageItemsDef[ this.page ][ button.childId ] : + this.itemsDef.find( it => it.value === itemValue ) ; + + if ( ! itemDef || ! itemDef.items || ! itemDef.items.length ) { return ; } + + if ( this.submenu ) { + if ( this.submenu.meta.fromItemDef === itemDef ) { return ; } + else { this.submenu.destroy() ; } + } + + switch ( this.submenuOptions.disposition ) { + case 'overwrite' : + x = this.outputX ; + y = this.outputY ; + width = this.outputWidth ; + height = this.outputHeight ; + break ; + case 'right' : + default : + x = this.outputX + this.outputWidth ; + y = this.outputY ; + width = this.submenuOptions.width || this.outputWidth ; + break ; + } + + if ( this.submenuOptions.hideParent ) { + this.children.forEach( e => e.hidden = true ) ; + } + + this.submenu = new ColumnMenu( Object.assign( {} , this.submenuOptions , { + internal: true , + parent: this , + meta: { fromItemDef: itemDef } , + outputX: x , + outputY: y , + outputWidth: width , + outputHeight: height , + items: itemDef.items , + noDraw: true + } ) ) ; + + this.redraw() ; + this.document.giveFocusTo( this.submenu ) ; + + this.submenu.on( 'submit' , this.onSubmenuSubmit ) ; +} ; + + + +ColumnMenu.prototype.clearSubmenu = function() { + if ( ! this.submenu ) { return false ; } + this.submenu.destroy() ; + this.submenu = null ; + return true ; +} ; + + + +ColumnMenu.prototype.onSubmenuSubmit = function( buttonValue , action , button ) { + button.once( 'blinked' , ( buttonValue_ , reserved , button_ ) => { + if ( this.submenuOptions.hideParent ) { this.children.forEach( e => e.hidden = false ) ; } + this.submenu.destroy() ; + this.document.giveFocusTo( this ) ; + + this.emit( 'blinked' , buttonValue_ , reserved , this ) ; + } ) ; + + this.emit( 'submit' , buttonValue , action , this ) ; +} ; + + + ColumnMenu.prototype.onParentResize = function() { if ( ! this.autoWidth && ! this.autoHeight ) { return ; } diff --git a/sample/document/column-menu-submenu-test.js b/sample/document/column-menu-submenu-test.js new file mode 100755 index 00000000..4bc19e5d --- /dev/null +++ b/sample/document/column-menu-submenu-test.js @@ -0,0 +1,182 @@ +#!/usr/bin/env node +/* + Terminal Kit + + Copyright (c) 2009 - 2021 Cédric Ronvel + + The MIT License (MIT) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +"use strict" ; + + + +const termkit = require( '../..' ) ; +const term = termkit.terminal ; + + + +term.clear() ; + +var document = term.createDocument( { palette: new termkit.Palette() } ) ; + + + +var columnMenu = new termkit.ColumnMenu( { + parent: document , + x: 0 , + y: 5 , + width: 20 , + pageMaxHeight: 5 , + //height: 5 , + blurLeftPadding: '^; ' , + focusLeftPadding: '^;^R> ' , + disabledLeftPadding: '^; ' , + paddingHasMarkup: true , + multiLineItems: true , + submenu: { + /* + disposition: 'overwrite' , + hideParent: true , + openOn: 'blinked' , + //*/ + //* + disposition: 'right' , + openOn: 'focus' , + //*/ + } , + buttonEvenBlurAttr: { bgColor: '@dark-gray' , color: 'white' , bold: true } , + buttonKeyBindings: { + ENTER: 'submit' , + CTRL_UP: 'submit' , + CTRL_DOWN: 'submit' + } , + buttonActionKeyBindings: { + CTRL_UP: 'up' , + CTRL_DOWN: 'down' + } , + items: [ + { + content: 'File' , + value: 'file' , + items: [ + { + content: 'Open' , + value: 'open' + } , + { + content: 'Save' , + value: 'save' + } + ] + } , + { + content: 'Edit' , + value: 'edit' , + items: [ + { + content: 'Copy' , + value: 'copy' + } , + { + content: 'Cut' , + value: 'cut' + } , + { + content: 'Paste' , + value: 'paste' + } + ] + } , + { + content: 'Tools' , + value: 'tools' , + items: [ + { + content: 'Decrunch' , + value: 'decrunch' + } + ] + } , + { + content: 'Help' , + value: 'help' , + items: [ + { + content: 'About' , + value: 'about' + } , + { + content: 'Manual' , + value: 'manual' + } + ] + } + ] +} ) ; + + + +var submitCount = 0 , focusCount = 0 ; + +function onSubmit( buttonValue , action ) { + //console.error( 'Submitted: ' , value ) ; + if ( buttonValue === 'view' ) { columnMenu.setItem( buttonValue , { content: 'bob' } ) ; } + + term.saveCursor() ; + term.moveTo.styleReset.eraseLine( 1 , 22 , 'Submitted #%i: %s %s\n' , submitCount ++ , buttonValue , action ) ; + term.restoreCursor() ; +} + +function onItemFocus( buttonValue , focus ) { + //console.error( 'Submitted: ' , value ) ; + term.saveCursor() ; + term.moveTo.styleReset.eraseLine( 1 , 24 , 'Item focus #%i: %s %s\n' , focusCount ++ , buttonValue , focus ) ; + term.restoreCursor() ; +} + +columnMenu.on( 'submit' , onSubmit ) ; +//columnMenu.on( 'blinked' , onSubmit ) ; +columnMenu.on( 'itemFocus' , onItemFocus ) ; + + + +//document.giveFocusTo( columnMenu ) ; +columnMenu.focusValue( 'edit' ) ; + +term.on( 'key' , function( key ) { + switch( key ) { + case 'CTRL_C' : + term.grabInput( false ) ; + term.hideCursor( false ) ; + term.styleReset() ; + term.clear() ; + process.exit() ; + break ; + case 'CTRL_D' : + columnMenu.draw() ; + break ; + case 'CTRL_R' : + columnMenu.redraw() ; + break ; + } +} ) ; +