blob: d4adb01d3c86c6e7de9264b933dbd53bd7b671f3 [file] [log] [blame]
Sean Condonf4f54a12018-10-10 23:25:46 +01001/*
2 * Copyright 2018-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16import { Injectable } from '@angular/core';
17import * as d3 from 'd3';
18import { LogService } from '../log.service';
19import { FnService } from '../util/fn.service';
20import { LionService } from './lion.service';
21import { NavService } from '../nav/nav.service';
22
23export interface KeyHandler {
24 globalKeys: Object;
25 maskedKeys: Object;
26 dialogKeys: Object;
27 viewKeys: any;
28 viewFn: any;
29 viewGestures: string[][];
30}
31
32export enum KeysToken {
33 KEYEV = 'keyev'
34}
35
36/**
37 * ONOS GUI -- Keys Service Module.
38 */
39@Injectable({
40 providedIn: 'root',
41})
42export class KeysService {
43 enabled: boolean = true;
44 globalEnabled: boolean = true;
45 keyHandler: KeyHandler = <KeyHandler>{
46 globalKeys: {},
47 maskedKeys: {},
48 dialogKeys: {},
49 viewKeys: {},
50 viewFn: null,
51 viewGestures: [],
52 };
53
54 seq: any = {};
55 matching: boolean = false;
56 matched: string = '';
57 lookup: any;
58 textFieldDoesNotBlock: any = {
59 enter: 1,
60 esc: 1,
61 };
Sean Condonb2c483c2019-01-16 20:28:55 +000062 quickHelpShown: boolean = false;
Sean Condonf4f54a12018-10-10 23:25:46 +010063
64 constructor(
65 protected log: LogService,
66 protected fs: FnService,
67 protected ls: LionService,
68 protected ns: NavService
69 ) {
70 this.log.debug('KeyService constructed');
71 }
72
Sean Condonf4f54a12018-10-10 23:25:46 +010073 installOn(elem) {
74 this.log.debug('Installing keys handler');
75 elem.on('keydown', () => { this.keyIn(); });
76 this.setupGlobalKeys();
77 }
78
79 keyBindings(x) {
80 if (x === undefined) {
81 return this.getKeyBindings();
82 } else {
83 this.setKeyBindings(x);
84 }
85 }
86
87 unbindKeys() {
88 this.keyHandler.viewKeys = {};
89 this.keyHandler.viewFn = null;
90 this.keyHandler.viewGestures = [];
91 }
92
93 dialogKeys(x) {
94 if (x === undefined) {
95 this.unbindDialogKeys();
96 } else {
97 this.bindDialogKeys(x);
98 }
99 }
100
101 addSeq(word, data) {
102 this.fs.addToTrie(this.seq, word, data);
103 }
104
105 remSeq(word) {
106 this.fs.removeFromTrie(this.seq, word);
107 }
108
109 gestureNotes(g?) {
110 if (g === undefined) {
111 return this.keyHandler.viewGestures;
112 } else {
113 this.keyHandler.viewGestures = this.fs.isA(g) || [];
114 }
115 }
116
117 enableKeys(b) {
118 this.enabled = b;
119 }
120
121 enableGlobalKeys(b) {
122 this.globalEnabled = b;
123 }
124
125 checkNotGlobal(o) {
126 const oops = [];
127 if (this.fs.isO(o)) {
128 o.forEach((val, key) => {
129 if (this.keyHandler.globalKeys[key]) {
130 oops.push(key);
131 }
132 });
133 if (oops.length) {
134 this.log.warn('Ignoring reserved global key(s):', oops.join(','));
135 oops.forEach((key) => {
136 delete o[key];
137 });
138 }
139 }
140 }
141
142 protected matchSeq(key) {
143 if (!this.matching && key === 'shift-shift') {
144 this.matching = true;
145 return true;
146 }
147 if (this.matching) {
148 this.matched += key;
149 this.lookup = this.fs.trieLookup(this.seq, this.matched);
150 if (this.lookup === -1) {
151 return true;
152 }
153 this.matching = false;
154 this.matched = '';
155 if (!this.lookup) {
156 return;
157 }
158 // ee.cluck(lookup);
159 return true;
160 }
161 }
162
163 protected whatKey(code: number): string {
164 switch (code) {
165 case 8: return 'delete';
166 case 9: return 'tab';
167 case 13: return 'enter';
168 case 16: return 'shift';
169 case 27: return 'esc';
170 case 32: return 'space';
171 case 37: return 'leftArrow';
172 case 38: return 'upArrow';
173 case 39: return 'rightArrow';
174 case 40: return 'downArrow';
175 case 186: return 'semicolon';
176 case 187: return 'equals';
177 case 188: return 'comma';
178 case 189: return 'dash';
179 case 190: return 'dot';
180 case 191: return 'slash';
181 case 192: return 'backQuote';
182 case 219: return 'openBracket';
183 case 220: return 'backSlash';
184 case 221: return 'closeBracket';
185 case 222: return 'quote';
186 default:
187 if ((code >= 48 && code <= 57) ||
188 (code >= 65 && code <= 90)) {
189 return String.fromCharCode(code);
190 } else if (code >= 112 && code <= 123) {
191 return 'F' + (code - 111);
192 }
193 return null;
194 }
195 }
196
197 protected textFieldInput() {
198 const t = d3.event.target.tagName.toLowerCase();
199 return t === 'input' || t === 'textarea';
200 }
201
202 protected keyIn() {
203 const event = d3.event;
204 // d3.events can set the keyCode, but unit tests based on KeyboardEvent
205 // cannot set keyCode since the attribute has been deprecated
206 const code = event.keyCode ? event.keyCode : event.code;
Sean Condon98b6ddb2019-12-24 08:07:40 +0000207 const codeNum: number = parseInt(code, 10);
208 let key = this.whatKey(codeNum);
Sean Condonf4f54a12018-10-10 23:25:46 +0100209 this.log.debug('Key detected', event, key, event.code, event.keyCode);
210 const textBlockable = !this.textFieldDoesNotBlock[key];
211 const modifiers = [];
212
213 if (event.metaKey) {
214 modifiers.push('cmd');
215 }
216 if (event.altKey) {
217 modifiers.push('alt');
218 }
219 if (event.shiftKey) {
220 modifiers.push('shift');
221 }
222
223 if (!key) {
224 return;
225 }
226
227 modifiers.push(key);
228 key = modifiers.join('-');
229
230 if (textBlockable && this.textFieldInput()) {
231 return;
232 }
233
234 const kh: KeyHandler = this.keyHandler;
235 const gk = kh.globalKeys[key];
236 const gcb = this.fs.isF(gk) || (this.fs.isA(gk) && this.fs.isF(gk[0]));
237 const dk = kh.dialogKeys[key];
238 const dcb = this.fs.isF(dk);
239 const vk = kh.viewKeys[key];
240 const kl = this.fs.isF(kh.viewKeys._keyListener);
241 const vcb = this.fs.isF(vk) || (this.fs.isA(vk) && this.fs.isF(vk[0])) || this.fs.isF(kh.viewFn);
242 const token: KeysToken = KeysToken.KEYEV; // indicate this was a key-pressed event
243
244 event.stopPropagation();
245
246 if (this.enabled) {
247 if (this.matchSeq(key)) {
248 return;
249 }
250
251 // global callback?
252 if (gcb && gcb(token, key, code, event)) {
253 // if the event was 'handled', we are done
254 return;
255 }
256 // dialog callback?
257 if (dcb) {
258 dcb(token, key, code, event);
259 // assume dialog handled the event
260 return;
261 }
262 // otherwise, let the view callback have a shot
263 if (vcb) {
264 this.log.debug('Letting view callback have a shot', vcb, token, key, code, event );
265 vcb(token, key, code, event);
266 }
267 if (kl) {
268 kl(key);
269 }
270 }
271 }
272
273 // functions to obtain localized strings deferred from the setup of the
274 // global key data structures.
275 protected qhlion() {
276 return this.ls.bundle('core.fw.QuickHelp');
277 }
278 protected qhlionShowHide() {
279 return this.qhlion()('qh_hint_show_hide_qh');
280 }
281
282 protected qhlionHintEsc() {
283 return this.qhlion()('qh_hint_esc');
284 }
285
286 protected qhlionHintT() {
287 return this.qhlion()('qh_hint_t');
288 }
289
290 protected setupGlobalKeys() {
Sean Condon98b6ddb2019-12-24 08:07:40 +0000291 (<any>Object).assign(this.keyHandler, {
Sean Condonf4f54a12018-10-10 23:25:46 +0100292 globalKeys: {
293 backSlash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
294 slash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
295 esc: [(view, key, code, ev) => this.escapeKey(view, key, code, ev), this.qhlionHintEsc],
296 T: [(view, key, code, ev) => this.toggleTheme(view, key, code, ev), this.qhlionHintT],
297 },
298 globalFormat: ['backSlash', 'slash', 'esc', 'T'],
299
300 // Masked keys are global key handlers that always return true.
301 // That is, the view will never see the event for that key.
302 maskedKeys: {
303 slash: 1,
304 backSlash: 1,
305 T: 1,
306 },
307 });
308 }
309
310 protected quickHelp(view, key, code, ev) {
311 if (!this.globalEnabled) {
312 return false;
313 }
Sean Condonb2c483c2019-01-16 20:28:55 +0000314 this.quickHelpShown = !this.quickHelpShown;
Sean Condonf4f54a12018-10-10 23:25:46 +0100315 return true;
316 }
317
318 // returns true if we 'consumed' the ESC keypress, false otherwise
319 protected escapeKey(view, key, code, ev) {
Sean Condonb2c483c2019-01-16 20:28:55 +0000320 this.quickHelpShown = false;
Sean Condon7d275162018-11-02 16:29:06 +0000321 return this.ns.hideNav();
Sean Condonf4f54a12018-10-10 23:25:46 +0100322 }
323
324 protected toggleTheme(view, key, code, ev) {
325 if (!this.globalEnabled) {
326 return false;
327 }
328 // ts.toggleTheme();
329 return true;
330 }
331
332 protected filterMaskedKeys(map: any, caller: any, remove: boolean): any[] {
333 const masked = [];
334 const msgs = [];
335
336 d3.map(map).keys().forEach((key) => {
337 if (this.keyHandler.maskedKeys[key]) {
338 masked.push(key);
339 msgs.push(caller, ': Key "' + key + '" is reserved');
340 }
341 });
342
343 if (msgs.length) {
344 this.log.warn(msgs.join('\n'));
345 }
346
347 if (remove) {
348 masked.forEach((k) => {
349 delete map[k];
350 });
351 }
352 return masked;
353 }
354
355 protected unexParam(fname, x) {
356 this.log.warn(fname, ': unexpected parameter-- ', x);
357 }
358
359 protected setKeyBindings(keyArg) {
360 const fname = 'setKeyBindings()';
361 const kFunc = this.fs.isF(keyArg);
362 const kMap = this.fs.isO(keyArg);
363
364 if (kFunc) {
365 // set general key handler callback
366 this.keyHandler.viewFn = kFunc;
367 } else if (kMap) {
368 this.filterMaskedKeys(kMap, fname, true);
369 this.keyHandler.viewKeys = kMap;
370 } else {
371 this.unexParam(fname, keyArg);
372 }
373 }
374
375 getKeyBindings() {
376 const gkeys = d3.map(this.keyHandler.globalKeys).keys();
377 const masked = d3.map(this.keyHandler.maskedKeys).keys();
378 const vkeys = d3.map(this.keyHandler.viewKeys).keys();
379 const vfn = !!this.fs.isF(this.keyHandler.viewFn);
380
381 return {
382 globalKeys: gkeys,
383 maskedKeys: masked,
384 viewKeys: vkeys,
385 viewFunction: vfn,
386 };
387 }
388
389 protected bindDialogKeys(map) {
390 const fname = 'bindDialogKeys()';
391 const kMap = this.fs.isO(map);
392
393 if (kMap) {
394 this.filterMaskedKeys(map, fname, true);
395 this.keyHandler.dialogKeys = kMap;
396 } else {
397 this.unexParam(fname, map);
398 }
399 }
400
401 protected unbindDialogKeys() {
402 this.keyHandler.dialogKeys = {};
403 }
404
405}