diff --git a/core/client.js b/core/client.js index e142d2d8..390d417e 100644 --- a/core/client.js +++ b/core/client.js @@ -17,13 +17,18 @@ exports.Client = Client; //var ANSI_CONTROL_REGEX = /(?:(?:\u001b\[)|\u009b)(?:(?:[0-9]{1,3})?(?:(?:;[0-9]{0,3})*)?[A-M|f-m])|\u001b[A-M]/g; // :TODO: Move all of the key stuff to it's own module + +// +// Resources & Standards: +// * http://www.ansi-bbs.org/ansi-bbs-core-server.html +// var ANSI_KEY_NAME_MAP = { - 0x08 : 'backspace', - 0x09 : 'tab', + 0x08 : 'backspace', // BS + 0x09 : 'tab', // 0x7f : 'del', 0x1b : 'esc', 0x0d : 'enter', - 0x19 : 'ctrl-y' + 0x19 : 'end of medium', // EM / CTRL-Y }; var ANSI_KEY_CSI_NAME_MAP = { diff --git a/core/edit_text_view.js b/core/edit_text_view.js index 3028e0b8..146693ea 100644 --- a/core/edit_text_view.js +++ b/core/edit_text_view.js @@ -37,7 +37,6 @@ EditTextView.prototype.onKeyPress = function(key, isSpecial) { assert(1 === key.length); - // :TODO: how to handle justify left/center? if(this.text.length < this.maxLength) { key = strUtil.stylizeString(key, this.textStyle); @@ -61,8 +60,6 @@ EditTextView.prototype.onKeyPress = function(key, isSpecial) { }; EditTextView.prototype.onSpecialKeyPress = function(keyName) { - // :TODO: handle 'enter' & others for multiLine - if(this.isSpecialKeyMapped('backspace', keyName)) { if(this.text.length > 0) { this.text = this.text.substr(0, this.text.length - 1); @@ -76,10 +73,10 @@ EditTextView.prototype.onSpecialKeyPress = function(keyName) { } } } - } else if(this.isSpecialKeyMapped('clear', keyName)) { - // :TODO: this doesn't work right at all: - //this.setText(''); - //this.client.term.write(ansi.goto(this.position.x, this.position.y)); + } else if(this.isSpecialKeyMapped('clearLine', keyName)) { + this.text = ''; + this.cursorPos.x = 0; + this.setFocus(true); // resetting focus will redraw & adjust cursor } diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index ba761165..aac8c93f 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -6,6 +6,7 @@ var EditTextView = require('./edit_text_view.js').EditTextView; var ButtonView = require('./button_view.js').ButtonView; var VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; var SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; +var ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; var Config = require('./config.js').config; var packageJson = require('../package.json'); @@ -177,6 +178,14 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { view = new SpinnerMenuView(options); break; + case 'TM' : + setOption(0, 'textStyle'); + + setFocusOption(0, 'focusTextStyle') + + view = new ToggleMenuView(options); + break; + default : options.text = this.getPredefinedViewLabel(mci.code); if(options.text) { diff --git a/core/menu_view.js b/core/menu_view.js index aab10b87..21a47dbe 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -4,8 +4,10 @@ var View = require('./view.js').View; var ansi = require('./ansi_term.js'); var miscUtil = require('./misc_util.js'); + var util = require('util'); var assert = require('assert'); +var _ = require('lodash'); exports.MenuView = MenuView; @@ -23,6 +25,10 @@ function MenuView(options) { this.items = []; } + this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); + + this.setHotKeys(options.hotKeys); + this.focusedItemIndex = options.focusedItemIndex || 0; this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; @@ -35,6 +41,7 @@ function MenuView(options) { this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); this.justify = options.justify || 'none'; + /* this.moveSelection = function(fromIndex, toIndex) { assert(!self.positionCacheExpired); assert(fromIndex >= 0 && fromIndex <= self.items.length); @@ -47,6 +54,7 @@ function MenuView(options) { self.focusedItemIndex = toIndex; self.drawItem(toIndex); }; + */ /* this.cachePositions = function() { @@ -97,3 +105,16 @@ MenuView.prototype.setItems = function(items) { } }; +MenuView.prototype.setHotKeys = function(hotKeys) { + if(_.isObject(hotKeys)) { + if(this.caseInsensitiveHotKeys) { + this.hotKeys = {}; + for(var key in hotKeys) { + this.hotKeys[key.toLowerCase()] = hotKeys[key]; + } + } else { + this.hotKeys = hotKeys; + } + } +} + diff --git a/core/toggle_menu_view.js b/core/toggle_menu_view.js new file mode 100644 index 00000000..a5e7d21a --- /dev/null +++ b/core/toggle_menu_view.js @@ -0,0 +1,109 @@ +/* jslint node: true */ +'use strict'; + +var MenuView = require('./menu_view.js').MenuView; +var ansi = require('./ansi_term.js'); +var strUtil = require('./string_util.js'); + +var util = require('util'); +var assert = require('assert'); +var _ = require('lodash'); + +exports.ToggleMenuView = ToggleMenuView; + +function ToggleMenuView (options) { + options.cursor = options.cursor || 'hide'; + + MenuView.call(this, options); + + var self = this; + + this.cachePositions = function() { + self.positionCacheExpired = false; + }; + + this.updateSelection = function() { + assert(!self.positionCacheExpired); + assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); + + self.redraw(); + }; +} + +util.inherits(ToggleMenuView, MenuView); + +ToggleMenuView.prototype.redraw = function() { + ToggleMenuView.super_.prototype.redraw.call(this); + + this.cachePositions(); + + this.client.term.write(this.getANSIColor(this.hasFocus ? this.getFocusColor() : this.getColor())); + + assert(this.items.length === 2); + for(var i = 0; i < 2; i++) { + var item = this.items[i]; + var text = strUtil.stylizeString(item.text, i === this.focusedItemIndex ? this.focusTextStyle : this.textStyle); + + if(1 === i) { + this.client.term.write(this.getANSIColor(this.getColor()) + ' / '); // :TODO: We need a color for this!!! + } + + this.client.term.write(this.getANSIColor(i === this.focusedItemIndex ? this.getFocusColor() : this.getColor())); + this.client.term.write(text); + } +} + +ToggleMenuView.prototype.setFocus = function(focused) { + ToggleMenuView.super_.prototype.setFocus.call(this, focused); + + this.redraw(); +}; + +ToggleMenuView.prototype.onKeyPress = function(key, isSpecial) { + if(isSpecial || !this.hotKeys) { + return; + } + + assert(1 === key.length); + + var keyIndex = this.hotKeys[this.caseInsensitiveHotKeys ? key.toLowerCase() : key]; + if(!_.isUndefined(keyIndex)) { + this.focusedItemIndex = keyIndex; + this.updateSelection(); + } + + ToggleMenuView.super_.prototype.onKeyPress.call(this, key, isSpecial); +} + +ToggleMenuView.prototype.onSpecialKeyPress = function(keyName) { + + if(this.isSpecialKeyMapped('right', keyName) || this.isSpecialKeyMapped('down', keyName)) { + if(this.items.length - 1 === this.focusedItemIndex) { + this.focusedItemIndex = 0; + } else { + this.focusedItemIndex++; + } + } else if(this.isSpecialKeyMapped('left', keyName) || this.isSpecialKeyMapped('up', keyName)) { + if(0 === this.focusedItemIndex) { + this.focusedItemIndex = this.items.length - 1; + } else { + this.focusedItemIndex--; + } + } + + this.updateSelection(); + + ToggleMenuView.super_.prototype.onSpecialKeyPress.call(this, keyName); +} + +ToggleMenuView.prototype.getData = function() { + return this.focusedItemIndex; +}; + +ToggleMenuView.prototype.setItems = function(items) { + ToggleMenuView.super_.prototype.setItems.call(this, items); + + this.items = this.items.splice(0, 2); // switch/toggle only works with two elements + + this.dimens.width = this.items.join(' / ').length; // :TODO: allow configurable seperator +} diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index d58a632b..1a87995f 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -16,7 +16,7 @@ function VerticalMenuView(options) { var self = this; - this.itemSpacing = 3; + this.itemSpacing = 3; // :TODO: bring from options/configurable this.calculateDimens = function() { if(!self.dimens || !self.dimens.width) { @@ -54,6 +54,19 @@ function VerticalMenuView(options) { } }; + this.changeSelection = function(fromIndex, toIndex) { + assert(!self.positionCacheExpired); + assert(fromIndex >= 0 && fromIndex <= self.items.length); + assert(toIndex >= 0 && toIndex <= self.items.length); + + self.items[fromIndex].focused = false; + self.drawItem(fromIndex); + + self.items[toIndex].focused = true; + self.focusedItemIndex = toIndex; + self.drawItem(toIndex); + }; + this.drawItem = function(index) { assert(!this.positionCacheExpired); @@ -91,6 +104,7 @@ VerticalMenuView.prototype.setFocus = function(focused) { this.redraw(); }; + VerticalMenuView.prototype.onSpecialKeyPress = function(keyName) { var prevFocusedItemIndex = this.focusedItemIndex; @@ -110,7 +124,7 @@ VerticalMenuView.prototype.onSpecialKeyPress = function(keyName) { } if(prevFocusedItemIndex !== this.focusedItemIndex) { - this.moveSelection(prevFocusedItemIndex, this.focusedItemIndex); + this.changeSelection(prevFocusedItemIndex, this.focusedItemIndex); } VerticalMenuView.super_.prototype.onSpecialKeyPress.call(this, keyName); diff --git a/core/view.js b/core/view.js index 05a198f2..0c53a5bf 100644 --- a/core/view.js +++ b/core/view.js @@ -18,10 +18,11 @@ var VIEW_SPECIAL_KEY_MAP_DEFAULT = { next : [ 'tab' ], up : [ 'up arrow' ], down : [ 'down arrow' ], - - clear : [ 'ctrl-y' ], end : [ 'end' ], home : [ 'home' ], + left : [ 'left arrow' ], + right : [ 'right arrow' ], + clearLine : [ 'end of medium' ], }; function View(options) { diff --git a/core/view_controller.js b/core/view_controller.js index 5c7d08c0..9a1d7385 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -203,6 +203,11 @@ function ViewController(options) { view.textMaskChar = self.client.currentThemeInfo.getPasswordChar(); } + value = getViewProp('hotkeys'); + if(_.isObject(value)) { + view.setHotKeys(value); + } + value = getViewProp('submit'); if(_.isBoolean(value)) { diff --git a/mods/art/demo_edit_text_view.ans b/mods/art/demo_edit_text_view.ans index c0b234cf..e6f7a240 100644 Binary files a/mods/art/demo_edit_text_view.ans and b/mods/art/demo_edit_text_view.ans differ diff --git a/mods/art/demo_edit_text_view1.ans b/mods/art/demo_edit_text_view1.ans new file mode 100644 index 00000000..6307d29e Binary files /dev/null and b/mods/art/demo_edit_text_view1.ans differ diff --git a/mods/menu.json b/mods/menu.json index a917520f..83969d24 100644 --- a/mods/menu.json +++ b/mods/menu.json @@ -199,27 +199,67 @@ } } }, + "demoEditTextView" : { + "art" : "demo_edit_text_view1.ans", + "options" : { "cls" : true }, + "form" : { + "0" : { + "BT4ET1ET2ET3" : { + "mci" : { + "ET1" : { + "maxLength" : 20 + }, + "ET2" : { + "maxLength" : 20 + }, + "ET3" : { + "fillChar" : " ", + "maxLength" : 20 + }, + "BT4" : { + "text" : "Back", + "submit" : true + } + }, + "submit" : { + "*" : [ + { + "value" : 4, + "action" : "@menu:demoMain" + } + ] + } + } + } + } + } + /* "demoEditTextView" : { "art" : "demo_edit_text_view.ans", "options" : { "cls" : true }, "form" : { "0" : { - "ET1ET2ET3ET5SM4" : { + "ET1ET2ET3ET5SM4TM6" : { "mci" : { "ET1" : { "maxLength" : 1 }, "ET2" : { "maxLength" : 1 }, "ET3" : { "maxLength" : 1 }, "SM4" : { - "items" : [ "YES", "NO", "Maybe So" ] + "items" : [ "One", "Two", "Three", "Four" ] }, "ET5" : { "password" : true, "submit" : [ "esc" ], "fillChar" : "#" + }, + "TM6" : { + "items" : [ "Yes", "No" ], + "hotkeys" : { "Y" : 0, "n" : 1 } } } } } } } + */ } \ No newline at end of file