1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates an empty keyboard manager. Intended for use as a singleton.
 26  * @constructor
 27  * @class
 28  * This class is responsible for managing focus and shortcuts via the keyboard. That includes dispatching
 29  * keyboard events (shortcuts), as well as managing tab groups. It is at the heart of the
 30  * Dwt keyboard navigation framework.
 31  * <p>
 32  * {@link DwtKeyboardMgr} intercepts key strokes and translates
 33  * them into actions which it then dispatches to the component with focus. If the key
 34  * stroke is a TAB (or Shift-TAB), then focus is moved based on the current tab group.
 35  * </p><p>
 36  * A {@link DwtShell} instantiates its own <i>DwtKeyboardMgr</i> at construction.
 37  * The keyboard manager may then be retrieved via the shell's <code>getKeyboardMgr()</code>
 38  * function. Once a handle to the shell's keyboard manager is retrieved, then the user is free
 39  * to add tab groups, and to register keymaps and handlers with the keyboard manager.
 40  * </p><p>
 41  * Focus is managed among a stack of tab groups. The TAB button will move the focus within the
 42  * current tab group. When a non-TAB is received, we first check if the control can handle it.
 43  * In general, control key events simulate something the user could do with the mouse, and change
 44  * the state/appearance of the control. For example, ENTER on a DwtButton simulates a button
 45  * press. If the control does not handle the key event, the event is handed to the application,
 46  * which handles it based on its current state. The application key event handler is in a sense
 47  * global, since it does not matter which control received the event.
 48  * </p><p>
 49  * At any given time there is a default handler, which is responsible for determining what
 50  * action is associated with a particular key sequence, and then taking it. A handler should support
 51  * the following methods:
 52  * 
 53  * <ul>
 54  * <li><i>getKeyMapName()</i> -- returns the name of the map that defines shortcuts for this handler</li>
 55  * <li><i>handleKeyAction()</i> -- performs the action associated with a shortcut</li>
 56  * <li><i>handleKeyEvent()</i>	-- optional override; handler solely responsible for handling event</li>
 57  * </ul>
 58  * </p>
 59  *
 60  * @author Ross Dargahi
 61  *
 62  * @param	{DwtShell}	shell		the shell
 63  * @see DwtShell
 64  * @see DwtTabGroup
 65  * @see DwtKeyMap
 66  * @see DwtKeyMapMgr
 67  * 
 68  * @private
 69  */
 70 DwtKeyboardMgr = function(shell) {
 71 
 72 	DwtKeyboardMgr.__shell = shell;
 73 
 74     this.__kbEventStatus = DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED;
 75     this.__keyTimeout = DwtKeyboardMgr.SHORTCUT_TIMEOUT;
 76 
 77     // focus
 78     this.__tabGrpStack = [];
 79     this.__currTabGroup = null;
 80     this.__tabGroupChangeListenerObj = this.__tabGrpChangeListener.bind(this);
 81 
 82     // shortcuts
 83     this.__shortcutsEnabled = false;
 84 	this.__defaultHandlerStack = [];
 85 	this.__currDefaultHandler = null;
 86     this.__killKeySeqTimedAction = new AjxTimedAction(this, this.__killKeySequenceAction);
 87     this.__killKeySeqTimedActionId = -1;
 88     this.__keySequence = [];
 89     this._evtMgr = new AjxEventMgr();
 90 
 91     Dwt.setHandler(document, DwtEvent.ONKEYDOWN, DwtKeyboardMgr.__keyDownHdlr);
 92     Dwt.setHandler(document, DwtEvent.ONKEYUP, DwtKeyboardMgr.__keyUpHdlr);
 93     Dwt.setHandler(document, DwtEvent.ONKEYPRESS, DwtKeyboardMgr.__keyPressHdlr);
 94 };
 95 
 96 DwtKeyboardMgr.prototype.isDwtKeyboardMgr = true;
 97 DwtKeyboardMgr.prototype.toString = function() { return "DwtKeyboardMgr"; };
 98 
 99 DwtKeyboardMgr.SHORTCUT_TIMEOUT = 750;
