blob: 2e8900a52cf97c0efe337d55e584a5ae784448c2 [file] [log] [blame]
Simon Hunt1eecfa22014-12-16 14:46:29 -08001/*
Brian O'Connor5ab426f2016-04-09 01:19:45 -07002 * Copyright 2014-present Open Networking Laboratory
Simon Hunt1eecfa22014-12-16 14:46:29 -08003 *
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 */
16
17/*
Simon Huntdc6362a2014-12-18 19:55:23 -080018 ONOS GUI -- Util -- Key Handler Service
Simon Hunt1eecfa22014-12-16 14:46:29 -080019 */
Simon Huntdc6362a2014-12-18 19:55:23 -080020(function () {
Simon Hunt1eecfa22014-12-16 14:46:29 -080021 'use strict';
22
23 // references to injected services
Simon Hunta6ab9f02017-07-11 11:45:50 -070024 var $log, $timeout, fs, ts, ns, ee, qhs, ls;
Simon Hunt1eecfa22014-12-16 14:46:29 -080025
26 // internal state
Simon Hunt36fc15c2015-02-12 17:02:58 -080027 var enabled = true,
Simon Huntd5579252015-10-06 15:09:14 -070028 globalEnabled = true,
Simon Hunt36fc15c2015-02-12 17:02:58 -080029 keyHandler = {
Simon Hunt1eecfa22014-12-16 14:46:29 -080030 globalKeys: {},
31 maskedKeys: {},
Simon Hunt5198f082016-02-04 13:41:17 -080032 dialogKeys: {},
Simon Hunt1eecfa22014-12-16 14:46:29 -080033 viewKeys: {},
34 viewFn: null,
35 viewGestures: []
Simon Hunt9f5e97d2015-12-11 10:32:09 -080036 },
Simon Huntf4ef6dd2016-02-03 17:05:14 -080037 seq = {},
38 matching = false,
39 matched = '',
40 lookup;
Simon Hunt1eecfa22014-12-16 14:46:29 -080041
Simon Huntf4ef6dd2016-02-03 17:05:14 -080042 function matchSeq(key) {
Simon Huntd189b922016-09-29 10:35:29 -070043 if (!matching && key === 'shift-shift') {
Simon Huntf4ef6dd2016-02-03 17:05:14 -080044 matching = true;
Thomas Vachuskafe1f01a2015-12-10 23:56:02 -080045 return true;
Simon Hunt9f5e97d2015-12-11 10:32:09 -080046 }
Simon Huntf4ef6dd2016-02-03 17:05:14 -080047 if (matching) {
48 matched += key;
49 lookup = fs.trieLookup(seq, matched);
50 if (lookup === -1) {
51 return true;
52 }
53 matching = false;
54 matched = '';
55 if (!lookup) {
56 return;
57 }
58 ee.cluck(lookup);
59 return true;
Thomas Vachuskafe1f01a2015-12-10 23:56:02 -080060 }
Thomas Vachuskafe1f01a2015-12-10 23:56:02 -080061 }
62
Simon Hunt1eecfa22014-12-16 14:46:29 -080063 function whatKey(code) {
64 switch (code) {
Simon Hunt18d4bc82016-01-08 14:09:35 -080065 case 8: return 'delete';
66 case 9: return 'tab';
Simon Hunt1eecfa22014-12-16 14:46:29 -080067 case 13: return 'enter';
Simon Huntd189b922016-09-29 10:35:29 -070068 case 16: return 'shift';
Simon Hunt1eecfa22014-12-16 14:46:29 -080069 case 27: return 'esc';
70 case 32: return 'space';
71 case 37: return 'leftArrow';
72 case 38: return 'upArrow';
73 case 39: return 'rightArrow';
74 case 40: return 'downArrow';
Simon Hunt18d4bc82016-01-08 14:09:35 -080075 case 186: return 'semicolon';
Simon Hunt1eecfa22014-12-16 14:46:29 -080076 case 187: return 'equals';
Simon Hunt90dcc3e2015-03-25 15:01:27 -070077 case 188: return 'comma';
Simon Hunt1eecfa22014-12-16 14:46:29 -080078 case 189: return 'dash';
Simon Hunt90dcc3e2015-03-25 15:01:27 -070079 case 190: return 'dot';
Simon Hunt1eecfa22014-12-16 14:46:29 -080080 case 191: return 'slash';
81 case 192: return 'backQuote';
Simon Hunt18d4bc82016-01-08 14:09:35 -080082 case 219: return 'openBracket';
Simon Hunt1eecfa22014-12-16 14:46:29 -080083 case 220: return 'backSlash';
Simon Hunt18d4bc82016-01-08 14:09:35 -080084 case 221: return 'closeBracket';
85 case 222: return 'quote';
Simon Hunt1eecfa22014-12-16 14:46:29 -080086 default:
87 if ((code >= 48 && code <= 57) ||
88 (code >= 65 && code <= 90)) {
89 return String.fromCharCode(code);
90 } else if (code >= 112 && code <= 123) {
91 return 'F' + (code - 111);
92 }
Steven Burrows8dab1c72016-09-27 11:17:36 -070093 return null;
Simon Hunt1eecfa22014-12-16 14:46:29 -080094 }
95 }
96
Simon Hunt120904e2016-06-23 14:26:26 -070097 var textFieldDoesNotBlock = {
98 enter: 1,
99 esc: 1
100 };
101
Fan Cheng12ed9af2016-06-17 15:04:27 +0800102 function textFieldInput() {
103 var t = d3.event.target.tagName.toLowerCase();
104 return t === 'input' || t === 'textarea';
105 }
106
Simon Hunt1eecfa22014-12-16 14:46:29 -0800107 function keyIn() {
108 var event = d3.event,
109 keyCode = event.keyCode,
110 key = whatKey(keyCode),
Steven Burrows8dab1c72016-09-27 11:17:36 -0700111 textBlockable = !textFieldDoesNotBlock[key],
112 modifiers = [];
113
114 event.metaKey && modifiers.push('cmd');
115 event.altKey && modifiers.push('alt');
116 event.shiftKey && modifiers.push('shift');
117
118 if (!key) {
119 return;
120 }
121
122 modifiers.push(key);
123 key = modifiers.join('-');
Simon Hunt120904e2016-06-23 14:26:26 -0700124
125 if (textBlockable && textFieldInput()) {
126 return;
127 }
128
129 var kh = keyHandler,
Simon Hunt1eecfa22014-12-16 14:46:29 -0800130 gk = kh.globalKeys[key],
Yuta HIGUCHI4f39bcd2014-12-18 20:46:14 -0800131 gcb = fs.isF(gk) || (fs.isA(gk) && fs.isF(gk[0])),
Simon Hunt5198f082016-02-04 13:41:17 -0800132 dk = kh.dialogKeys[key],
133 dcb = fs.isF(dk),
Simon Hunt1eecfa22014-12-16 14:46:29 -0800134 vk = kh.viewKeys[key],
Simon Hunt09060142015-03-18 20:23:32 -0700135 kl = fs.isF(kh.viewKeys._keyListener),
Yuta HIGUCHI4f39bcd2014-12-18 20:46:14 -0800136 vcb = fs.isF(vk) || (fs.isA(vk) && fs.isF(vk[0])) || fs.isF(kh.viewFn),
Simon Hunt1bf1e8c2015-04-08 10:12:43 -0700137 token = 'keyev'; // indicate this was a key-pressed event
Simon Hunt1eecfa22014-12-16 14:46:29 -0800138
Simon Hunt120904e2016-06-23 14:26:26 -0700139 event.stopPropagation();
Simon Huntdc6362a2014-12-18 19:55:23 -0800140
Simon Hunt36fc15c2015-02-12 17:02:58 -0800141 if (enabled) {
Simon Huntf4ef6dd2016-02-03 17:05:14 -0800142 if (matchSeq(key)) return;
Thomas Vachuskafe1f01a2015-12-10 23:56:02 -0800143
Simon Hunt36fc15c2015-02-12 17:02:58 -0800144 // global callback?
145 if (gcb && gcb(token, key, keyCode, event)) {
146 // if the event was 'handled', we are done
147 return;
148 }
Simon Hunt5198f082016-02-04 13:41:17 -0800149 // dialog callback?
150 if (dcb) {
151 dcb(token, key, keyCode, event);
152 // assume dialog handled the event
153 return;
154 }
Simon Hunt36fc15c2015-02-12 17:02:58 -0800155 // otherwise, let the view callback have a shot
156 if (vcb) {
157 vcb(token, key, keyCode, event);
158 }
Simon Hunt09060142015-03-18 20:23:32 -0700159 if (kl) {
160 kl(key);
161 }
Simon Hunt1eecfa22014-12-16 14:46:29 -0800162 }
163 }
164
Simon Hunta6ab9f02017-07-11 11:45:50 -0700165 // functions to obtain localized strings deferred from the setup of the
166 // global key data structures.
167 function qhlion() {
168 return ls.bundle('core.fw.QuickHelp');
169 }
170 function qhlion_show_hide() {
171 return qhlion()('qh_hint_show_hide_qh');
172 }
173
174 function qhlion_hint_esc() {
175 return qhlion()('qh_hint_esc');
176 }
177
178 function qhlion_hint_t() {
179 return qhlion()('qh_hint_t');
180 }
181
Simon Hunt1eecfa22014-12-16 14:46:29 -0800182 function setupGlobalKeys() {
Simon Hunt404f6b22015-01-21 14:00:56 -0800183 angular.extend(keyHandler, {
Simon Hunt1eecfa22014-12-16 14:46:29 -0800184 globalKeys: {
Simon Hunta6ab9f02017-07-11 11:45:50 -0700185 backSlash: [quickHelp, qhlion_show_hide],
186 slash: [quickHelp, qhlion_show_hide],
187 esc: [escapeKey, qhlion_hint_esc],
188 T: [toggleTheme, qhlion_hint_t]
Simon Hunt1eecfa22014-12-16 14:46:29 -0800189 },
190 globalFormat: ['backSlash', 'slash', 'esc', 'T'],
191
192 // Masked keys are global key handlers that always return true.
193 // That is, the view will never see the event for that key.
194 maskedKeys: {
Simon Hunt120904e2016-06-23 14:26:26 -0700195 slash: 1,
196 backSlash: 1,
197 T: 1
Simon Hunt1eecfa22014-12-16 14:46:29 -0800198 }
199 });
200 }
201
202 function quickHelp(view, key, code, ev) {
Simon Huntd5579252015-10-06 15:09:14 -0700203 if (!globalEnabled) {
204 return false;
205 }
Simon Hunt639dc662015-02-18 14:19:20 -0800206 qhs.showQuickHelp(keyHandler);
Simon Hunt1eecfa22014-12-16 14:46:29 -0800207 return true;
208 }
209
Simon Hunta0eb0a82015-02-11 12:30:06 -0800210 // returns true if we 'consumed' the ESC keypress, false otherwise
Simon Hunt1eecfa22014-12-16 14:46:29 -0800211 function escapeKey(view, key, code, ev) {
Simon Hunt9d286562015-03-09 13:53:50 -0700212 return ns.hideIfShown() || qhs.hideQuickHelp();
Simon Hunt1eecfa22014-12-16 14:46:29 -0800213 }
214
215 function toggleTheme(view, key, code, ev) {
Simon Huntd5579252015-10-06 15:09:14 -0700216 if (!globalEnabled) {
217 return false;
218 }
Yuta HIGUCHI4f39bcd2014-12-18 20:46:14 -0800219 ts.toggleTheme();
Simon Hunt1eecfa22014-12-16 14:46:29 -0800220 return true;
221 }
222
Simon Hunt5198f082016-02-04 13:41:17 -0800223 function filterMaskedKeys(map, caller, remove) {
224 var masked = [],
225 msgs = [];
Simon Hunt59df0b22014-12-17 10:32:25 -0800226
Simon Hunt5198f082016-02-04 13:41:17 -0800227 d3.map(map).keys().forEach(function (key) {
228 if (keyHandler.maskedKeys[key]) {
229 masked.push(key);
230 msgs.push(caller, ': Key "' + key + '" is reserved');
Simon Hunt59df0b22014-12-17 10:32:25 -0800231 }
Simon Hunt5198f082016-02-04 13:41:17 -0800232 });
233
234 if (msgs.length) {
235 $log.warn(msgs.join('\n'));
236 }
237
238 if (remove) {
239 masked.forEach(function (k) {
240 delete map[k];
241 });
242 }
243 return masked;
244 }
245
246 function unexParam(fname, x) {
247 $log.warn(fname, ": unexpected parameter-- ", x);
248 }
249
250 function setKeyBindings(keyArg) {
251 var fname = 'setKeyBindings()',
252 kFunc = fs.isF(keyArg),
253 kMap = fs.isO(keyArg);
254
255 if (kFunc) {
256 // set general key handler callback
257 keyHandler.viewFn = kFunc;
258 } else if (kMap) {
259 filterMaskedKeys(kMap, fname, true);
260 keyHandler.viewKeys = kMap;
261 } else {
262 unexParam(fname, keyArg);
Simon Hunt59df0b22014-12-17 10:32:25 -0800263 }
264 }
265
266 function getKeyBindings() {
267 var gkeys = d3.map(keyHandler.globalKeys).keys(),
268 masked = d3.map(keyHandler.maskedKeys).keys(),
269 vkeys = d3.map(keyHandler.viewKeys).keys(),
Yuta HIGUCHI4f39bcd2014-12-18 20:46:14 -0800270 vfn = !!fs.isF(keyHandler.viewFn);
Simon Hunt59df0b22014-12-17 10:32:25 -0800271
272 return {
273 globalKeys: gkeys,
274 maskedKeys: masked,
275 viewKeys: vkeys,
276 viewFunction: vfn
277 };
278 }
279
Bri Prebilic Cole9dcaea52015-07-21 14:39:48 -0700280 function unbindKeys() {
281 keyHandler.viewKeys = {};
282 keyHandler.viewFn = null;
283 keyHandler.viewGestures = [];
284 }
285
Simon Hunt5198f082016-02-04 13:41:17 -0800286 function bindDialogKeys(map) {
287 var fname = 'bindDialogKeys()',
288 kMap = fs.isO(map);
289
290 if (kMap) {
291 filterMaskedKeys(map, fname, true);
292 keyHandler.dialogKeys = kMap;
293 } else {
294 unexParam(fname, map);
295 }
296 }
297
298 function unbindDialogKeys() {
299 keyHandler.dialogKeys = {};
300 }
301
Simon Hunt71892222015-09-29 13:39:40 -0700302 function checkNotGlobal(o) {
303 var oops = [];
304 if (fs.isO(o)) {
305 angular.forEach(o, function (val, key) {
306 if (keyHandler.globalKeys[key]) {
307 oops.push(key);
308 }
309 });
310 if (oops.length) {
311 $log.warn('Ignoring reserved global key(s):', oops.join(','));
312 oops.forEach(function (key) {
313 delete o[key];
314 });
315 }
316 }
317 }
318
Simon Huntdc6362a2014-12-18 19:55:23 -0800319 angular.module('onosUtil')
Simon Hunt639dc662015-02-18 14:19:20 -0800320 .factory('KeyService',
Simon Hunt9f5e97d2015-12-11 10:32:09 -0800321 ['$log', '$timeout', 'FnService', 'ThemeService', 'NavService',
Simon Hunta6ab9f02017-07-11 11:45:50 -0700322 'EeService', 'LionService',
Simon Hunt639dc662015-02-18 14:19:20 -0800323
Simon Hunta6ab9f02017-07-11 11:45:50 -0700324 function (_$log_, _$timeout_, _fs_, _ts_, _ns_, _ee_, _ls_) {
Yuta HIGUCHI4f39bcd2014-12-18 20:46:14 -0800325 $log = _$log_;
Simon Hunt9f5e97d2015-12-11 10:32:09 -0800326 $timeout = _$timeout_;
Yuta HIGUCHI4f39bcd2014-12-18 20:46:14 -0800327 fs = _fs_;
328 ts = _ts_;
Simon Hunt9d286562015-03-09 13:53:50 -0700329 ns = _ns_;
Simon Huntf4ef6dd2016-02-03 17:05:14 -0800330 ee = _ee_;
Simon Hunta6ab9f02017-07-11 11:45:50 -0700331 ls = _ls_;
Yuta HIGUCHI4f39bcd2014-12-18 20:46:14 -0800332
Simon Huntdc6362a2014-12-18 19:55:23 -0800333 return {
Simon Hunt639dc662015-02-18 14:19:20 -0800334 bindQhs: function (_qhs_) {
335 qhs = _qhs_;
336 },
Simon Huntdc6362a2014-12-18 19:55:23 -0800337 installOn: function (elem) {
338 elem.on('keydown', keyIn);
339 setupGlobalKeys();
340 },
Simon Huntdc6362a2014-12-18 19:55:23 -0800341 keyBindings: function (x) {
342 if (x === undefined) {
343 return getKeyBindings();
344 } else {
345 setKeyBindings(x);
346 }
347 },
Bri Prebilic Cole9dcaea52015-07-21 14:39:48 -0700348 unbindKeys: unbindKeys,
Simon Hunt5198f082016-02-04 13:41:17 -0800349 dialogKeys: function (x) {
350 if (x === undefined) {
351 unbindDialogKeys();
352 } else {
353 bindDialogKeys(x);
354 }
355 },
Simon Huntf4ef6dd2016-02-03 17:05:14 -0800356 addSeq: function (word, data) {
357 fs.addToTrie(seq, word, data);
358 },
359 remSeq: function (word) {
360 fs.removeFromTrie(seq, word);
361 },
Simon Huntdc6362a2014-12-18 19:55:23 -0800362 gestureNotes: function (g) {
363 if (g === undefined) {
364 return keyHandler.viewGestures;
365 } else {
Yuta HIGUCHI4f39bcd2014-12-18 20:46:14 -0800366 keyHandler.viewGestures = fs.isA(g) || [];
Simon Huntdc6362a2014-12-18 19:55:23 -0800367 }
Simon Hunt36fc15c2015-02-12 17:02:58 -0800368 },
369 enableKeys: function (b) {
370 enabled = b;
Simon Hunt71892222015-09-29 13:39:40 -0700371 },
Simon Huntd5579252015-10-06 15:09:14 -0700372 enableGlobalKeys: function (b) {
373 globalEnabled = b;
374 },
Simon Hunt71892222015-09-29 13:39:40 -0700375 checkNotGlobal: checkNotGlobal
Simon Huntdc6362a2014-12-18 19:55:23 -0800376 };
Simon Hunt1eecfa22014-12-16 14:46:29 -0800377 }]);
378
Simon Huntdc6362a2014-12-18 19:55:23 -0800379}());