blob: decb87b677842fff3d26c4629e4258ece653ceba [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) {
Sean Condon7d275162018-11-02 16:29:06 +0000323 return this.ns.hideNav();
324 // TODO - also hide this.qhs.hideQuickHelp();
Sean Condonf4f54a12018-10-10 23:25:46 +0100325 }
326
327 protected toggleTheme(view, key, code, ev) {
328 if (!this.globalEnabled) {
329 return false;
330 }
331 // ts.toggleTheme();
332 return true;
333 }
334
335 protected filterMaskedKeys(map: any, caller: any, remove: boolean): any[] {
336 const masked = [];
337 const msgs = [];
338
339 d3.map(map).keys().forEach((key) => {
340 if (this.keyHandler.maskedKeys[key]) {
341 masked.push(key);
342 msgs.push(caller, ': Key "' + key + '" is reserved');
343 }
344 });
345
346 if (msgs.length) {
347 this.log.warn(msgs.join('\n'));
348 }
349
350 if (remove) {
351 masked.forEach((k) => {
352 delete map[k];
353 });
354 }
355 return masked;
356 }
357
358 protected unexParam(fname, x) {
359 this.log.warn(fname, ': unexpected parameter-- ', x);
360 }
361
362 protected setKeyBindings(keyArg) {
363 const fname = 'setKeyBindings()';
364 const kFunc = this.fs.isF(keyArg);
365 const kMap = this.fs.isO(keyArg);
366
367 if (kFunc) {
368 // set general key handler callback
369 this.keyHandler.viewFn = kFunc;
370 } else if (kMap) {
371 this.filterMaskedKeys(kMap, fname, true);
372 this.keyHandler.viewKeys = kMap;
373 } else {
374 this.unexParam(fname, keyArg);
375 }
376 }
377
378 getKeyBindings() {
379 const gkeys = d3.map(this.keyHandler.globalKeys).keys();
380 const masked = d3.map(this.keyHandler.maskedKeys).keys();
381 const vkeys = d3.map(this.keyHandler.viewKeys).keys();
382 const vfn = !!this.fs.isF(this.keyHandler.viewFn);
383
384 return {
385 globalKeys: gkeys,
386 maskedKeys: masked,
387 viewKeys: vkeys,
388 viewFunction: vfn,
389 };
390 }
391
392 protected bindDialogKeys(map) {
393 const fname = 'bindDialogKeys()';
394 const kMap = this.fs.isO(map);
395
396 if (kMap) {
397 this.filterMaskedKeys(map, fname, true);
398 this.keyHandler.dialogKeys = kMap;
399 } else {
400 this.unexParam(fname, map);
401 }
402 }
403
404 protected unbindDialogKeys() {
405 this.keyHandler.dialogKeys = {};
406 }
407
408}