diff --git a/core/edit_text_view.js b/core/edit_text_view.js new file mode 100644 index 00000000..89f8b6e9 --- /dev/null +++ b/core/edit_text_view.js @@ -0,0 +1,66 @@ +/* jslint node: true */ +'use strict'; + +var TextView = require('./text_view.js').TextView; +var miscUtil = require('./misc_util.js'); +var strUtil = require('./string_util.js'); +var util = require('util'); +var assert = require('assert'); + +exports.EditTextView = EditTextView; + +function EditTextView(client, options) { + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + + TextView.call(this, client, options); + + this.clientBackspace = function() { + this.client.term.write('\b \b'); + }; +} + +util.inherits(EditTextView, TextView); + +EditTextView.prototype.onKeyPress = function(key, isSpecial) { + assert(this.hasFocus); + assert(this.acceptsInput); + + if(isSpecial) { + return; + } + + assert(1 === key.length); + + if(this.text.length < this.options.maxLength) { + key = strUtil.stylizeString(key, this.textStyle); + + this.text += key; + + if(this.textMaskChar) { + this.client.term.write(this.textMaskChar); + } else { + this.client.term.write(key); + } + } +}; + +EditTextView.prototype.onSpecialKeyPress = function(keyName) { + assert(this.hasFocus); + assert(this.acceptsInput); + assert(this.specialKeyMap); + + if(this.isSpecialKeyMapped('backspace', keyName)) { + if(this.text.length > 0) { + this.text = this.text.substr(0, this.text.length - 1); + this.clientBackspace(); + } + } else if(this.isSpecialKeyMapped('enter', keyName)) { + if(this.multiLine) { + } else { + this.emit('action', 'accepted'); + } + } else if(this.isSpecialKeyMapped('next', keyName)) { + this.emit('action', 'next'); + } +}; \ No newline at end of file diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js new file mode 100644 index 00000000..9fbbf488 --- /dev/null +++ b/core/mci_view_factory.js @@ -0,0 +1,44 @@ +/* jslint node: true */ +'use strict'; + +var TextView = require('./text_view.js').TextView; +var EditTextView = require('./edit_text_view.js').EditTextView; +var assert = require('assert'); + +exports.MCIViewFactory = MCIViewFactory; + +function MCIViewFactory(client) { + this.client = client; +} + +MCIViewFactory.prototype.createFromMCI = function(mci) { + assert(mci.code); + assert(mci.id > 0); + assert(mci.position); + + var view; + var options = { + id : mci.id, + color : mci.color, + focusColor : mci.focusColor, + position : { x : mci.position[0], y : mci.position[1] }, + }; + + switch(mci.code) { + case 'EV' : + if(mci.args.length > 0) { + options.maxLength = mci.args[0]; + options.dimens = { width : options.maxLength }; + } + + if(mci.args.length > 1) { + options.textStyle = mci.args[1]; + } + + view = new EditTextView(this.client, options); + break; + } + + return view; +}; + diff --git a/core/text_view.js b/core/text_view.js index 084565dc..0f0ee2df 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -33,7 +33,7 @@ function TextView(client, options) { this.isPasswordTextStyle = 'P' === self.textStyle || 'password' === self.textStyle; if(this.isPasswordTextStyle) { - this.passwordMaskChar = miscUtil.valueWithDefault(this.options.passwordMaskChar, '*').substr(0, 1); + this.textMaskChar = miscUtil.valueWithDefault(this.options.textMaskChar, '*').substr(0, 1); } } @@ -47,12 +47,20 @@ TextView.prototype.redraw = function() { this.client.term.write(ansi.sgr(color.flags, color.fg, color.bg)); if(this.isPasswordTextStyle) { - this.client.term.write(strUtil.pad(new Array(this.text.length).join(this.passwordMaskChar), this.dimens.width)); + this.client.term.write(strUtil.pad(new Array(this.text.length).join(this.textMaskChar), this.dimens.width)); } else { this.client.term.write(strUtil.pad(this.text, this.dimens.width)); } }; +TextView.prototype.setFocus = function(focused) { + TextView.super_.prototype.setFocus.call(this, focused); + + this.client.term.write(ansi.goto(this.position.x, this.position.y)); + this.redraw(); + this.client.term.write(ansi.goto(this.position.x, this.position.y + this.text.length)); +}; + TextView.prototype.setText = function(text) { this.text = text; @@ -62,7 +70,7 @@ TextView.prototype.setText = function(text) { this.text = strUtil.stylizeString(this.text, this.textStyle); - if(!this.multiLine) { + if(!this.multiLine && !this.dimens.width) { this.dimens.width = this.text.length; } }; diff --git a/core/view.js b/core/view.js index a7a8f366..c4f13b15 100644 --- a/core/view.js +++ b/core/view.js @@ -26,16 +26,21 @@ function View(client, options) { this.client = client; this.options = options || {}; - this.acceptsFocus = false; - this.acceptsInput = false; + this.acceptsFocus = options.acceptsFocus || false; + this.acceptsInput = options.acceptsInput || false; this.position = { x : 0, y : 0 }; this.dimens = { height : 1, width : 0 }; + if(this.options.id) { + this.setId(this.options.id); + } + if(this.options.position) { this.setPosition(this.options.position); } + // :TODO: Don't allow width/height > client.term if(this.options.dimens && this.options.dimens.height) { this.dimens.height = this.options.dimens.height; } @@ -50,10 +55,17 @@ function View(client, options) { if(this.acceptsInput) { this.specialKeyMap = this.options.specialKeyMap || VIEW_SPECIAL_KEY_MAP_DEFAULT; } + + this.isSpecialKeyMapped = function(keySet, keyName) { + return this.specialKeyMap[keySet].indexOf(keyName) > -1; + }; } util.inherits(View, events.EventEmitter); +View.prototype.setId = function(id) { + this.id = id; +}; View.prototype.setPosition = function(pos) { // @@ -66,13 +78,12 @@ View.prototype.setPosition = function(pos) { this.position.x = pos.x; this.position.y = pos.y; } else if(2 === arguments.length) { - var x = parseInt(arguments[0], 10); - var y = parseInt(arguments[1], 10); - if(!isNaN(x) && !isNaN(y)) { - this.position.x = x; - this.position.y = y; - } + this.position.x = parseInt(arguments[0], 10); + this.position.y = parseInt(arguments[1], 10); } + + assert(!(isNaN(this.position.x))); + assert(!(isNaN(this.position.y))); assert(this.position.x > 0 && this.position.x < this.client.term.termHeight); assert(this.position.y > 0 && this.position.y < this.client.term.termWidth); diff --git a/core/view_controller.js b/core/view_controller.js new file mode 100644 index 00000000..bbd30f06 --- /dev/null +++ b/core/view_controller.js @@ -0,0 +1,151 @@ +/* jslint node: true */ +'use strict'; + +var events = require('events'); +var util = require('util'); +var assert = require('assert'); +var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; + +exports.ViewController = ViewController; + +function ViewController(client) { + events.EventEmitter.call(this); + + var self = this; + + this.client = client; + this.views = {}; // map of ID -> view + + this.onClientKeyPress = function(key, isSpecial) { + if(isSpecial) { + return; + } + + if(self.focusedView && self.focusedView.acceptsInput) { + key = 'string' === typeof key ? key : key.toString(); + self.focusedView.onKeyPress(key, isSpecial); + } + }; + + this.onClientSpecialKeyPress = function(keyName) { + if(self.focusedView && self.focusedView.acceptsInput) { + self.focusedView.onSpecialKeyPress(keyName); + } + }; + + this.onViewAction = function(action) { + switch(action) { + case 'next' : + self.emit('action', { view : this, action : action }); + self.nextFocus(); + break; + + case 'accepted' : + // :TODO: check if id is submit, etc. + self.nextFocus(); + break; + } + }; + + this.attachClientEvents(); +} + +util.inherits(ViewController, events.EventEmitter); + +ViewController.prototype.attachClientEvents = function() { + if(this.attached) { + return; + } + + this.client.on('key press', this.onClientKeyPress); + this.client.on('special key', this.onClientSpecialKeyPress); + + this.attached = true; +}; + +ViewController.prototype.detachClientEvents = function() { + if(!this.attached) { + return; + } + + this.client.removeListener('key press', this.onClientKeyPress); + this.client.removeListener('special key', this.onClientSpecialKeyPress); + + this.attached = false; +}; + +ViewController.prototype.viewExists = function(id) { + return id in this.views; +}; + +ViewController.prototype.addView = function(view) { + assert(!this.viewExists(view.id), 'View with ID ' + view.id + ' already exists'); + + this.views[view.id] = view; +}; + +ViewController.prototype.getView = function(id) { + return this.views[id]; +}; + +ViewController.prototype.switchFocus = function(id) { + if(this.focusedView && this.focusedView.acceptsFocus) { + this.focusedView.setFocus(false); + } + + var view = this.getView(id); + if(view && view.acceptsFocus) { + this.focusedView = view; + this.focusedView.setFocus(true); + } + + // :TODO: Probably log here +}; + +ViewController.prototype.nextFocus = function() { + if(!this.focusedView) { + this.switchFocus(this.views[this.firstId].id); + } else { + var nextId = this.views[this.focusedView.id].nextId; + this.switchFocus(nextId); + } +}; + +ViewController.prototype.setViewOrder = function(order) { + var viewIdOrder = order || []; + + if(0 === viewIdOrder.length) { + for(var id in this.views) { + viewIdOrder.push(id); + } + + viewIdOrder.sort(); + } + + var view; + var count = viewIdOrder.length - 1; + for(var i = 0; i < count; ++i) { + this.views[viewIdOrder[i]].nextId = viewIdOrder[i + 1]; + } + + this.firstId = viewIdOrder[0]; + var lastId = viewIdOrder[viewIdOrder.length - 1]; + this.views[lastId].nextId = this.firstId; + +}; + +ViewController.prototype.loadFromMCIMap = function(mciMap) { + var factory = new MCIViewFactory(this.client); + var self = this; + + Object.keys(mciMap).forEach(function onMciEntry(name) { + var mci = mciMap[name]; + var view = factory.createFromMCI(mci); + + if(view) { + view.on('action', self.onViewAction); + self.addView(view); + } + }); +}; + diff --git a/mods/matrix.js b/mods/matrix.js index e5c8940b..072f090b 100644 --- a/mods/matrix.js +++ b/mods/matrix.js @@ -9,6 +9,8 @@ var user = require('../core/user.js'); //var view = require('../core/view.js'); var textView = require('../core/text_view.js'); +var editTextView = require('../core/edit_text_view.js'); +var viewController = require('../core/view_controller.js'); exports.moduleInfo = { name : 'Matrix', @@ -34,14 +36,48 @@ function entryPoint(client) { return; } + /* var tv = new textView.TextView(client, { position : [5, 5], text : 'Hello, World!', textStyle : 'password', - maxLength : 10 + maxLength : 10, + id : 1, }); tv.redraw(); + + var etv = new editTextView.EditTextView(client, { + position : [10, 10], + textStyle : 'upper', + maxLength : 20, + dimens : { width : 30 }, + text : 'default', + color : { flags : 0, fg : 31, bg : 40 }, + focusColor : { flags : 1, fg : 37, bg : 44 }, + id : 2, + }); + + etv.redraw();*/ + + var vc = new viewController.ViewController(client); + vc.loadFromMCIMap(mci); + vc.setViewOrder(); + vc.switchFocus(1); + //vc.addView(etv); + //vc.switchFocus(2); + + /* + + client.on('key press', function onKp(key, isSpecial) { + key = 'string' === typeof key ? key : key.toString(); + etv.onKeyPress(key, isSpecial); + }); + + client.on('special key', function onSK(keyName) { + etv.onSpecialKeyPress(keyName); + }); + */ /* var vc = new view.ViewsController(client);