diff --git a/README.md b/README.md index cfaabc9..639f878 100644 --- a/README.md +++ b/README.md @@ -193,14 +193,15 @@ The screen on which every other node renders. - **clearRegion(x1, x2, y1, y2)** - clear any region on the screen. - **fillRegion(attr, ch, x1, x2, y1, y2)** - fill any region with a character of a certain attribute. -- **focus(offset)** - focus element by offset of focusable elements. -- **focusPrev()** - focus previous element in the index. +- **focusOffset(offset)** - focus element by offset of focusable elements. +- **focusPrevious()** - focus previous element in the index. - **focusNext()** - focus next element in the index. - **focusPush(element)** - push element on the focus stack (equivalent to `screen.focused = el`). -- **focusPop()/focusLast()** - pop element off the focus stack. +- **focusPop()** - pop element off the focus stack. - **saveFocus()** - save the focused element. - **restoreFocus()** - restore the saved focused element. +- **rewindFocus()** - "rewind" focus to the last visible and attached element. - **key(name, listener)** - bind a keypress listener for a specific key. - **onceKey(name, listener)** - bind a keypress listener for a specific key once. diff --git a/lib/widget.js b/lib/widget.js index 4b0ed15..9640aa4 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -30,6 +30,7 @@ function Node(options) { options = options || {}; this.options = options; this.screen = this.screen + || options.screen || Screen.global || (function(){throw new Error('No active screen.')})(); this.parent = options.parent || null; @@ -38,19 +39,15 @@ function Node(options) { this.uid = Node.uid++; this.index = -1; + if (this.type !== 'screen') { + this.detached = true; + } + if (this.parent) { this.parent.append(this); } - if (!this.parent) { - this._detached = true; - } - (options.children || []).forEach(this.append.bind(this)); - - // if (this.type === 'screen' && !this.focused) { - // this.focused = this.children[0]; - // } } Node.uid = 0; @@ -60,37 +57,31 @@ Node.prototype.__proto__ = EventEmitter.prototype; Node.prototype.type = 'node'; Node.prototype.insert = function(element, i) { - var old = element.parent; + var self = this; element.detach(); element.parent = this; - if (this.type === 'screen' && !this.focused) { - this.focused = element; - } - - if (!~this.children.indexOf(element)) { - if (i === 0) { - this.children.unshift(element); - } else if (i === this.children.length) { - this.children.push(element); - } else { - this.children.splice(i, 0, element); - } + if (i === 0) { + this.children.unshift(element); + } else if (i === this.children.length) { + this.children.push(element); + } else { + this.children.splice(i, 0, element); } element.emit('reparent', this); this.emit('adopt', element); - if (!old) { - // element.emitDescendants('attach', function(el) { - // el._detached = false; - // }); - (function emit(el) { - el._detached = false; - el.emit('attach'); - if (el.children) el.children.forEach(emit); - })(element); + (function emit(el) { + var n = el.detached !== self.detached; + el.detached = self.detached; + if (n) el.emit('attach'); + el.children.forEach(emit); + })(element); + + if (!this.screen.focused) { + this.screen.focused = element; } }; @@ -115,12 +106,16 @@ Node.prototype.insertAfter = function(element, other) { Node.prototype.remove = function(element) { if (element.parent !== this) return; + var i = this.children.indexOf(element); + if (!~i) return; + + if (this.type !== 'screen') { + this.clearPos(); + } + element.parent = null; - var i = this.children.indexOf(element); - if (~i) { - this.children.splice(i, 1); - } + this.children.splice(i, 1); if (this.type !== 'screen') { i = this.screen.clickable.indexOf(element); @@ -129,23 +124,19 @@ Node.prototype.remove = function(element) { if (~i) this.screen.keyable.splice(i, 1); } - if (this.type === 'screen' && this.focused === element) { - this.focused = this.children[0]; - } - element.emit('reparent', null); this.emit('remove', element); - // element.emitDescendants('detach', function(el) { - // el._detached = true; - // }); (function emit(el) { - el._detached = true; - el.emit('detach'); - if (el.children) el.children.forEach(emit); + var n = el.detached !== true; + el.detached = true; + if (n) el.emit('detach'); + el.children.forEach(emit); })(element); - // this.clearPos(); + if (this.screen.focused === element) { + this.screen.rewindFocus(); + } }; Node.prototype.detach = function() { @@ -317,9 +308,7 @@ function Screen(options) { self.render(); (function emit(el) { el.emit('resize'); - if (el.children) { - el.children.forEach(emit); - } + el.children.forEach(emit); })(self); // self.emitDescendants('resize'); } @@ -1333,13 +1322,18 @@ Screen.prototype.codeAttr = function(code) { return '\x1b[' + out + 'm'; }; -Screen.prototype.focus = function(offset) { +Screen.prototype.focusOffset = function(offset) { var shown = this.keyable.filter(function(el) { return el.visible; - }); - if (!shown.length || !offset) return; + }).length; + + if (!shown || !offset) { + return; + } + var i = this.keyable.indexOf(this.focused); if (!~i) return; + if (offset > 0) { while (offset--) { if (++i > this.keyable.length - 1) i = 0; @@ -1352,28 +1346,33 @@ Screen.prototype.focus = function(offset) { if (!this.keyable[i].visible) offset++; } } + return this.keyable[i].focus(); }; -Screen.prototype.focusPrev = function() { - return this.focus(-1); +Screen.prototype.focusPrev = +Screen.prototype.focusPrevious = function() { + return this.focusOffset(-1); }; Screen.prototype.focusNext = function() { - return this.focus(1); + return this.focusOffset(1); }; Screen.prototype.focusPush = function(el) { + if (!el) return; + var old = this.history[this.history.length-1]; if (this.history.length === 10) { this.history.shift(); } this.history.push(el); - el._focus(); + el._focus(old); }; -Screen.prototype.focusLast = Screen.prototype.focusPop = function() { - return this.history.pop(); + var old = this.history.pop(); + this.history[this.history.length-1]._focus(old); + return old; }; Screen.prototype.saveFocus = function() { @@ -1387,6 +1386,25 @@ Screen.prototype.restoreFocus = function() { return this.focused; }; +Screen.prototype.rewindFocus = function() { + var old = this.history[this.history.length-1] + , el; + + while (this.history.length) { + el = this.history.pop(); + if (!el.detached && !el.visible) { + this.history.push(el); + el._focus(old); + return el; + } + } + + if (old) { + old.emit('blur'); + this.screen.emit('element blur', old); + } +}; + Screen.prototype.__defineGetter__('focused', function() { return this.history[this.history.length-1]; }); @@ -1838,8 +1856,9 @@ Element.prototype.hide = function() { this.clearPos(); this.hidden = true; this.emit('hide'); - var below = this.screen.history[this.screen.history.length-2]; - if (below && this.screen.focused === this) below.focus(); + if (this.screen.focused === this) { + this.screen.rewindFocus(); + } }; Element.prototype.show = function() { @@ -1853,12 +1872,10 @@ Element.prototype.toggle = function() { }; Element.prototype.focus = function() { - this.screen.focused = this; + return this.screen.focused = this; }; -Element.prototype._focus = function() { - var old = this.screen.history[this.screen.history.length-2]; - +Element.prototype._focus = function(old) { // Find a scrollable ancestor if we have one. var el = this; while (el = el.parent) { @@ -1876,17 +1893,20 @@ Element.prototype._focus = function() { el.scrollTo(ryi); this.screen.render(); } else if (ryi >= el.childBase + visible) { - el.scrollTo(ryi); + el.scrollTo(ryi + this.height); this.screen.render(); } else if (ryl >= el.childBase + visible) { - el.scrollTo(ryi); + el.scrollTo(ryi + this.height); this.screen.render(); } } - if (old) old.emit('blur', this); + if (old) { + old.emit('blur', this); + this.screen.emit('element blur', old, this); + } + this.emit('focus', old); - this.screen.emit('element blur', old, this); this.screen.emit('element focus', old, this); }; @@ -1896,6 +1916,25 @@ Element.prototype.setContent = function(content, noClear) { this.parseContent(); }; +Element.prototype.setContent_ = function(content, noClear) { + var old, pos; + + if (!noClear) { + old = this._pcontent; + pos = this._getCoords(); + } + + this.content = content || ''; + this.parseContent(); + + if (!noClear && pos && this._pcontent !== old) { + // this.clearPos(pos); + this.screen.clearRegion( + pos.xi, pos.xl, + pos.yi, pos.yl); + } +}; + Element.prototype.getContent = function() { return this._clines.fake.join('\n'); }; @@ -2190,7 +2229,7 @@ Element.prototype.__defineGetter__('visible', function() { return true; }); -Element.prototype.__defineGetter__('detached', function() { +Element.prototype.__defineGetter__('_detached', function() { var el = this; do { if (el.type === 'screen') return false; @@ -2732,12 +2771,16 @@ Element.prototype._getCoords = function(get) { if (el) { ppos = this.parent.lpos; + // The shrink option can cause a stack overflow + // by calling _getCoords on the child again. + // if (!get && !this.parent.shrink) { + // ppos = this.parent._getCoords(); + // } + if (!ppos) return; - if (this.parent.scrollable) { - yi -= ppos.base; - yl -= ppos.base; - } + yi -= ppos.base; + yl -= ppos.base; if (yi < ppos.yi + this.parent.itop) { if (yl - 1 < ppos.yi + this.parent.itop) { @@ -4205,9 +4248,12 @@ Textarea.prototype.readInput = function(callback) { var self = this , focused = this.screen.focused === this; + // We need to maintain an array of + // callbacks for legacy reasons. if (this._callback) { return callback ? this._callbacks.push(callback) : null; } + this._callbacks = callback ? [callback] : []; if (!focused) { @@ -4225,11 +4271,14 @@ Textarea.prototype.readInput = function(callback) { if (fn.done) return; fn.done = true; - self.removeListener('keypress', self.__listener); - self.removeListener('blur', self._callback); - delete self.__listener; delete self._callback; + self.removeListener('keypress', self.__listener); + delete self.__listener; + + self.removeListener('blur', self.__callback); + delete self.__callback; + self.screen.program.hideCursor(); self.screen.grabKeys = false; @@ -4257,7 +4306,9 @@ Textarea.prototype.readInput = function(callback) { this.__listener = this._listener.bind(this); this.on('keypress', this.__listener); - this.on('blur', this._callback); + + this.__callback = this._callback.bind(this, null, null); + this.on('blur', this.__callback); }; Textarea.prototype._listener = function(ch, key) { @@ -4779,16 +4830,18 @@ function Checkbox(options) { } this.on('focus', function(old) { - var lpos = self._getCoords(); + var lpos = self.lpos; if (!lpos) return; - self.screen.program.saveCursor(); + //self.screen.program.saveCursor(); + self.screen.program.lsaveCursor('checkbox'); self.screen.program.cup(lpos.yi, lpos.xi + 1); self.screen.program.showCursor(); }); this.on('blur', function() { - self.screen.program.hideCursor(); - self.screen.program.restoreCursor(); + //self.screen.program.hideCursor(); + //self.screen.program.restoreCursor(); + self.screen.program.lrestoreCursor('checkbox', true); }); } diff --git a/test/widget-form.js b/test/widget-form.js index 57de17e..ab22b33 100644 --- a/test/widget-form.js +++ b/test/widget-form.js @@ -201,6 +201,16 @@ var output = blessed.scrollabletext({ content: 'foobar' }); +var bottom = blessed.line({ + parent: form, + type: 'line', + orientation: 'horizontal', + left: 0, + right: 0, + top: 50, + fg: 'blue' +}); + screen.key('q', function() { return process.exit(0); });