399 lines
16 KiB
Java
399 lines
16 KiB
Java
package org.mariotaku.twidere.util;
|
|
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences.Editor;
|
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.annotation.Nullable;
|
|
import android.support.v4.util.SparseArrayCompat;
|
|
import android.text.TextUtils;
|
|
import android.view.KeyEvent;
|
|
|
|
import org.mariotaku.twidere.R;
|
|
import org.mariotaku.twidere.activity.ComposeActivity;
|
|
import org.mariotaku.twidere.activity.QuickSearchBarActivity;
|
|
import org.mariotaku.twidere.constant.KeyboardShortcutConstants;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.Locale;
|
|
import java.util.Map.Entry;
|
|
|
|
import javax.inject.Singleton;
|
|
|
|
import static org.mariotaku.twidere.TwidereConstants.KEYBOARD_SHORTCUTS_PREFERENCES_NAME;
|
|
import static org.mariotaku.twidere.constant.IntentConstants.INTENT_ACTION_COMPOSE;
|
|
import static org.mariotaku.twidere.constant.IntentConstants.INTENT_ACTION_QUICK_SEARCH;
|
|
|
|
@Singleton
|
|
public class KeyboardShortcutsHandler implements KeyboardShortcutConstants {
|
|
|
|
public static final int MODIFIER_FLAG_CTRL = 0x00000001;
|
|
public static final int MODIFIER_FLAG_SHIFT = 0x00000002;
|
|
public static final int MODIFIER_FLAG_ALT = 0x00000004;
|
|
public static final int MODIFIER_FLAG_META = 0x00000008;
|
|
public static final int MODIFIER_FLAG_FN = 0x000000010;
|
|
|
|
private static final String KEYCODE_STRING_PREFIX = "KEYCODE_";
|
|
|
|
private static final HashMap<String, Integer> sActionLabelMap = new HashMap<>();
|
|
private static final SparseArrayCompat<String> sMetaNameMap = new SparseArrayCompat<>();
|
|
|
|
static {
|
|
sActionLabelMap.put(ACTION_COMPOSE, R.string.compose);
|
|
sActionLabelMap.put(ACTION_SEARCH, R.string.search);
|
|
sActionLabelMap.put(ACTION_MESSAGE, R.string.new_direct_message);
|
|
sActionLabelMap.put(ACTION_HOME_ACCOUNTS_DASHBOARD, R.string.open_accounts_dashboard);
|
|
sActionLabelMap.put(ACTION_STATUS_REPLY, R.string.action_reply);
|
|
sActionLabelMap.put(ACTION_STATUS_RETWEET, R.string.action_retweet);
|
|
sActionLabelMap.put(ACTION_STATUS_FAVORITE, R.string.action_like);
|
|
sActionLabelMap.put(ACTION_NAVIGATION_PREVIOUS, R.string.previous_item);
|
|
sActionLabelMap.put(ACTION_NAVIGATION_NEXT, R.string.next_item);
|
|
sActionLabelMap.put(ACTION_NAVIGATION_PAGE_DOWN, R.string.page_down);
|
|
sActionLabelMap.put(ACTION_NAVIGATION_PAGE_UP, R.string.page_up);
|
|
sActionLabelMap.put(ACTION_NAVIGATION_TOP, R.string.jump_to_top);
|
|
sActionLabelMap.put(ACTION_NAVIGATION_REFRESH, R.string.refresh);
|
|
sActionLabelMap.put(ACTION_NAVIGATION_PREVIOUS_TAB, R.string.previous_tab);
|
|
sActionLabelMap.put(ACTION_NAVIGATION_NEXT_TAB, R.string.next_tab);
|
|
sActionLabelMap.put(ACTION_NAVIGATION_BACK, R.string.keyboard_shortcut_back);
|
|
|
|
sMetaNameMap.put(KeyEvent.META_FUNCTION_ON, "fn");
|
|
sMetaNameMap.put(KeyEvent.META_META_ON, "meta");
|
|
sMetaNameMap.put(KeyEvent.META_CTRL_ON, "ctrl");
|
|
sMetaNameMap.put(KeyEvent.META_ALT_ON, "alt");
|
|
sMetaNameMap.put(KeyEvent.META_SHIFT_ON, "shift");
|
|
}
|
|
|
|
private final SharedPreferencesWrapper mPreferences;
|
|
|
|
public KeyboardShortcutsHandler(final Context context) {
|
|
mPreferences = SharedPreferencesWrapper.getInstance(context,
|
|
KEYBOARD_SHORTCUTS_PREFERENCES_NAME, Context.MODE_PRIVATE);
|
|
}
|
|
|
|
public String findAction(@NonNull KeyboardShortcutSpec spec) {
|
|
return mPreferences.getString(spec.getRawKey(), null);
|
|
}
|
|
|
|
public KeyboardShortcutSpec findKey(String action) {
|
|
for (Entry<String, ?> entry : mPreferences.getAll().entrySet()) {
|
|
if (action.equals(entry.getValue())) {
|
|
final KeyboardShortcutSpec spec = new KeyboardShortcutSpec(entry.getKey(), action);
|
|
if (spec.isValid()) return spec;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static String getActionLabel(Context context, String action) {
|
|
if (!sActionLabelMap.containsKey(action)) return null;
|
|
final int labelRes = sActionLabelMap.get(action);
|
|
return context.getString(labelRes);
|
|
}
|
|
|
|
@Nullable
|
|
public String getKeyAction(final String contextTag, final int keyCode, final KeyEvent event, int metaState) {
|
|
if (!isValidForHotkey(keyCode, event)) return null;
|
|
final String key = getKeyEventKey(contextTag, keyCode, event, metaState);
|
|
return mPreferences.getString(key, null);
|
|
}
|
|
|
|
public static String getKeyEventKey(String contextTag, int keyCode, KeyEvent event, int metaState) {
|
|
if (!isValidForHotkey(keyCode, event)) return null;
|
|
final StringBuilder keyNameBuilder = new StringBuilder();
|
|
if (!TextUtils.isEmpty(contextTag)) {
|
|
keyNameBuilder.append(contextTag);
|
|
keyNameBuilder.append(".");
|
|
}
|
|
final int normalizedMetaState = KeyEvent.normalizeMetaState(metaState | event.getMetaState());
|
|
|
|
for (int i = 0, j = sMetaNameMap.size(); i < j; i++) {
|
|
if ((sMetaNameMap.keyAt(i) & normalizedMetaState) != 0) {
|
|
keyNameBuilder.append(sMetaNameMap.valueAt(i));
|
|
keyNameBuilder.append("+");
|
|
}
|
|
}
|
|
final String keyCodeString = KeyEvent.keyCodeToString(keyCode);
|
|
if (keyCodeString.startsWith(KEYCODE_STRING_PREFIX)) {
|
|
keyNameBuilder.append(keyCodeString.substring(KEYCODE_STRING_PREFIX.length()).toLowerCase(Locale.US));
|
|
}
|
|
return keyNameBuilder.toString();
|
|
}
|
|
|
|
public static String getKeyEventKey(String contextTag, int metaState, String keyName) {
|
|
final StringBuilder keyNameBuilder = new StringBuilder();
|
|
if (!TextUtils.isEmpty(contextTag)) {
|
|
keyNameBuilder.append(contextTag);
|
|
keyNameBuilder.append(".");
|
|
}
|
|
|
|
for (int i = 0, j = sMetaNameMap.size(); i < j; i++) {
|
|
if ((sMetaNameMap.keyAt(i) & metaState) != 0) {
|
|
keyNameBuilder.append(sMetaNameMap.valueAt(i));
|
|
keyNameBuilder.append("+");
|
|
}
|
|
}
|
|
keyNameBuilder.append(keyName);
|
|
return keyNameBuilder.toString();
|
|
}
|
|
|
|
public static int getKeyEventMeta(String name) {
|
|
for (int i = 0, j = sMetaNameMap.size(); i < j; i++) {
|
|
if (sMetaNameMap.valueAt(i).equalsIgnoreCase(name)) return sMetaNameMap.keyAt(i);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public static KeyboardShortcutSpec getKeyboardShortcutSpec(String contextTag, int keyCode, KeyEvent event, int metaState) {
|
|
if (!isValidForHotkey(keyCode, event)) return null;
|
|
int metaStateNormalized = 0;
|
|
for (int i = 0, j = sMetaNameMap.size(); i < j; i++) {
|
|
if ((sMetaNameMap.keyAt(i) & metaState) != 0) {
|
|
metaStateNormalized |= sMetaNameMap.keyAt(i);
|
|
}
|
|
}
|
|
final String keyCodeString = KeyEvent.keyCodeToString(keyCode);
|
|
if (keyCodeString.startsWith(KEYCODE_STRING_PREFIX)) {
|
|
final String keyName = keyCodeString.substring(KEYCODE_STRING_PREFIX.length()).toLowerCase(Locale.US);
|
|
return new KeyboardShortcutSpec(contextTag, metaStateNormalized, keyName, null);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public boolean handleKey(final Context context, final String contextTag, final int keyCode, final KeyEvent event, int metaState) {
|
|
final String action = getKeyAction(contextTag, keyCode, event, metaState);
|
|
if (action == null) return false;
|
|
switch (action) {
|
|
case ACTION_COMPOSE: {
|
|
context.startActivity(new Intent(context, ComposeActivity.class).setAction(INTENT_ACTION_COMPOSE));
|
|
return true;
|
|
}
|
|
case ACTION_SEARCH: {
|
|
context.startActivity(new Intent(context, QuickSearchBarActivity.class).setAction(INTENT_ACTION_QUICK_SEARCH));
|
|
return true;
|
|
}
|
|
case ACTION_MESSAGE: {
|
|
IntentUtils.openMessageConversation(context, null, null);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static boolean isValidForHotkey(int keyCode, KeyEvent event) {
|
|
// These keys must use with modifiers
|
|
switch (keyCode) {
|
|
case KeyEvent.KEYCODE_DPAD_CENTER:
|
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
|
case KeyEvent.KEYCODE_DPAD_UP:
|
|
case KeyEvent.KEYCODE_ENTER:
|
|
case KeyEvent.KEYCODE_NUMPAD_ENTER:
|
|
case KeyEvent.KEYCODE_TAB: {
|
|
if (event.hasNoModifiers()) return false;
|
|
break;
|
|
}
|
|
}
|
|
return !isNavigationKey(keyCode) && !KeyEvent.isModifierKey(keyCode) && keyCode != KeyEvent.KEYCODE_UNKNOWN;
|
|
}
|
|
|
|
private static boolean isNavigationKey(int keyCode) {
|
|
return keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_HOME
|
|
|| keyCode == KeyEvent.KEYCODE_MENU;
|
|
}
|
|
|
|
public static String metaToFriendlyString(int metaState) {
|
|
final StringBuilder keyNameBuilder = new StringBuilder();
|
|
for (int i = 0, j = sMetaNameMap.size(); i < j; i++) {
|
|
if ((sMetaNameMap.keyAt(i) & metaState) != 0) {
|
|
final String value = sMetaNameMap.valueAt(i);
|
|
keyNameBuilder.append(value.substring(0, 1).toUpperCase(Locale.US));
|
|
keyNameBuilder.append(value.substring(1));
|
|
keyNameBuilder.append("+");
|
|
}
|
|
}
|
|
return keyNameBuilder.toString();
|
|
}
|
|
|
|
public void register(KeyboardShortcutSpec spec, String action) {
|
|
unregister(action);
|
|
mPreferences.edit().putString(spec.getRawKey(), action).apply();
|
|
}
|
|
|
|
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
|
mPreferences.registerOnSharedPreferenceChangeListener(listener);
|
|
}
|
|
|
|
public void reset() {
|
|
final Editor editor = mPreferences.edit();
|
|
editor.clear();
|
|
editor.putString("n", ACTION_COMPOSE);
|
|
editor.putString("m", ACTION_MESSAGE);
|
|
editor.putString("slash", ACTION_SEARCH);
|
|
editor.putString("home.q", ACTION_HOME_ACCOUNTS_DASHBOARD);
|
|
editor.putString("navigation.period", ACTION_NAVIGATION_REFRESH);
|
|
editor.putString("navigation.j", ACTION_NAVIGATION_NEXT);
|
|
editor.putString("navigation.k", ACTION_NAVIGATION_PREVIOUS);
|
|
editor.putString("navigation.h", ACTION_NAVIGATION_PREVIOUS_TAB);
|
|
editor.putString("navigation.l", ACTION_NAVIGATION_NEXT_TAB);
|
|
editor.putString("navigation.u", ACTION_NAVIGATION_TOP);
|
|
editor.putString("status.f", ACTION_STATUS_FAVORITE);
|
|
editor.putString("status.r", ACTION_STATUS_REPLY);
|
|
editor.putString("status.t", ACTION_STATUS_RETWEET);
|
|
editor.apply();
|
|
}
|
|
|
|
public void unregister(String action) {
|
|
final Editor editor = mPreferences.edit();
|
|
for (Entry<String, ?> entry : mPreferences.getAll().entrySet()) {
|
|
if (action.equals(entry.getValue())) {
|
|
final KeyboardShortcutSpec spec = new KeyboardShortcutSpec(entry.getKey(), action);
|
|
if (spec.isValid()) {
|
|
editor.remove(spec.getRawKey());
|
|
}
|
|
}
|
|
}
|
|
editor.apply();
|
|
}
|
|
|
|
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
|
mPreferences.unregisterOnSharedPreferenceChangeListener(listener);
|
|
}
|
|
|
|
public static int getMetaStateForKeyCode(int keyCode) {
|
|
switch (keyCode) {
|
|
case KeyEvent.KEYCODE_CTRL_LEFT:
|
|
return KeyEvent.META_CTRL_LEFT_ON;
|
|
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
|
return KeyEvent.META_CTRL_RIGHT_ON;
|
|
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
|
return KeyEvent.META_SHIFT_LEFT_ON;
|
|
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
|
return KeyEvent.META_SHIFT_RIGHT_ON;
|
|
case KeyEvent.KEYCODE_ALT_LEFT:
|
|
return KeyEvent.META_ALT_LEFT_ON;
|
|
case KeyEvent.KEYCODE_ALT_RIGHT:
|
|
return KeyEvent.META_ALT_RIGHT_ON;
|
|
case KeyEvent.KEYCODE_META_LEFT:
|
|
return KeyEvent.META_META_LEFT_ON;
|
|
case KeyEvent.KEYCODE_META_RIGHT:
|
|
return KeyEvent.META_META_RIGHT_ON;
|
|
case KeyEvent.KEYCODE_FUNCTION:
|
|
return KeyEvent.META_FUNCTION_ON;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public interface KeyboardShortcutCallback extends KeyboardShortcutConstants {
|
|
|
|
boolean handleKeyboardShortcutRepeat(@NonNull KeyboardShortcutsHandler handler, int keyCode,
|
|
int repeatCount, @NonNull KeyEvent event, int metaState);
|
|
|
|
boolean handleKeyboardShortcutSingle(@NonNull KeyboardShortcutsHandler handler, int keyCode,
|
|
@NonNull KeyEvent event, int metaState);
|
|
|
|
boolean isKeyboardShortcutHandled(@NonNull KeyboardShortcutsHandler handler, int keyCode,
|
|
@NonNull KeyEvent event, int metaState);
|
|
}
|
|
|
|
public interface TakeAllKeyboardShortcut {
|
|
|
|
}
|
|
|
|
/**
|
|
* Created by mariotaku on 15/4/11.
|
|
*/
|
|
public static final class KeyboardShortcutSpec {
|
|
|
|
private String action;
|
|
private String contextTag;
|
|
private int keyMeta;
|
|
private String keyName;
|
|
|
|
public KeyboardShortcutSpec(String contextTag, int keyMeta, String keyName, String action) {
|
|
this.contextTag = contextTag;
|
|
this.keyMeta = keyMeta;
|
|
this.keyName = keyName;
|
|
this.action = action;
|
|
}
|
|
|
|
public KeyboardShortcutSpec(String key, String action) {
|
|
final int contextDotIdx = key.indexOf('.');
|
|
if (contextDotIdx != -1) {
|
|
contextTag = key.substring(0, contextDotIdx);
|
|
}
|
|
int idx = contextDotIdx, previousIdx = idx;
|
|
while ((idx = key.indexOf('+', idx + 1)) != -1) {
|
|
keyMeta |= getKeyEventMeta(key.substring(previousIdx + 1, idx));
|
|
previousIdx = idx;
|
|
}
|
|
keyName = key.substring(previousIdx + 1);
|
|
this.action = action;
|
|
}
|
|
|
|
public KeyboardShortcutSpec copy() {
|
|
return new KeyboardShortcutSpec(contextTag, keyMeta, keyName, action);
|
|
}
|
|
|
|
public String getAction() {
|
|
return action;
|
|
}
|
|
|
|
public String getContextTag() {
|
|
return contextTag;
|
|
}
|
|
|
|
public void setContextTag(String contextTag) {
|
|
this.contextTag = contextTag;
|
|
}
|
|
|
|
public int getKeyMeta() {
|
|
return keyMeta;
|
|
}
|
|
|
|
public String getKeyName() {
|
|
return keyName;
|
|
}
|
|
|
|
public String getRawKey() {
|
|
return getKeyEventKey(contextTag, keyMeta, keyName);
|
|
}
|
|
|
|
public String getValueName(Context context) {
|
|
return getActionLabel(context, action);
|
|
}
|
|
|
|
public boolean isValid() {
|
|
return keyName != null;
|
|
}
|
|
|
|
public String toKeyString() {
|
|
return metaToFriendlyString(keyMeta) + keyToFriendlyString(keyName);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "KeyboardShortcutSpec{" +
|
|
"action='" + action + '\'' +
|
|
", contextTag='" + contextTag + '\'' +
|
|
", keyMeta=" + keyMeta +
|
|
", keyName='" + keyName + '\'' +
|
|
'}';
|
|
}
|
|
|
|
private static String keyToFriendlyString(String keyName) {
|
|
if (keyName == null) return null;
|
|
final String upperName = keyName.toUpperCase(Locale.US);
|
|
final int keyCode = KeyEvent.keyCodeFromString(KEYCODE_STRING_PREFIX + upperName);
|
|
if (keyCode == KeyEvent.KEYCODE_UNKNOWN) return upperName;
|
|
if (keyCode == KeyEvent.KEYCODE_DEL) return "Backspace";
|
|
if (keyCode == KeyEvent.KEYCODE_FORWARD_DEL) return "Delete";
|
|
if (keyCode == KeyEvent.KEYCODE_SPACE) return "Space";
|
|
final char displayLabel = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode).getDisplayLabel();
|
|
if (displayLabel == 0) return keyName.toUpperCase(Locale.US);
|
|
return String.valueOf(displayLabel);
|
|
}
|
|
}
|
|
}
|