100 
101 DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED	= "NOT HANDLED";
102 DwtKeyboardMgr.__KEYSEQ_HANDLED		= "HANDLED";
103 DwtKeyboardMgr.__KEYSEQ_PENDING		= "PENDING";
104 
105 /**
106  * Checks if the event may be a shortcut from within an input (text input or
107  * textarea). Since printable characters are echoed, the shortcut must be non-printable:
108  * 
109  * <ul>
110  * <li>Alt or Ctrl or Meta plus another key</li>
111  * <li>Esc</li>
112  * </ul>
113  * 
114  * @param {DwtKeyEvent}	ev	the key event
115  * @return	{boolean}	<code>true</code> if the event may be a shortcut
116  */
117 
118 // Enter and all four arrows can be used as shortcuts in an INPUT
119 DwtKeyboardMgr.IS_INPUT_SHORTCUT_KEY = AjxUtil.arrayAsHash([
120     DwtKeyEvent.KEY_END_OF_TEXT,
121     DwtKeyEvent.KEY_RETURN,
122     DwtKeyEvent.KEY_ARROW_LEFT,
123     DwtKeyEvent.KEY_ARROW_UP,
124     DwtKeyEvent.KEY_ARROW_RIGHT,
125     DwtKeyEvent.KEY_ARROW_DOWN
126 ]);
127 
128 // Returns true if the key event has a keycode that could be used in an input (INPUT or TEXTAREA) as a shortcut. That
129 // excludes printable characters.
130 DwtKeyboardMgr.isPossibleInputShortcut = function(ev) {
131 
132 	var target = DwtUiEvent.getTarget(ev);
133     return !DwtKeyMap.IS_MODIFIER[ev.keyCode] && (ev.keyCode === DwtKeyEvent.KEY_ESCAPE || DwtKeyMapMgr.hasModifier(ev) ||
134 			(target && target.nodeName.toLowerCase() == "input" && DwtKeyboardMgr.IS_INPUT_SHORTCUT_KEY[ev.keyCode]));
135 };
136 
137 /**
138  * Pushes the tab group onto the stack and makes it the active tab group.
139  * 
140  * @param 	{DwtTabGroup}	tabGroup	the tab group to push onto the stack
141  * 
142  * @see		#popTabGroup
143  */
144 DwtKeyboardMgr.prototype.pushTabGroup = function(tabGroup, preventFocus) {
145 
146     if (!(tabGroup && tabGroup.isDwtTabGroup)) {
147         DBG.println(AjxDebug.DBG1, "pushTabGroup() called without a tab group: " + tabGroup);
148         return;
149     }
150 
151 	DBG.println(AjxDebug.FOCUS, "PUSH tab group " + tabGroup.getName());
152 	this.__tabGrpStack.push(tabGroup);
153 	this.__currTabGroup = tabGroup;
154 	var focusMember = tabGroup.getFocusMember();
155 	if (!focusMember) {
156 		focusMember = tabGroup.resetFocusMember(true);
157 	}
158 	if (!focusMember) {
159 		DBG.println(AjxDebug.FOCUS, "DwtKeyboardMgr.pushTabGroup: tab group " + tabGroup.__name + " has no members!");
160 		return;
161 	}
162 	tabGroup.addFocusChangeListener(this.__tabGroupChangeListenerObj);
163 	if (!preventFocus) {
164 		this.grabFocus(focusMember);
165 	}
166 };
167 
168 /**
169  * Pops the current tab group off the top of the tab group stack. The previous 
170  * tab group (if there is one) then becomes the current tab group.
171  * 
172  * @param {DwtTabGroup} [tabGroup]		the tab group to pop. If supplied, then the tab group
173  * 		stack is searched for the tab group and it is removed. If <code>null</code>, then the
174  * 		top tab group is popped.
175  * 
176  * @return {DwtTabGroup}	the popped tab group or <code>null</code> if there is one or less tab groups
177  */
178 DwtKeyboardMgr.prototype.popTabGroup = function(tabGroup) {
179 
180     if (!(tabGroup && tabGroup.isDwtTabGroup)) {
181         DBG.println(AjxDebug.DBG1, "popTabGroup() called without a tab group: " + tabGroup);
182         return null;
183     }
184 
185     DBG.println(AjxDebug.FOCUS, "POP tab group " + tabGroup.getName());
186 	
187 	// we never want an empty stack
188 	if (this.__tabGrpStack.length <= 1) {
189 		return null;
190 	}
191 	
192 	// If we are popping a tab group that is not on the top of the stack then
193 	// we need to find it and remove it.
194 	if (tabGroup && this.__tabGrpStack[this.__tabGrpStack.length - 1] != tabGroup) {
195 		var a = this.__tabGrpStack;
196 		var len = a.length;
197 		for (var i = len - 1; i >= 0; i--) {
198 			if (tabGroup == a[i]) {
199 				a[i].dump(AjxDebug.DBG1);
200 				break;
201 			}
202 		}
203 		
204 		/* If there is no match in the stack for tabGroup, then simply return null,
205 		 * else if the match is not the top item on the stack, then remove it from 
206 		 * the stack. Else we are dealing with the topmost item on the stack so handle it 
207 		 * as a simple pop. */
208 		if (i < 0) { // No match
209 			return null;
210 		} else if (i != len - 1) { // item is not on top
211 			// Remove tabGroup
212 			a.splice(i, 1);
213 			return tabGroup;
214 		}
215 	} 
216 
217 	var tabGroup = this.__tabGrpStack.pop();
218 	tabGroup.removeFocusChangeListener(this.__tabGroupChangeListenerObj);
219 	
220 	var currTg = null;
221 	if (this.__tabGrpStack.length > 0) {
222 		currTg = this.__tabGrpStack[this.__tabGrpStack.length - 1];
223 		var focusMember = currTg.getFocusMember();
224 		if (!focusMember) {
225 			focusMember = currTg.resetFocusMember(true);
226 		}
227 		if (focusMember) {
228 			this.grabFocus(focusMember);
229 		}
230 	}
231 	this.__currTabGroup = currTg;
232 
233 	return tabGroup;
234 };
235 
236 /**
237  * Replaces the current tab group with the given tab group.
238  * 
239  * @param {DwtTabGroup} tabGroup 	the tab group to use
240  * @return {DwtTabGroup}	the old tab group
241  */
242 DwtKeyboardMgr.prototype.setTabGroup = function(tabGroup) {
243 
244 	var otg = this.popTabGroup();
245 	this.pushTabGroup(tabGroup);
246 
247 	return otg;
248 };
249 
250 /**
251  * Gets the current tab group
252  *
253  * @return {DwtTabGroup}	current tab group
254  */
255 DwtKeyboardMgr.prototype.getCurrentTabGroup = function() {
256 
257     return this.__currTabGroup;
258 };
259 
260 /**
261  * Adds a default handler to the stack. A handler should define a 'handleKeyAction' method.
262  *
263  * @param {Object}  handler     default handler
264  */
265 DwtKeyboardMgr.prototype.pushDefaultHandler = function(handler) {
266 
267 	if (!this.isEnabled() || !handler) {
268         return;
269     }
270 	DBG.println(AjxDebug.FOCUS, "PUSH default handler: " + handler);
271 		
272 	this.__defaultHandlerStack.push(handler);
273 	this.__currDefaultHandler = handler;
274 };
275 
276 /**
277  * Removes a default handler from the stack.
278  *
279  * @return {Object}  handler     a default handler
280  */
281 DwtKeyboardMgr.prototype.popDefaultHandler = function() {
282 
283 	DBG.println(AjxDebug.FOCUS, "POP default handler");
284 	// we never want an empty stack
285 	if (this.__defaultHandlerStack.length <= 1) {
286         return null;
287     }
288 
289 	DBG.println(AjxDebug.FOCUS, "Default handler stack length: " + this.__defaultHandlerStack.length);
290 	var handler = this.__defaultHandlerStack.pop();
291 	this.__currDefaultHandler = this.__defaultHandlerStack[this.__defaultHandlerStack.length - 1];
292 	DBG.println(AjxDebug.FOCUS, "Default handler is now: " + this.__currDefaultHandler);
293 
294 	return handler;
295 };
296 
297 /**
298  * Sets the focus to the given object.
299  * 
300  * @param {HTMLInputElement|DwtControl|string} focusObj		the object to which to set focus, or its ID
301  */ 
302 DwtKeyboardMgr.prototype.grabFocus = function(focusObj) {
303 
304 	if (typeof focusObj === "string") {
305 		focusObj = document.getElementById(focusObj);
306 	}
307     else if (focusObj && focusObj.isDwtTabGroup) {
308         focusObj = focusObj.getFocusMember() || focusObj.getFirstMember();
309     }
310 
311     if (!focusObj) {
312         return;
313     }
314 
315 	// Make sure tab group knows what's currently focused
316 	if (this.__currTabGroup) {
317 		this.__currTabGroup.setFocusMember(focusObj, false, true);
318 	}
319 		
320 	this.__doGrabFocus(focusObj);
321 };
322 
323 /**
324  * Tells the keyboard manager that the given control now has focus. That control will handle shortcuts and become
325  * the reference point for tabbing.
326  *
327  * @param {DwtControl|Element}  focusObj    control (or element) that has focus
328  */
329 DwtKeyboardMgr.prototype.updateFocus = function(focusObj, ev) {
330 
331     if (!focusObj) {
332         return;
333     }
334 
335     var ctg = this.__currTabGroup;
336     if (ctg) {
337         this.__currTabGroup.__showFocusedItem(focusObj, "updateFocus");
338     }
339     var control = focusObj.isDwtControl ? focusObj : DwtControl.findControl(focusObj);
340 
341     // Set the keyboard mgr's focus obj, which will be handed shortcuts. It must be a DwtControl.
342     if (control) {
343         this.__focusObj = control;
344         DBG.println(AjxDebug.FOCUS, "DwtKeyboardMgr UPDATEFOCUS kbMgr focus obj: " + control);
345     }
346 
347     // Update the current (usually root) tab group's focus member to whichever of these it contains: the focus obj,
348     // its tab group member, or its control.
349     var tgm = this._findTabGroupMember(ev || focusObj);
350     if (tgm && ctg) {
351         ctg.setFocusMember(tgm, false, true);
352     }
353 };
354 
355 // Goes up the DOM looking for something (element or control) that is in the current tab group.
356 DwtKeyboardMgr.prototype._findTabGroupMember = function(obj) {
357 
358     var ctg = this.__currTabGroup;
359     if (!obj || !ctg) {
360         return;
361     }
362 
363     var htmlEl = (obj.isDwtControl && obj.getHtmlElement()) || DwtUiEvent.getTarget(obj, false) || obj;
364 
365     try {
366         while (htmlEl) {
367             if (ctg.contains(htmlEl)) {
368                 return htmlEl;
369             }
370             else {
371                 var control = DwtControl.ALL_BY_ID[htmlEl.id];
372                 if (control && ctg.contains(control)) {
373                     return control;
374                 }
375                 else {
376                     var tgm = control && control.getTabGroupMember && control.getTabGroupMember();
377                     if (tgm && ctg.contains(tgm)) {
378                         return tgm;
379                     }
380                 }
381             }
382             htmlEl = htmlEl.parentNode;
383         }
384     } catch(e) {
385     }
386 
387     return null;
388 };
389 
390 /**
391  * Gets the object that has focus.
392  *
393  * @return {HTMLInputElement|DwtControl} focusObj		the object with focus
394  */
395 DwtKeyboardMgr.prototype.getFocusObj = function(focusObj) {
396 
397 	return this.__focusObj;
398 };
399 
400 /**
401  * This method is used to register an application key handler. If registered, this
402  * handler must support the following methods:
403  * <ul>
404  * <li><i>getKeyMapName</i>: This method returns a string representing the key map 
405  * to be used for looking up actions
406  * <li><i>handleKeyAction</i>: This method should handle the key action and return
407  * true if it handled it else false. <i>handleKeyAction</i> has two formal parameters
408  *    <ul>
409  *    <li><i>actionCode</i>: The action code to be handled</li>
410  *    <li><i>ev</i>: the {@link DwtKeyEvent} corresponding to the last key event in the sequence</li>
411  *    </ul>
412  * </ul>
413  * 
414  * @param 	{function}	hdlr	the handler function. This method should have the following
415  * 									signature <code>Boolean hdlr(Int actionCode DwtKeyEvent event);</code>
416  * 
417  * @see DwtKeyEvent
418  */
419 DwtKeyboardMgr.prototype.registerDefaultKeyActionHandler = function(hdlr) {
420 
421 	if (this.isEnabled()) {
422         this.__defaultKeyActionHdlr = hdlr;
423     }
424 };
425 
426 /**
427  * Registers a keymap with the shell. A keymap typically
428  * is a subclass of {@link DwtKeyMap} and defines the mappings from key sequences to
429  * actions.
430  *
431  * @param {DwtKeyMap} keyMap		the key map to register
432  * 
433  */
434 DwtKeyboardMgr.prototype.registerKeyMap = function(keyMap) {
435 
436 	if (this.isEnabled()) {
437 	    this.__keyMapMgr = new DwtKeyMapMgr(keyMap);
438     }
439 };
440 
441 /**
442  * Sets the timeout (in milliseconds) between key presses for handling multi-keypress sequences.
443  * 
444  * @param 	{number}	timeout		the timeout (in milliseconds)
445  */
446 DwtKeyboardMgr.prototype.setKeyTimeout = function(timeout) {
447 	this.__keyTimeout = timeout;
448 };
449 
450 /**
451  * Clears the key sequence. The next key event will begin a new one.
452  * 
453  */
454 DwtKeyboardMgr.prototype.clearKeySeq = function() {
455 
456 	this.__killKeySeqTimedActionId = -1;
457 	this.__keySequence = [];
458 };
459 
460 /**
461  * Enables/disables keyboard nav (shortcuts).
462  * 
463  * @param 	{boolean}	enabled		if <code>true</code>, enable keyboard nav
464  */
465 DwtKeyboardMgr.prototype.enable = function(enabled) {
466 
467 	DBG.println(AjxDebug.DBG2, "keyboard shortcuts enabled: " + enabled);
468 	this.__shortcutsEnabled = enabled;
469 };
470 
471 DwtKeyboardMgr.prototype.isEnabled = function() {
472 	return this.__shortcutsEnabled;
473 };
474 
475 /**
476  * Adds a global key event listener.
477  *
478  * @param {constant}	ev			key event type
479  * @param {AjxListener}	listener	listener to notify
480  */
481 DwtKeyboardMgr.prototype.addListener = function(ev, listener) {
482 	this._evtMgr.addListener(ev, listener);
483 };
484 
485 /**
486  * Removes a global key event listener.
487  *
488  * @param {constant}	ev			key event type
489  * @param {AjxListener}	listener	listener to remove
490  */
491 DwtKeyboardMgr.prototype.removeListener = function(ev, listener) {
492 	this._evtMgr.removeListener(ev, listener);
493 };
494 
495 DwtKeyboardMgr.prototype.__doGrabFocus = function(focusObj) {
496 
497 	if (!focusObj) {
498         return;
499     }
500 
501     var curFocusObj = this.getFocusObj();
502     if (curFocusObj && curFocusObj.blur) {
503         DBG.println(AjxDebug.FOCUS, "DwtKeyboardMgr DOGRABFOCUS cur focus obj: " + [curFocusObj, curFocusObj._htmlElId || curFocusObj.id].join(' / '));
504         curFocusObj.blur();
505     }
506 
507     DBG.println(AjxDebug.FOCUS, "DwtKeyboardMgr DOGRABFOCUS new focus obj: " + [focusObj, focusObj._htmlElId || focusObj.id].join(' / '));
508     if (focusObj.focus) {
509         // focus handler should lead to focus update, but just in case ...
510         this.updateFocus(focusObj.focus() || focusObj);
511     }
512 };
513 
514 /**
515  * @private
516  */
517 DwtKeyboardMgr.__keyUpHdlr = function(ev) {
518 
519 	ev = DwtUiEvent.getEvent(ev);
520 	DBG.println(AjxDebug.KEYBOARD, "keyup: " + ev.keyCode);
521 
522 	var kbMgr = DwtKeyboardMgr.__shell.getKeyboardMgr();
523 	if (kbMgr._evtMgr.notifyListeners(DwtEvent.ONKEYUP, ev) === false) {
524 		return false;
525 	}
526 
527 	// clear saved Gecko key
528 	if (AjxEnv.isMac && AjxEnv.isGeckoBased && ev.keyCode === 0) {
529 		return DwtKeyboardMgr.__keyDownHdlr(ev);
530 	}
531     else {
532 		return DwtKeyboardMgr.__handleKeyEvent(ev);
533 	}
534 };
535 
536 /**
537  * @private
538  */
539 DwtKeyboardMgr.__keyPressHdlr = function(ev) {
540 
541 	ev = DwtUiEvent.getEvent(ev);
542 	DBG.println(AjxDebug.KEYBOARD, "keypress: " + (ev.keyCode || ev.charCode));
543 
544 	var kbMgr = DwtKeyboardMgr.__shell.getKeyboardMgr();
545 	if (kbMgr._evtMgr.notifyListeners(DwtEvent.ONKEYPRESS, ev) === false) {
546 		return false;
547 	}
548 
549 	DwtKeyEvent.geckoCheck(ev);
550 
551 	return DwtKeyboardMgr.__handleKeyEvent(ev);
552 };
553 
554 /**
555  * @private
556  */
557 DwtKeyboardMgr.__handleKeyEvent =
558 function(ev) {
559 
560 	if (DwtKeyboardMgr.__shell._blockInput) {
561         return false;
562     }
563 
564 	ev = DwtUiEvent.getEvent(ev, this);
565 	DBG.println(AjxDebug.KEYBOARD, [ev.type, ev.keyCode, ev.charCode, ev.which].join(" / "));
566 	var kbMgr = DwtKeyboardMgr.__shell.getKeyboardMgr();
567 	var kev = DwtShell.keyEvent;
568 	kev.setFromDhtmlEvent(ev);
569 
570 	if (kbMgr.__kbEventStatus != DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED) {
571 		return kbMgr.__processKeyEvent(ev, kev, false);
572 	}
573 };
574 
575 /**
576  * @private
577  */
578 DwtKeyboardMgr.__keyDownHdlr = function(ev) {
579 
580 	try {
581 
582 	ev = DwtUiEvent.getEvent(ev, this);
583 	var kbMgr = DwtKeyboardMgr.__shell.getKeyboardMgr();
584 	ev.focusObj = null;
585 	if (kbMgr._evtMgr.notifyListeners(DwtEvent.ONKEYDOWN, ev) === false) {
586 		return false;
587 	}
588 
589 	if (DwtKeyboardMgr.__shell._blockInput) {
590         return false;
591     }
592 	DBG.println(AjxDebug.KEYBOARD, [ev.type, ev.keyCode, ev.charCode, ev.which].join(" / "));
593 
594 	var kev = DwtShell.keyEvent;
595 	kev.setFromDhtmlEvent(ev);
596 	var keyCode = DwtKeyEvent.getCharCode(ev);
597 	DBG.println(AjxDebug.KEYBOARD, "keydown: " + keyCode + " -------- " + ev.target);
598 
599 	// Popdown any tooltip
600 	DwtKeyboardMgr.__shell.getToolTip().popdown();
601 
602     /********* FOCUS MANAGEMENT *********/
603 
604 	/* The first thing we care about is the tab key since we want to manage
605 	 * focus based on the tab groups. 
606 	 * 
607 	 * If the tab hit happens in the currently
608 	 * focused obj, the go to the next/prev element in the tab group. 
609 	 * 
610 	 * If the tab happens in an element that is in the tab group hierarchy, but that 
611 	 * element is not the currently focus element in the tab hierarchy (e.g. the user
612 	 * clicked in it and we didnt detect it) then sync the tab group's current focus 
613 	 * element and handle the tab
614 	 * 
615 	 * If the tab happens in an object not under the tab group hierarchy, then set
616 	 * focus to the current focus object in the tab hierarchy i.e. grab back control
617 	 */
618     var ctg = kbMgr.__currTabGroup,
619         member;
620 
621 	if (keyCode == DwtKeyEvent.KEY_TAB) {
622 	    if (ctg && !DwtKeyMapMgr.hasModifier(kev)) {
623 			DBG.println(AjxDebug.FOCUS, "Tab");
624 			// If the tab hit is in an element or if the current tab group has a focus member
625 			if (ctg.getFocusMember()) {
626                 member = kev.shiftKey ? ctg.getPrevFocusMember(true) : ctg.getNextFocusMember(true);
627 			}
628             else {
629 			 	DBG.println(AjxDebug.FOCUS, "DwtKeyboardMgr.__keyDownHdlr: no current focus member, resetting to first in tab group");
630 			 	// If there is no current focus member, then reset
631                 member = ctg.resetFocusMember(true);
632 			}
633 	    }
634         // If we did not handle the Tab, let the browser handle it
635         return kbMgr.__processKeyEvent(ev, kev, !member, member ? DwtKeyboardMgr.__KEYSEQ_HANDLED : DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED);
636 	}
637     else if (ctg && AjxEnv.isGecko && kev.target instanceof HTMLHtmlElement) {
638 	 	/* With FF we focus get set to the <html> element when tabbing in
639 	 	 * from the address or search fields. What we want to do is capture
640 	 	 * this here and reset the focus to the first element in the tabgroup
641 	 	 * 
642 	 	 * TODO Verify this trick is needed/works with IE/Safari
643 	 	 */
644         member = ctg.resetFocusMember(true);
645 	}
646 	 
647     // Allow key events to propagate when keyboard manager is disabled (to avoid taking over browser shortcuts). Bugzilla #45469.
648     if (!kbMgr.isEnabled()) {
649         return true;
650     }
651 
652 
653     /********* SHORTCUTS *********/
654 
655 	// Filter out modifier keys. If we're in an input field, filter out legitimate input.
656 	// (A shortcut from an input field must use a modifier key.)
657 	if (DwtKeyMap.IS_MODIFIER[keyCode] || (kbMgr.__killKeySeqTimedActionId === -1 &&
658 		kev.target && DwtKeyMapMgr.isInputElement(kev.target) && !kev.target["data-hidden"] && !DwtKeyboardMgr.isPossibleInputShortcut(kev))) {
659 
660 	 	return kbMgr.__processKeyEvent(ev, kev, true, DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED);
661 	}
662 	 
663 	/* Cancel any pending time action to kill the keysequence */
664 	if (kbMgr.__killKeySeqTimedActionId != -1) {
665 		AjxTimedAction.cancelAction(kbMgr.__killKeySeqTimedActionId);
666 		kbMgr.__killKeySeqTimedActionId = -1;
667 	}
668 		
669  	var parts = [];
670 	if (kev.altKey) 	{ parts.push(DwtKeyMap.ALT); }
671 	if (kev.ctrlKey) 	{ parts.push(DwtKeyMap.CTRL); }
672  	if (kev.metaKey) 	{ parts.push(DwtKeyMap.META); }
673 	if (kev.shiftKey) 	{ parts.push(DwtKeyMap.SHIFT); }
674 	parts.push(keyCode);
675 	kbMgr.__keySequence[kbMgr.__keySequence.length] = parts.join(DwtKeyMap.JOIN);
676 
677 	DBG.println(AjxDebug.KEYBOARD, "KEYCODE: " + keyCode + " - KEY SEQ: " + kbMgr.__keySequence.join(""));
678 	
679 	var handled = DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED;
680 
681 	// First see if the control that currently has focus can handle the key event
682 	var obj = ev.focusObj || kbMgr.__focusObj;
683     var hasFocus = obj && obj.hasFocus && obj.hasFocus();
684     DBG.println(AjxDebug.KEYBOARD, "DwtKeyboardMgr::__keyDownHdlr - focus object " + obj + " has focus: " + hasFocus);
685 	if (hasFocus && obj.handleKeyAction) {
686 		handled = kbMgr.__dispatchKeyEvent(obj, kev);
687 		while ((handled === DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED) && obj.parent) {
688 			obj = obj.parent;
689             if (obj.getKeyMapName) {
690 			    handled = kbMgr.__dispatchKeyEvent(obj, kev);
691             }
692 		}
693 	}
694 
695 	// If the currently focused control didn't handle the event, hand it to the default key event handler
696 	if (handled === DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED && kbMgr.__currDefaultHandler) {
697 		handled = kbMgr.__dispatchKeyEvent(kbMgr.__currDefaultHandler, kev);
698 	}
699 
700 	// see if we should let browser handle the event as well; note that we need to set the 'handled' var rather than
701 	// just the 'propagate' one below, since the keyboard mgr isn't built for both it and the browser to handle the event.
702 	if (kev.forcePropagate) {
703 		handled = DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED;
704 		kev.forcePropagate = false;
705 	}
706 	
707 	kbMgr.__kbEventStatus = handled;
708 	var propagate = (handled == DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED);
709 
710 	if (handled != DwtKeyboardMgr.__KEYSEQ_PENDING) {
711 		kbMgr.clearKeySeq();
712 	}
713 
714 	return kbMgr.__processKeyEvent(ev, kev, propagate);
715 
716 	} catch (ex) {
717 		AjxException.reportScriptError(ex);
718 	}
719 };
720 
721 /**
722  * Handles event dispatching
723  * 
724  * @private
725  */
726 DwtKeyboardMgr.prototype.__dispatchKeyEvent = function(hdlr, ev, forceActionCode) {
727 
728 	if (hdlr && hdlr.handleKeyEvent) {
729 		var handled = hdlr.handleKeyEvent(ev);
730 		return handled ? DwtKeyboardMgr.__KEYSEQ_HANDLED : DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED;
731 	}
732 
733 	var mapName = (hdlr && hdlr.getKeyMapName) ? hdlr.getKeyMapName() : null;
734 	if (!mapName) {
735 		return DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED;
736 	}
737 
738 	DBG.println(AjxDebug.KEYBOARD, "DwtKeyboardMgr.__dispatchKeyEvent: handler " + hdlr.toString() + " handling " + this.__keySequence + " for map: " + mapName);
739 	var actionCode = this.__keyMapMgr.getActionCode(this.__keySequence, mapName, forceActionCode);
740 	if (actionCode === DwtKeyMapMgr.NOT_A_TERMINAL) {
741 		DBG.println(AjxDebug.KEYBOARD, "scheduling action to kill key sequence");
742 		/* setup a timed action to redispatch/kill the key sequence in the event
743 		 * the user does not press another key in the allotted time */
744 		this.__hdlr = hdlr;
745 		this.__mapName = mapName;
746 		this.__ev = ev;
747 		this.__killKeySeqTimedActionId = AjxTimedAction.scheduleAction(this.__killKeySeqTimedAction, this.__keyTimeout);
748 		return DwtKeyboardMgr.__KEYSEQ_PENDING;	
749 	}
750     else if (actionCode != null) {
751 		/* It is possible that the component may not handle a valid action
752 		 * particulary actions defined in the default map */
753 		DBG.println(AjxDebug.KEYBOARD, "DwtKeyboardMgr.__dispatchKeyEvent: handling action: " + actionCode);
754 		if (!hdlr.handleKeyAction) {
755 			return DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED;
756 		}
757 		var result = hdlr.handleKeyAction(actionCode, ev);
758 		return result ? DwtKeyboardMgr.__KEYSEQ_HANDLED : DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED;
759 	}
760     else {
761 		DBG.println(AjxDebug.KEYBOARD, "DwtKeyboardMgr.__dispatchKeyEvent: no action code for " + this.__keySequence);
762 		return DwtKeyboardMgr.__KEYSEQ_NOT_HANDLED;
763 	}
764 };
765 
766 /**
767  * This method will reattempt to handle the event in the case that the intermediate
768  * node in the keymap may have an action code associated with it.
769  * 
770  * @private
771  */
772 DwtKeyboardMgr.prototype.__killKeySequenceAction = function() {
773 
774 	DBG.println(AjxDebug.KEYBOARD, "DwtKeyboardMgr.__killKeySequenceAction: " + this.__mapName);
775 	this.__dispatchKeyEvent(this.__hdlr, this.__ev, true);
776 	this.clearKeySeq();
777 };
778 
779 /**
780  * @private
781  */
782 DwtKeyboardMgr.prototype.__tabGrpChangeListener = function(ev) {
783 	this.__doGrabFocus(ev.newFocusMember);
784 };
785 
786 /**
787  * @private
788  */
789 DwtKeyboardMgr.prototype.__processKeyEvent = function(ev, kev, propagate, status) {
790 
791 	if (status) {
792 		this.__kbEventStatus = status;
793 	}
794 	kev._stopPropagation = !propagate;
795 	kev._returnValue = propagate;
796 	kev.setToDhtmlEvent(ev);
797 	DBG.println(AjxDebug.KEYBOARD, "key event returning: " + propagate);
798 	return propagate;
799 };
800