blob: ac68c1a5622efa2bc7835dd4edc68f18db0b53df [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 };
62 qhs: any; // Quick Help Service ??
63
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
73 bindQhs(_qhs_) {
74 this.qhs = _qhs_;
75 }
76
77 installOn(elem) {
78 this.log.debug('Installing keys handler');
79 elem.on('keydown', () => { this.keyIn(); });
80 this.setupGlobalKeys();
81 }
82
83 keyBindings(x) {
84 if (x === undefined) {
85 return this.getKeyBindings();
86 } else {
87 this.setKeyBindings(x);
88 }
89 }
90
91 unbindKeys() {
92 this.keyHandler.viewKeys = {};
93 this.keyHandler.viewFn = null;
94 this.keyHandler.viewGestures = [];
95 }
96
97 dialogKeys(x) {
98 if (x === undefined) {
99 this.unbindDialogKeys();
100 } else {
101 this.bindDialogKeys(x);
102 }
103 }
104
105 addSeq(word, data) {
106 this.fs.addToTrie(this.seq, word, data);
107 }
108
109 remSeq(word) {
110 this.fs.removeFromTrie(this.seq, word);
111 }
112
113 gestureNotes(g?) {
114 if (g === undefined) {
115 return this.keyHandler.viewGestures;
116 } else {
117 this.keyHandler.viewGestures = this.fs.isA(g) || [];
118 }
119 }
120
121 enableKeys(b) {
122 this.enabled = b;
123 }
124
125 enableGlobalKeys(b) {
126 this.globalEnabled = b;
127 }
128
129 checkNotGlobal(o) {
130 const oops = [];
131 if (this.fs.isO(o)) {
132 o.forEach((val, key) => {
133 if (this.keyHandler.globalKeys[key]) {
134 oops.push(key);
135 }
136 });
137 if (oops.length) {
138 this.log.warn('Ignoring reserved global key(s):', oops.join(','));
139 oops.forEach((key) => {
140 delete o[key];
141 });
142 }
143 }
144 }
145
146 protected matchSeq(key) {
147 if (!this.matching && key === 'shift-shift') {
148 this.matching = true;
149 return true;
150 }
151 if (this.matching) {
152 this.matched += key;
153 this.lookup = this.fs.trieLookup(this.seq, this.matched);
154 if (this.lookup === -1) {
155 return true;
156 }
157 this.matching = false;
158 this.matched = '';
159 if (!this.lookup) {
160 return;
161 }
162 // ee.cluck(lookup);
163 return true;
164 }
165 }
166
167 protected whatKey(code: number): string {
168 switch (code) {
169 case 8: return 'delete';
170 case 9: return 'tab';
171 case 13: return 'enter';
172 case 16: return 'shift';
173 case 27: return 'esc';
174 case 32: return 'space';
175 case 37: return 'leftArrow';
176 case 38: return 'upArrow';
177 case 39: return 'rightArrow';
178 case 40: return 'downArrow';
179 case 186: return 'semicolon';
180 case 187: return 'equals';
181 case 188: return 'comma';
182 case 189: return 'dash';
183 case 190: return 'dot';
184 case 191: return 'slash';
185 case 192: return 'backQuote';
186 case 219: return 'openBracket';
187 case 220: return 'backSlash';
188 case 221: return 'closeBracket';
189 case 222: return 'quote';
190 default:
191 if ((code >= 48 && code <= 57) ||
192 (code >= 65 && code <= 90)) {
193 return String.fromCharCode(code);
194 } else if (code >= 112 && code <= 123) {
195 return 'F' + (code - 111);
196 }
197 return null;
198 }
199 }
200
201 protected textFieldInput() {
202 const t = d3.event.target.tagName.toLowerCase();
203 return t === 'input' || t === 'textarea';
204 }
205
206 protected keyIn() {
207 const event = d3.event;
208 // d3.events can set the keyCode, but unit tests based on KeyboardEvent
209 // cannot set keyCode since the attribute has been deprecated
210 const code = event.keyCode ? event.keyCode : event.code;
211 let key = this.whatKey(Number.parseInt(code));
212 this.log.debug('Key detected', event, key, event.code, event.keyCode);
213 const textBlockable = !this.textFieldDoesNotBlock[key];
214 const modifiers = [];
215
216 if (event.metaKey) {
217 modifiers.push('cmd');
218 }
219 if (event.altKey) {
220 modifiers.push('alt');
221 }
222 if (event.shiftKey) {
223 modifiers.push('shift');
224 }
225
226 if (!key) {
227 return;
228 }
229
230 modifiers.push(key);
231 key = modifiers.join('-');
232
233 if (textBlockable && this.textFieldInput()) {
234 return;
235 }
236
237 const kh: KeyHandler = this.keyHandler;
238 const gk = kh.globalKeys[key];
239 const gcb = this.fs.isF(gk) || (this.fs.isA(gk) && this.fs.isF(gk[0]));
240 const dk = kh.dialogKeys[key];
241 const dcb = this.fs.isF(dk);
242 const vk = kh.viewKeys[key];
243 const kl = this.fs.isF(kh.viewKeys._keyListener);
244 const vcb = this.fs.isF(vk) || (this.fs.isA(vk) && this.fs.isF(vk[0])) || this.fs.isF(kh.viewFn);
245 const token: KeysToken = KeysToken.KEYEV; // indicate this was a key-pressed event
246
247 event.stopPropagation();
248
249 if (this.enabled) {
250 if (this.matchSeq(key)) {
251 return;
252 }
253
254 // global callback?
255 if (gcb && gcb(token, key, code, event)) {
256 // if the event was 'handled', we are done
257 return;
258 }
259 // dialog callback?
260 if (dcb) {
261 dcb(token, key, code, event);
262 // assume dialog handled the event
263 return;
264 }
265 // otherwise, let the view callback have a shot
266 if (vcb) {
267 this.log.debug('Letting view callback have a shot', vcb, token, key, code, event );
268 vcb(token, key, code, event);
269 }
270 if (kl) {
271 kl(key);
272 }
273 }
274 }
275
276 // functions to obtain localized strings deferred from the setup of the
277 // global key data structures.
278 protected qhlion() {
279 return this.ls.bundle('core.fw.QuickHelp');
280 }
281 protected qhlionShowHide() {
282 return this.qhlion()('qh_hint_show_hide_qh');
283 }
284
285 protected qhlionHintEsc() {
286 return this.qhlion()('qh_hint_esc');
287 }
288
289 protected qhlionHintT() {
290 return this.qhlion()('qh_hint_t');
291 }
292
293 protected setupGlobalKeys() {
294 Object.assign(this.keyHandler, {
295 globalKeys: {
296 backSlash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
297 slash: [(view, key, code, ev) => this.quickHelp(view, key, code, ev), this.qhlionShowHide],
298 esc: [(view, key, code, ev) => this.escapeKey(view, key, code, ev), this.qhlionHintEsc],
299 T: [(view, key, code, ev) => this.toggleTheme(view, key, code, ev), this.qhlionHintT],
300 },
301 globalFormat: ['backSlash', 'slash', 'esc', 'T'],
302
303 // Masked keys are global key handlers that always return true.
304 // That is, the view will never see the event for that key.
305 maskedKeys: {
306 slash: 1,
307 backSlash: 1,
308 T: 1,
309 },
310 });
311 }
312
313 protected quickHelp(view, key, code, ev) {
314 if (!this.globalEnabled) {
315 return false;
316 }
317 this.qhs.showQuickHelp(this.keyHandler);
318 return true;
319 }
320
321 // returns true if we 'consumed' the ESC keypress, false otherwise
322 protected escapeKey(view, key, code, ev) {
323 return this.ns.hideNav() || this.qhs.hideQuickHelp();
324 }
325
326 protected toggleTheme(view, key, code, ev) {
327 if (!this.globalEnabled) {
328 return false;
329 }
330 // ts.toggleTheme();
331 return true;
332 }
333
334 protected filterMaskedKeys(map: any, caller: any, remove: boolean): any[] {
335 const masked = [];
336 const msgs = [];
337
338 d3.map(map).keys().forEach((key) => {
339 if (this.keyHandler.maskedKeys[key]) {
340 masked.push(key);
341 msgs.push(caller, ': Key "' + key + '" is reserved');
342 }
343 });
344
345 if (msgs.length) {
346 this.log.warn(msgs.join('\n'));
347 }
348
349 if (remove) {
350 masked.forEach((k) => {
351 delete map[k];
352 });
353 }
354 return masked;
355 }
356
357 protected unexParam(fname, x) {
358 this.log.warn(fname, ': unexpected parameter-- ', x);
359 }
360
361 protected setKeyBindings(keyArg) {
362 const fname = 'setKeyBindings()';
363 const kFunc = this.fs.isF(keyArg);
364 const kMap = this.fs.isO(keyArg);
365
366 if (kFunc) {
367 // set general key handler callback
368 this.keyHandler.viewFn = kFunc;
369 } else if (kMap) {
370 this.filterMaskedKeys(kMap, fname, true);
371 this.keyHandler.viewKeys = kMap;
372 } else {
373 this.unexParam(fname, keyArg);
374 }
375 }
376
377 getKeyBindings() {
378 const gkeys = d3.map(this.keyHandler.globalKeys).keys();
379 const masked = d3.map(this.keyHandler.maskedKeys).keys();
380 const vkeys = d3.map(this.keyHandler.viewKeys).keys();
381 const vfn = !!this.fs.isF(this.keyHandler.viewFn);
382
383 return {
384 globalKeys: gkeys,
385 maskedKeys: masked,
386 viewKeys: vkeys,
387 viewFunction: vfn,
388 };
389 }
390
391 protected bindDialogKeys(map) {
392 const fname = 'bindDialogKeys()';
393 const kMap = this.fs.isO(map);
394
395 if (kMap) {
396 this.filterMaskedKeys(map, fname, true);
397 this.keyHandler.dialogKeys = kMap;
398 } else {
399 this.unexParam(fname, map);
400 }
401 }
402
403 protected unbindDialogKeys() {
404 this.keyHandler.dialogKeys = {};
405 }
406
407}