blob: 035037eb361bb0584ec87b573b5e130d6f4f3f1e [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;
207 let key = this.whatKey(Number.parseInt(code));
208 this.log.debug('Key detected', event, key, event.code, event.keyCode);
209 const textBlockable = !this.textFieldDoesNotBlock[key];
210 const modifiers = [];
211
212 if (event.metaKey) {
213 modifiers.push('cmd');
214 }
215 if (event.altKey) {
216 modifiers.push('alt');
217 }
218 if (event.shiftKey) {
219 modifiers.push('shift');
220 }
221
222 if (!key) {
223 return;
224 }
225
226 modifiers.push(key);
227 key = modifiers.join('-');
228
229 if (textBlockable && this.textFieldInput()) {
230 return;
231 }
232
233 const kh: KeyHandler = this.keyHandler;
234 const gk = kh.globalKeys[key];
235 const gcb = this.fs.isF(gk) || (this.fs.isA(gk) && this.fs.isF(gk[0]));
236 const dk = kh.dialogKeys[key];
237 const dcb = this.fs.isF(dk);
238 const vk = kh.viewKeys[key];
239 const kl = this.fs.isF(kh.viewKeys._keyListener);
240 const vcb = this.fs.isF(vk) || (this.fs.isA(vk) && this.fs.isF(vk[0])) || this.fs.isF(kh.viewFn);
241 const token: KeysToken = KeysToken.KEYEV; // indicate this was a key-pressed event
242
243 event.stopPropagation();
244
245 if (this.enabled) {
246 if (this.matchSeq(key)) {
247 return;
248 }
249
250 // global callback?
251 if (gcb && gcb(token, key, code, event)) {
252 // if the event was 'handled', we are done
253 return;
254 }
255 // dialog callback?
256 if (dcb) {
257 dcb(token, key, code, event);
258 // assume dialog handled the event
259 return;
260 }
261 // otherwise, let the view callback have a shot
262 if (vcb) {
263 this.log.debug('Letting view callback have a shot', vcb, token, key, code, event );
264 vcb(token, key, code, event);
265 }
266 if (kl) {
267 kl(key);
268 }
269 }
270 }
271
272 // functions to obtain localized strings deferred from the setup of the
273 // global key data structures.
274 protected qhlion() {
275 return this.ls.bundle('core.fw.QuickHelp');
276 }
277 protected qhlionShowHide() {
278 return this.qhlion()('qh_hint_show_hide_qh');
279 }
280
281 protected qhlionHintEsc() {
282 return this.qhlion()('qh_hint_esc');
283 }
284
285 protected qhlionHintT() {
286 return this.qhlion()('qh_hint_t');
287 }
288
289 protected setupGlobalKeys() {
290 Object.assign(this.keyHandler, {
291 globalKeys: {
292 backSlash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
293 slash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
294 esc: [(view, key, code, ev) => this.escapeKey(view, key, code, ev), this.qhlionHintEsc],
295 T: [(view, key, code, ev) => this.toggleTheme(view, key, code, ev), this.qhlionHintT],
296 },
297 globalFormat: ['backSlash', 'slash', 'esc', 'T'],
298
299 // Masked keys are global key handlers that always return true.
300 // That is, the view will never see the event for that key.
301 maskedKeys: {
302 slash: 1,
303 backSlash: 1,
304 T: 1,
305 },
306 });
307 }
308
309 protected quickHelp(view, key, code, ev) {
310 if (!this.globalEnabled) {
311 return false;
312 }
Sean Condonb2c483c2019-01-16 20:28:55 +0000313 this.quickHelpShown = !this.quickHelpShown;
Sean Condonf4f54a12018-10-10 23:25:46 +0100314 return true;
315 }
316
317 // returns true if we 'consumed' the ESC keypress, false otherwise
318 protected escapeKey(view, key, code, ev) {
Sean Condonb2c483c2019-01-16 20:28:55 +0000319 this.quickHelpShown = false;
Sean Condon7d275162018-11-02 16:29:06 +0000320 return this.ns.hideNav();
Sean Condonf4f54a12018-10-10 23:25:46 +0100321 }
322
323 protected toggleTheme(view, key, code, ev) {
324 if (!this.globalEnabled) {
325 return false;
326 }
327 // ts.toggleTheme();
328 return true;
329 }
330
331 protected filterMaskedKeys(map: any, caller: any, remove: boolean): any[] {
332 const masked = [];
333 const msgs = [];
334
335 d3.map(map).keys().forEach((key) => {
336 if (this.keyHandler.maskedKeys[key]) {
337 masked.push(key);
338 msgs.push(caller, ': Key "' + key + '" is reserved');
339 }
340 });
341
342 if (msgs.length) {
343 this.log.warn(msgs.join('\n'));
344 }
345
346 if (remove) {
347 masked.forEach((k) => {
348 delete map[k];
349 });
350 }
351 return masked;
352 }
353
354 protected unexParam(fname, x) {
355 this.log.warn(fname, ': unexpected parameter-- ', x);
356 }
357
358 protected setKeyBindings(keyArg) {
359 const fname = 'setKeyBindings()';
360 const kFunc = this.fs.isF(keyArg);
361 const kMap = this.fs.isO(keyArg);
362
363 if (kFunc) {
364 // set general key handler callback
365 this.keyHandler.viewFn = kFunc;
366 } else if (kMap) {
367 this.filterMaskedKeys(kMap, fname, true);
368 this.keyHandler.viewKeys = kMap;
369 } else {
370 this.unexParam(fname, keyArg);
371 }
372 }
373
374 getKeyBindings() {
375 const gkeys = d3.map(this.keyHandler.globalKeys).keys();
376 const masked = d3.map(this.keyHandler.maskedKeys).keys();
377 const vkeys = d3.map(this.keyHandler.viewKeys).keys();
378 const vfn = !!this.fs.isF(this.keyHandler.viewFn);
379
380 return {
381 globalKeys: gkeys,
382 maskedKeys: masked,
383 viewKeys: vkeys,
384 viewFunction: vfn,
385 };
386 }
387
388 protected bindDialogKeys(map) {
389 const fname = 'bindDialogKeys()';
390 const kMap = this.fs.isO(map);
391
392 if (kMap) {
393 this.filterMaskedKeys(map, fname, true);
394 this.keyHandler.dialogKeys = kMap;
395 } else {
396 this.unexParam(fname, map);
397 }
398 }
399
400 protected unbindDialogKeys() {
401 this.keyHandler.dialogKeys = {};
402 }
403
404}