From a98bef4c9001ec444b83fdc263f238862d2a54e9 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 19 Jul 2013 09:17:21 -0500 Subject: [PATCH] more scrolling improvements. scrolling form. --- lib/widget.js | 127 ++++++++++++++++++++++++++------------------ test/widget-form.js | 15 +++++- 2 files changed, 90 insertions(+), 52 deletions(-) diff --git a/lib/widget.js b/lib/widget.js index 303bc01..fe19fe3 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -1614,6 +1614,32 @@ Element.prototype.toggle = function() { Element.prototype.focus = function() { var old = this.screen.focused; this.screen.focused = this; + + // Find a scrollable ancestor if we have one. + var el = this; + while (el = el.parent) { + if (el.childBase != null) break; + } + + // If we're in a scrollable element, + // automatically scroll to the focused element. + if (el) { + var ryi = this.top - el.top - el.itop + , ryl = this.top + this.height - el.top - el.ibottom + , visible = el.height - el.iheight; + + if (ryi < el.childBase) { + el.scrollTo(ryi); + this.screen.render(); + } else if (ryi >= el.childBase + visible) { + el.scrollTo(ryi); + this.screen.render(); + } else if (ryl >= el.childBase + visible) { + el.scrollTo(ryi); + this.screen.render(); + } + } + old.emit('blur', this); this.emit('focus', old); this.screen.emit('element blur', old, this); @@ -2374,35 +2400,39 @@ Box.prototype._getCoords = function(get) { , coords , v; + // Find a scrollable ancestor if we have one. + var el = this; + while (el = el.parent) { + if (el.childBase != null) break; + } + // Check to make sure we're visible and // inside of the visible scroll area. - if (this.parent.childBase != null - && (!this.parent.items - || ~this.parent.items.indexOf(this))) { - ryi = yi - this.parent._getTop(get) - this.parent.itop; - ryl = yl - this.parent._getTop(get) - this.parent.ibottom; - visible = this.parent._getHeight(get) - this.parent.iheight; + if (el && (!el.items || ~el.items.indexOf(this))) { + ryi = yi - el._getTop(get) - el.itop; + ryl = yl - el._getTop(get) - el.ibottom; + visible = el._getHeight(get) - el.iheight; - if (ryi < this.parent.childBase) { - if (ryl > this.parent.childBase) { - // Is partially covered above. TODO: Improve. - v = ryl - this.parent.childBase; + if (ryi < el.childBase) { + if (ryl > el.childBase) { + // Is partially covered above. + v = ryl - el.childBase; yi += (ryl - ryi) - v; } else { // Is above. return; } - } else if (ryi >= this.parent.childBase + visible) { + } else if (ryi >= el.childBase + visible) { // Is below. return; - } else if (ryl >= this.parent.childBase + visible) { - // Is partially covered below. TODO: Improve. - v = this.parent.childBase + visible + (yl - yi) - ryl; + } else if (ryl >= el.childBase + visible) { + // Is partially covered below. + v = el.childBase + visible + (yl - yi) - ryl; yl = yi + v; } - yi -= this.parent.childBase; - yl -= this.parent.childBase; + yi -= el.childBase; + yl -= el.childBase; } // Attempt to shrink the element base on the @@ -2455,11 +2485,12 @@ Box.prototype.render = function() { // If we're in a scrollable text box, check to // see which attributes this line starts with. if (this.contentIndex != null && this.childBase != null) { - if (this._clines.length > this.childBase) { - attr = this._clines.attr[this.childBase]; - } else { - attr = this._clines.attr[this._clines.length - 1]; - } + // if (this._clines.length > this.childBase) { + // attr = this._clines.attr[this.childBase]; + // } else { + // attr = this._clines.attr[this._clines.length - 1]; + // } + attr = this._clines.attr[this.childBase]; } if (this.border) xi++, xl--, yi++, yl--; @@ -3081,22 +3112,22 @@ function List(options) { return; } if (options.vi && key.name === 'u' && key.ctrl) { - self.move(-((self.height - (self.border ? 2 : 0)) / 2) | 0); + self.move(-((self.height - self.iheight) / 2) | 0); self.screen.render(); return; } if (options.vi && key.name === 'd' && key.ctrl) { - self.move((self.height - (self.border ? 2 : 0)) / 2 | 0); + self.move((self.height - self.iheight) / 2 | 0); self.screen.render(); return; } if (options.vi && key.name === 'b' && key.ctrl) { - self.move(-(self.height - (self.border ? 2 : 0))); + self.move(-(self.height - self.iheight)); self.screen.render(); return; } if (options.vi && key.name === 'f' && key.ctrl) { - self.move(self.height - (self.border ? 2 : 0)); + self.move(self.height - self.iheight); self.screen.render(); return; } @@ -3109,7 +3140,7 @@ function List(options) { // TODO: Maybe use Math.min(this.items.length, // ... for calculating visible items elsewhere. var visible = Math.min( - self.height - (self.border ? 2 : 0), + self.height - self.iheight, self.items.length) / 2 | 0; self.move(self.childBase + visible - self.selected); self.screen.render(); @@ -3118,7 +3149,7 @@ function List(options) { if (options.vi && key.name === 'l' && key.shift) { // XXX This goes one too far on lists with an odd number of items. self.down(self.childBase - + Math.min(self.height - (self.border ? 2 : 0), self.items.length) + + Math.min(self.height - self.iheight, self.items.length) - self.selected); self.screen.render(); return; @@ -3438,7 +3469,7 @@ function Form(options) { options = options || {}; - Box.call(this, options); + ScrollableBox.call(this, options); if (options.keys) { this.screen._listenKeys(this); @@ -3458,11 +3489,11 @@ function Form(options) { || (options.vi && key.name === 'j')) { if (el.type === 'textbox' || el.type === 'textarea') { if (key.name === 'j') return; - self.screen.grabKeys = false; if (key.name === 'tab') { // Workaround, since we can't stop the tab from being added. el.emit('keypress', null, { name: 'backspace' }); } + el.emit('keypress', '\x1b', { name: 'escape' }); } self.focusNext(); return; @@ -3473,7 +3504,7 @@ function Form(options) { || (options.vi && key.name === 'k')) { if (el.type === 'textbox' || el.type === 'textarea') { if (key.name === 'k') return; - self.screen.grabKeys = false; + el.emit('keypress', '\x1b', { name: 'escape' }); } self.focusPrevious(); return; @@ -3487,7 +3518,7 @@ function Form(options) { } } -Form.prototype.__proto__ = Box.prototype; +Form.prototype.__proto__ = ScrollableBox.prototype; Form.prototype.type = 'form'; @@ -3677,22 +3708,21 @@ function Textbox(options) { this.secret = options.secret; this.censor = options.censor; - this.on('resize', updateCursor); - this.on('move', updateCursor); - - function updateCursor() { - if (self.screen.focused !== self) return; - self.screen.program.cup( - self.top + (self.border ? 1 : 0), - self.left + (self.border ? 1 : 0) - + self.value.length); - } + this.__updateCursor = this.updateCursor.bind(this); + this.on('resize', this.__updateCursor); + this.on('move', this.__updateCursor); } Textbox.prototype.__proto__ = Input.prototype; Textbox.prototype.type = 'textbox'; +Textbox.prototype.updateCursor = function() { + if (this.screen.focused !== this) return; + this.screen.program.cup(this.top + this.itop, + this.left + this.ileft + this.value.length); +}; + Textbox.prototype.input = Textbox.prototype.readInput = Textbox.prototype.setInput = function(callback) { @@ -3706,12 +3736,7 @@ Textbox.prototype.setInput = function(callback) { this.screen.grabKeys = true; - // Could possibly save and restore cursor. - - this.screen.program.cup( - this.top + this.itop, - this.left + this.ileft - + this.value.length); + this.updateCursor(); this.screen.program.showCursor(); this.screen.program.sgr('normal'); @@ -3801,6 +3826,7 @@ Textbox.prototype.render = function() { // setContent is necessary to clear the area in case // .shrink is being used and the new content is smaller. // Could technically optimize this. + if (!this._getCoords(true)) return; if (this.secret) { this.setContent(''); return this._render(); @@ -4134,11 +4160,8 @@ ProgressBar.prototype.type = 'progress-bar'; ProgressBar.prototype._render = ProgressBar.prototype.render; ProgressBar.prototype.render = function() { - // NOTE: Maybe move this `hidden` check - // down below `stop` check and return `ret`. - if (this.hidden) return; - var ret = this._render(); + if (!ret) return; var xi = ret.xi , xl = ret.xl @@ -4412,6 +4435,7 @@ Checkbox.prototype.type = 'checkbox'; Checkbox.prototype._render = Checkbox.prototype.render; Checkbox.prototype.render = function() { + if (!this._getCoords(true)) return; if (this.type === 'radio-button') { this.setContent('(' + (this.checked ? '*' : ' ') + ') ' + this.text); } else { @@ -4981,6 +5005,7 @@ Listbar.prototype.setItems = function(commands) { Listbar.prototype._render = Listbar.prototype.render; Listbar.prototype.render = function() { + if (!this._getCoords(true)) return; var self = this , drawn = 0; diff --git a/test/widget-form.js b/test/widget-form.js index 1ea7725..def57b8 100644 --- a/test/widget-form.js +++ b/test/widget-form.js @@ -1,5 +1,5 @@ var blessed = require('../') - , screen = blessed.screen(); + , screen = blessed.screen({ dump: __dirname + '/form.log' }); var form = blessed.form({ parent: screen, @@ -84,6 +84,19 @@ var check = blessed.checkbox({ content: 'check' }); +var check2 = blessed.checkbox({ + parent: form, + mouse: true, + keys: true, + shrink: true, + bg: 'magenta', + height: 1, + left: 28, + top: 10, + name: 'foooooooo2', + content: 'foooooooo2' +}); + var submit = blessed.button({ parent: form, mouse: true,