// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser; import android.view.KeyEvent; import org.chromium.base.annotations.SuppressFBWarnings; import org.chromium.chrome.R; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tabmodel.TabModel; import org.chromium.chrome.browser.tabmodel.TabModelUtils; import org.chromium.content.browser.ContentViewCore; /** * Implements app-level keyboard shortcuts for ChromeTabbedActivity and DocumentActivity. */ public class KeyboardShortcuts { private static final int CTRL = 1 << 31; private static final int ALT = 1 << 30; private static final int SHIFT = 1 << 29; private KeyboardShortcuts() {} private static int getMetaState(KeyEvent event) { return (event.isCtrlPressed() ? CTRL : 0) | (event.isAltPressed() ? ALT : 0) | (event.isShiftPressed() ? SHIFT : 0); } private static boolean isGamepadAPIActive(ChromeActivity activity) { ContentViewCore cvc = activity.getCurrentContentViewCore(); return (cvc != null) ? cvc.isGamepadAPIActive() : false; } /** * This should be called from the Activity's dispatchKeyEvent() to handle keyboard shortcuts. * * Note: dispatchKeyEvent() is called before the active view or web page gets a chance to handle * the key event. So the keys handled here cannot be overridden by any view or web page. * * @param event The KeyEvent to handle. * @param activity The ChromeActivity in which the key was pressed. * @param uiInitialized Whether the UI has been initialized. If this is false, most keys will * not be handled. * @return True if the event was handled. False if the event was ignored. Null if the event * should be handled by the activity's parent class. */ @SuppressFBWarnings("NP_BOOLEAN_RETURN_NULL") public static Boolean dispatchKeyEvent(KeyEvent event, ChromeActivity activity, boolean uiInitialized) { int keyCode = event.getKeyCode(); if (!uiInitialized) { if (keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_MENU) return true; return null; } switch (keyCode) { case KeyEvent.KEYCODE_SEARCH: if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { activity.onMenuOrKeyboardAction(R.id.focus_url_bar, false); } // Always consume the SEARCH key events to prevent android from showing // the default app search UI, which locks up Chrome. return true; case KeyEvent.KEYCODE_MENU: if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { activity.onMenuOrKeyboardAction(R.id.show_menu, false); } return true; case KeyEvent.KEYCODE_ESCAPE: if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { if (activity.exitFullscreenIfShowing()) return true; } break; case KeyEvent.KEYCODE_TV: case KeyEvent.KEYCODE_GUIDE: case KeyEvent.KEYCODE_DVR: case KeyEvent.KEYCODE_AVR_INPUT: case KeyEvent.KEYCODE_AVR_POWER: case KeyEvent.KEYCODE_STB_INPUT: case KeyEvent.KEYCODE_STB_POWER: case KeyEvent.KEYCODE_TV_INPUT: case KeyEvent.KEYCODE_TV_POWER: case KeyEvent.KEYCODE_WINDOW: // Do not consume the AV device-related keys so that the system will take // an appropriate action, such as switching to TV mode. return false; } return null; } /** * This should be called from the Activity's onKeyDown() to handle keyboard shortcuts. * * Note: onKeyDown() is called after the active view or web page has had a chance to handle * the key event. So the keys handled here *can* be overridden by any view or web page. * * @param event The KeyEvent to handle. * @param activity The ChromeActivity in which the key was pressed. * @param isCurrentTabVisible Whether page-related actions are valid, e.g. reload, zoom in. * This should be false when in the tab switcher. * @param tabSwitchingEnabled Whether shortcuts that switch between tabs are enabled (e.g. * Ctrl+Tab, Ctrl+3). * @return Whether the key event was handled. */ public static boolean onKeyDown(KeyEvent event, ChromeActivity activity, boolean isCurrentTabVisible, boolean tabSwitchingEnabled) { int keyCode = event.getKeyCode(); if (event.getRepeatCount() != 0 || KeyEvent.isModifierKey(keyCode)) return false; if (KeyEvent.isGamepadButton(keyCode)) { if (isGamepadAPIActive(activity)) return false; } else if (!event.isCtrlPressed() && !event.isAltPressed() && keyCode != KeyEvent.KEYCODE_F3 && keyCode != KeyEvent.KEYCODE_F5 && keyCode != KeyEvent.KEYCODE_F10 && keyCode != KeyEvent.KEYCODE_FORWARD) { return false; } TabModel curModel = activity.getCurrentTabModel(); int count = curModel.getCount(); int metaState = getMetaState(event); int keyCodeAndMeta = keyCode | metaState; switch (keyCodeAndMeta) { case CTRL | SHIFT | KeyEvent.KEYCODE_T: activity.onMenuOrKeyboardAction(R.id.open_recently_closed_tab, false); return true; case CTRL | KeyEvent.KEYCODE_T: activity.onMenuOrKeyboardAction(curModel.isIncognito() ? R.id.new_incognito_tab_menu_id : R.id.new_tab_menu_id, false); return true; case CTRL | KeyEvent.KEYCODE_N: activity.onMenuOrKeyboardAction(R.id.new_tab_menu_id, false); return true; case CTRL | SHIFT | KeyEvent.KEYCODE_N: activity.onMenuOrKeyboardAction(R.id.new_incognito_tab_menu_id, false); return true; // Alt+E represents a special character ยด (latin code: ´) in Android. // If an EditText or ContentView has focus, Alt+E will be swallowed by // the default dispatchKeyEvent and cannot open the menu. case ALT | KeyEvent.KEYCODE_E: case ALT | KeyEvent.KEYCODE_F: case KeyEvent.KEYCODE_F10: case KeyEvent.KEYCODE_BUTTON_Y: activity.onMenuOrKeyboardAction(R.id.show_menu, false); return true; } if (isCurrentTabVisible) { if (tabSwitchingEnabled && (metaState == CTRL || metaState == ALT)) { int numCode = keyCode - KeyEvent.KEYCODE_0; if (numCode > 0 && numCode <= Math.min(count, 8)) { // Ctrl+1 to Ctrl+8: select tab by index TabModelUtils.setIndex(curModel, numCode - 1); return true; } else if (numCode == 9 && count != 0) { // Ctrl+9: select last tab TabModelUtils.setIndex(curModel, count - 1); return true; } } switch (keyCodeAndMeta) { case CTRL | KeyEvent.KEYCODE_TAB: case CTRL | KeyEvent.KEYCODE_PAGE_DOWN: case KeyEvent.KEYCODE_BUTTON_R1: if (tabSwitchingEnabled && count > 1) { TabModelUtils.setIndex(curModel, (curModel.index() + 1) % count); } return true; case CTRL | SHIFT | KeyEvent.KEYCODE_TAB: case CTRL | KeyEvent.KEYCODE_PAGE_UP: case KeyEvent.KEYCODE_BUTTON_L1: if (tabSwitchingEnabled && count > 1) { TabModelUtils.setIndex(curModel, (curModel.index() + count - 1) % count); } return true; case CTRL | KeyEvent.KEYCODE_W: case CTRL | KeyEvent.KEYCODE_F4: case KeyEvent.KEYCODE_BUTTON_B: TabModelUtils.closeCurrentTab(curModel); return true; case CTRL | KeyEvent.KEYCODE_F: case CTRL | KeyEvent.KEYCODE_G: case CTRL | SHIFT | KeyEvent.KEYCODE_G: case KeyEvent.KEYCODE_F3: case SHIFT | KeyEvent.KEYCODE_F3: activity.onMenuOrKeyboardAction(R.id.find_in_page_id, false); return true; case CTRL | KeyEvent.KEYCODE_L: case ALT | KeyEvent.KEYCODE_D: case KeyEvent.KEYCODE_BUTTON_X: activity.onMenuOrKeyboardAction(R.id.focus_url_bar, false); return true; case CTRL | SHIFT | KeyEvent.KEYCODE_B: activity.onMenuOrKeyboardAction(R.id.all_bookmarks_menu_id, false); return true; case KeyEvent.KEYCODE_BOOKMARK: case CTRL | KeyEvent.KEYCODE_D: activity.onMenuOrKeyboardAction(R.id.bookmark_this_page_id, false); return true; case CTRL | KeyEvent.KEYCODE_H: activity.onMenuOrKeyboardAction(R.id.open_history_menu_id, false); return true; case CTRL | KeyEvent.KEYCODE_P: activity.onMenuOrKeyboardAction(R.id.print_id, false); return true; case CTRL | KeyEvent.KEYCODE_PLUS: case CTRL | KeyEvent.KEYCODE_EQUALS: case CTRL | SHIFT | KeyEvent.KEYCODE_PLUS: case CTRL | SHIFT | KeyEvent.KEYCODE_EQUALS: case KeyEvent.KEYCODE_ZOOM_IN: ContentViewCore cvc = activity.getCurrentContentViewCore(); if (cvc != null) cvc.zoomIn(); return true; case CTRL | KeyEvent.KEYCODE_MINUS: case KeyEvent.KEYCODE_ZOOM_OUT: cvc = activity.getCurrentContentViewCore(); if (cvc != null) cvc.zoomOut(); return true; case CTRL | KeyEvent.KEYCODE_0: cvc = activity.getCurrentContentViewCore(); if (cvc != null) cvc.zoomReset(); return true; case SHIFT | CTRL | KeyEvent.KEYCODE_R: case CTRL | KeyEvent.KEYCODE_R: case SHIFT | KeyEvent.KEYCODE_F5: case KeyEvent.KEYCODE_F5: Tab tab = activity.getActivityTab(); if (tab != null) { if ((keyCodeAndMeta & SHIFT) == SHIFT) { tab.reloadIgnoringCache(); } else { tab.reload(); } if (activity.getToolbarManager() != null && tab.getWebContents() != null && tab.getWebContents().focusLocationBarByDefault()) { activity.getToolbarManager().revertLocationBarChanges(); } else { tab.requestFocus(); } } return true; case ALT | KeyEvent.KEYCODE_DPAD_LEFT: tab = activity.getActivityTab(); if (tab != null && tab.canGoBack()) tab.goBack(); return true; case ALT | KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_FORWARD: case KeyEvent.KEYCODE_BUTTON_START: tab = activity.getActivityTab(); if (tab != null && tab.canGoForward()) tab.goForward(); return true; case CTRL | SHIFT | KeyEvent.KEYCODE_SLASH: // i.e. Ctrl+? activity.onMenuOrKeyboardAction(R.id.help_id, false); return true; } } return false; } }