First part of migrating Topo2 to GUI2
Change-Id: I316dd34cba161688e01dfb7b340bff5f2c3c57d4
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/keys.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/keys.service.ts
new file mode 100644
index 0000000..ac68c1a
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/keys.service.ts
@@ -0,0 +1,407 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Injectable } from '@angular/core';
+import * as d3 from 'd3';
+import { LogService } from '../log.service';
+import { FnService } from '../util/fn.service';
+import { LionService } from './lion.service';
+import { NavService } from '../nav/nav.service';
+
+export interface KeyHandler {
+ globalKeys: Object;
+ maskedKeys: Object;
+ dialogKeys: Object;
+ viewKeys: any;
+ viewFn: any;
+ viewGestures: string[][];
+}
+
+export enum KeysToken {
+ KEYEV = 'keyev'
+}
+
+/**
+ * ONOS GUI -- Keys Service Module.
+ */
+@Injectable({
+ providedIn: 'root',
+})
+export class KeysService {
+ enabled: boolean = true;
+ globalEnabled: boolean = true;
+ keyHandler: KeyHandler = <KeyHandler>{
+ globalKeys: {},
+ maskedKeys: {},
+ dialogKeys: {},
+ viewKeys: {},
+ viewFn: null,
+ viewGestures: [],
+ };
+
+ seq: any = {};
+ matching: boolean = false;
+ matched: string = '';
+ lookup: any;
+ textFieldDoesNotBlock: any = {
+ enter: 1,
+ esc: 1,
+ };
+ qhs: any; // Quick Help Service ??
+
+ constructor(
+ protected log: LogService,
+ protected fs: FnService,
+ protected ls: LionService,
+ protected ns: NavService
+ ) {
+ this.log.debug('KeyService constructed');
+ }
+
+ bindQhs(_qhs_) {
+ this.qhs = _qhs_;
+ }
+
+ installOn(elem) {
+ this.log.debug('Installing keys handler');
+ elem.on('keydown', () => { this.keyIn(); });
+ this.setupGlobalKeys();
+ }
+
+ keyBindings(x) {
+ if (x === undefined) {
+ return this.getKeyBindings();
+ } else {
+ this.setKeyBindings(x);
+ }
+ }
+
+ unbindKeys() {
+ this.keyHandler.viewKeys = {};
+ this.keyHandler.viewFn = null;
+ this.keyHandler.viewGestures = [];
+ }
+
+ dialogKeys(x) {
+ if (x === undefined) {
+ this.unbindDialogKeys();
+ } else {
+ this.bindDialogKeys(x);
+ }
+ }
+
+ addSeq(word, data) {
+ this.fs.addToTrie(this.seq, word, data);
+ }
+
+ remSeq(word) {
+ this.fs.removeFromTrie(this.seq, word);
+ }
+
+ gestureNotes(g?) {
+ if (g === undefined) {
+ return this.keyHandler.viewGestures;
+ } else {
+ this.keyHandler.viewGestures = this.fs.isA(g) || [];
+ }
+ }
+
+ enableKeys(b) {
+ this.enabled = b;
+ }
+
+ enableGlobalKeys(b) {
+ this.globalEnabled = b;
+ }
+
+ checkNotGlobal(o) {
+ const oops = [];
+ if (this.fs.isO(o)) {
+ o.forEach((val, key) => {
+ if (this.keyHandler.globalKeys[key]) {
+ oops.push(key);
+ }
+ });
+ if (oops.length) {
+ this.log.warn('Ignoring reserved global key(s):', oops.join(','));
+ oops.forEach((key) => {
+ delete o[key];
+ });
+ }
+ }
+ }
+
+ protected matchSeq(key) {
+ if (!this.matching && key === 'shift-shift') {
+ this.matching = true;
+ return true;
+ }
+ if (this.matching) {
+ this.matched += key;
+ this.lookup = this.fs.trieLookup(this.seq, this.matched);
+ if (this.lookup === -1) {
+ return true;
+ }
+ this.matching = false;
+ this.matched = '';
+ if (!this.lookup) {
+ return;
+ }
+ // ee.cluck(lookup);
+ return true;
+ }
+ }
+
+ protected whatKey(code: number): string {
+ switch (code) {
+ case 8: return 'delete';
+ case 9: return 'tab';
+ case 13: return 'enter';
+ case 16: return 'shift';
+ case 27: return 'esc';
+ case 32: return 'space';
+ case 37: return 'leftArrow';
+ case 38: return 'upArrow';
+ case 39: return 'rightArrow';
+ case 40: return 'downArrow';
+ case 186: return 'semicolon';
+ case 187: return 'equals';
+ case 188: return 'comma';
+ case 189: return 'dash';
+ case 190: return 'dot';
+ case 191: return 'slash';
+ case 192: return 'backQuote';
+ case 219: return 'openBracket';
+ case 220: return 'backSlash';
+ case 221: return 'closeBracket';
+ case 222: return 'quote';
+ default:
+ if ((code >= 48 && code <= 57) ||
+ (code >= 65 && code <= 90)) {
+ return String.fromCharCode(code);
+ } else if (code >= 112 && code <= 123) {
+ return 'F' + (code - 111);
+ }
+ return null;
+ }
+ }
+
+ protected textFieldInput() {
+ const t = d3.event.target.tagName.toLowerCase();
+ return t === 'input' || t === 'textarea';
+ }
+
+ protected keyIn() {
+ const event = d3.event;
+ // d3.events can set the keyCode, but unit tests based on KeyboardEvent
+ // cannot set keyCode since the attribute has been deprecated
+ const code = event.keyCode ? event.keyCode : event.code;
+ let key = this.whatKey(Number.parseInt(code));
+ this.log.debug('Key detected', event, key, event.code, event.keyCode);
+ const textBlockable = !this.textFieldDoesNotBlock[key];
+ const modifiers = [];
+
+ if (event.metaKey) {
+ modifiers.push('cmd');
+ }
+ if (event.altKey) {
+ modifiers.push('alt');
+ }
+ if (event.shiftKey) {
+ modifiers.push('shift');
+ }
+
+ if (!key) {
+ return;
+ }
+
+ modifiers.push(key);
+ key = modifiers.join('-');
+
+ if (textBlockable && this.textFieldInput()) {
+ return;
+ }
+
+ const kh: KeyHandler = this.keyHandler;
+ const gk = kh.globalKeys[key];
+ const gcb = this.fs.isF(gk) || (this.fs.isA(gk) && this.fs.isF(gk[0]));
+ const dk = kh.dialogKeys[key];
+ const dcb = this.fs.isF(dk);
+ const vk = kh.viewKeys[key];
+ const kl = this.fs.isF(kh.viewKeys._keyListener);
+ const vcb = this.fs.isF(vk) || (this.fs.isA(vk) && this.fs.isF(vk[0])) || this.fs.isF(kh.viewFn);
+ const token: KeysToken = KeysToken.KEYEV; // indicate this was a key-pressed event
+
+ event.stopPropagation();
+
+ if (this.enabled) {
+ if (this.matchSeq(key)) {
+ return;
+ }
+
+ // global callback?
+ if (gcb && gcb(token, key, code, event)) {
+ // if the event was 'handled', we are done
+ return;
+ }
+ // dialog callback?
+ if (dcb) {
+ dcb(token, key, code, event);
+ // assume dialog handled the event
+ return;
+ }
+ // otherwise, let the view callback have a shot
+ if (vcb) {
+ this.log.debug('Letting view callback have a shot', vcb, token, key, code, event );
+ vcb(token, key, code, event);
+ }
+ if (kl) {
+ kl(key);
+ }
+ }
+ }
+
+ // functions to obtain localized strings deferred from the setup of the
+ // global key data structures.
+ protected qhlion() {
+ return this.ls.bundle('core.fw.QuickHelp');
+ }
+ protected qhlionShowHide() {
+ return this.qhlion()('qh_hint_show_hide_qh');
+ }
+
+ protected qhlionHintEsc() {
+ return this.qhlion()('qh_hint_esc');
+ }
+
+ protected qhlionHintT() {
+ return this.qhlion()('qh_hint_t');
+ }
+
+ protected setupGlobalKeys() {
+ Object.assign(this.keyHandler, {
+ globalKeys: {
+ backSlash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
+ slash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
+ esc: [(view, key, code, ev) => this.escapeKey(view, key, code, ev), this.qhlionHintEsc],
+ T: [(view, key, code, ev) => this.toggleTheme(view, key, code, ev), this.qhlionHintT],
+ },
+ globalFormat: ['backSlash', 'slash', 'esc', 'T'],
+
+ // Masked keys are global key handlers that always return true.
+ // That is, the view will never see the event for that key.
+ maskedKeys: {
+ slash: 1,
+ backSlash: 1,
+ T: 1,
+ },
+ });
+ }
+
+ protected quickHelp(view, key, code, ev) {
+ if (!this.globalEnabled) {
+ return false;
+ }
+ this.qhs.showQuickHelp(this.keyHandler);
+ return true;
+ }
+
+ // returns true if we 'consumed' the ESC keypress, false otherwise
+ protected escapeKey(view, key, code, ev) {
+ return this.ns.hideNav() || this.qhs.hideQuickHelp();
+ }
+
+ protected toggleTheme(view, key, code, ev) {
+ if (!this.globalEnabled) {
+ return false;
+ }
+ // ts.toggleTheme();
+ return true;
+ }
+
+ protected filterMaskedKeys(map: any, caller: any, remove: boolean): any[] {
+ const masked = [];
+ const msgs = [];
+
+ d3.map(map).keys().forEach((key) => {
+ if (this.keyHandler.maskedKeys[key]) {
+ masked.push(key);
+ msgs.push(caller, ': Key "' + key + '" is reserved');
+ }
+ });
+
+ if (msgs.length) {
+ this.log.warn(msgs.join('\n'));
+ }
+
+ if (remove) {
+ masked.forEach((k) => {
+ delete map[k];
+ });
+ }
+ return masked;
+ }
+
+ protected unexParam(fname, x) {
+ this.log.warn(fname, ': unexpected parameter-- ', x);
+ }
+
+ protected setKeyBindings(keyArg) {
+ const fname = 'setKeyBindings()';
+ const kFunc = this.fs.isF(keyArg);
+ const kMap = this.fs.isO(keyArg);
+
+ if (kFunc) {
+ // set general key handler callback
+ this.keyHandler.viewFn = kFunc;
+ } else if (kMap) {
+ this.filterMaskedKeys(kMap, fname, true);
+ this.keyHandler.viewKeys = kMap;
+ } else {
+ this.unexParam(fname, keyArg);
+ }
+ }
+
+ getKeyBindings() {
+ const gkeys = d3.map(this.keyHandler.globalKeys).keys();
+ const masked = d3.map(this.keyHandler.maskedKeys).keys();
+ const vkeys = d3.map(this.keyHandler.viewKeys).keys();
+ const vfn = !!this.fs.isF(this.keyHandler.viewFn);
+
+ return {
+ globalKeys: gkeys,
+ maskedKeys: masked,
+ viewKeys: vkeys,
+ viewFunction: vfn,
+ };
+ }
+
+ protected bindDialogKeys(map) {
+ const fname = 'bindDialogKeys()';
+ const kMap = this.fs.isO(map);
+
+ if (kMap) {
+ this.filterMaskedKeys(map, fname, true);
+ this.keyHandler.dialogKeys = kMap;
+ } else {
+ this.unexParam(fname, map);
+ }
+ }
+
+ protected unbindDialogKeys() {
+ this.keyHandler.dialogKeys = {};
+ }
+
+}