1/*
2 * Copyright (C) 2008 The Android Open Source Project
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 */
16
17package android.widget;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.graphics.PixelFormat;
24import android.graphics.Rect;
25import android.os.Handler;
26import android.os.Message;
27import android.util.Log;
28import android.view.Gravity;
29import android.view.KeyEvent;
30import android.view.LayoutInflater;
31import android.view.MotionEvent;
32import android.view.View;
33import android.view.ViewConfiguration;
34import android.view.ViewGroup;
35import android.view.ViewRootImpl;
36import android.view.WindowManager;
37import android.view.View.OnClickListener;
38import android.view.WindowManager.LayoutParams;
39
40/*
41 * Implementation notes:
42 * - The zoom controls are displayed in their own window.
43 *   (Easier for the client and better performance)
44 * - This window is never touchable, and by default is not focusable.
45 *   Its rect is quite big (fills horizontally) but has empty space between the
46 *   edges and center.  Touches there should be given to the owner.  Instead of
47 *   having the window touchable and dispatching these empty touch events to the
48 *   owner, we set the window to not touchable and steal events from owner
49 *   via onTouchListener.
50 * - To make the buttons clickable, it attaches an OnTouchListener to the owner
51 *   view and does the hit detection locally (attaches when visible, detaches when invisible).
52 * - When it is focusable, it forwards uninteresting events to the owner view's
53 *   view hierarchy.
54 */
55/**
56 * The {@link ZoomButtonsController} handles showing and hiding the zoom
57 * controls and positioning it relative to an owner view. It also gives the
58 * client access to the zoom controls container, allowing for additional
59 * accessory buttons to be shown in the zoom controls window.
60 * <p>
61 * Typically, clients should call {@link #setVisible(boolean) setVisible(true)}
62 * on a touch down or move (no need to call {@link #setVisible(boolean)
63 * setVisible(false)} since it will time out on its own). Also, whenever the
64 * owner cannot be zoomed further, the client should update
65 * {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}.
66 * <p>
67 * If you are using this with a custom View, please call
68 * {@link #setVisible(boolean) setVisible(false)} from
69 * {@link View#onDetachedFromWindow} and from {@link View#onVisibilityChanged}
70 * when <code>visibility != View.VISIBLE</code>.
71 *
72 */
73public class ZoomButtonsController implements View.OnTouchListener {
74
75    private static final String TAG = "ZoomButtonsController";
76
77    private static final int ZOOM_CONTROLS_TIMEOUT =
78            (int) ViewConfiguration.getZoomControlsTimeout();
79
80    private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20;
81    private int mTouchPaddingScaledSq;
82
83    private final Context mContext;
84    private final WindowManager mWindowManager;
85    private boolean mAutoDismissControls = true;
86
87    /**
88     * The view that is being zoomed by this zoom controller.
89     */
90    private final View mOwnerView;
91
92    /**
93     * The location of the owner view on the screen. This is recalculated
94     * each time the zoom controller is shown.
95     */
96    private final int[] mOwnerViewRawLocation = new int[2];
97
98    /**
99     * The container that is added as a window.
100     */
101    private final FrameLayout mContainer;
102    private LayoutParams mContainerLayoutParams;
103    private final int[] mContainerRawLocation = new int[2];
104
105    private ZoomControls mControls;
106
107    /**
108     * The view (or null) that should receive touch events. This will get set if
109     * the touch down hits the container. It will be reset on the touch up.
110     */
111    private View mTouchTargetView;
112    /**
113     * The {@link #mTouchTargetView}'s location in window, set on touch down.
114     */
115    private final int[] mTouchTargetWindowLocation = new int[2];
116
117    /**
118     * If the zoom controller is dismissed but the user is still in a touch
119     * interaction, we set this to true. This will ignore all touch events until
120     * up/cancel, and then set the owner's touch listener to null.
121     * <p>
122     * Otherwise, the owner view would get mismatched events (i.e., touch move
123     * even though it never got the touch down.)
124     */
125    private boolean mReleaseTouchListenerOnUp;
126
127    /** Whether the container has been added to the window manager. */
128    private boolean mIsVisible;
129
130    private final Rect mTempRect = new Rect();
131    private final int[] mTempIntArray = new int[2];
132
133    private OnZoomListener mCallback;
134
135    /**
136     * When showing the zoom, we add the view as a new window. However, there is
137     * logic that needs to know the size of the zoom which is determined after
138     * it's laid out. Therefore, we must post this logic onto the UI thread so
139     * it will be exceuted AFTER the layout. This is the logic.
140     */
141    private Runnable mPostedVisibleInitializer;
142
143    private final IntentFilter mConfigurationChangedFilter =
144            new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
145
146    /**
147     * Needed to reposition the zoom controls after configuration changes.
148     */
149    private final BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() {
150        @Override
151        public void onReceive(Context context, Intent intent) {
152            if (!mIsVisible) return;
153
154            mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED);
155            mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED);
156        }
157    };
158
159    /** When configuration changes, this is called after the UI thread is idle. */
160    private static final int MSG_POST_CONFIGURATION_CHANGED = 2;
161    /** Used to delay the zoom controller dismissal. */
162    private static final int MSG_DISMISS_ZOOM_CONTROLS = 3;
163    /**
164     * If setVisible(true) is called and the owner view's window token is null,
165     * we delay the setVisible(true) call until it is not null.
166     */
167    private static final int MSG_POST_SET_VISIBLE = 4;
168
169    private final Handler mHandler = new Handler() {
170        @Override
171        public void handleMessage(Message msg) {
172            switch (msg.what) {
173                case MSG_POST_CONFIGURATION_CHANGED:
174                    onPostConfigurationChanged();
175                    break;
176
177                case MSG_DISMISS_ZOOM_CONTROLS:
178                    setVisible(false);
179                    break;
180
181                case MSG_POST_SET_VISIBLE:
182                    if (mOwnerView.getWindowToken() == null) {
183                        // Doh, it is still null, just ignore the set visible call
184                        Log.e(TAG,
185                                "Cannot make the zoom controller visible if the owner view is " +
186                                "not attached to a window.");
187                    } else {
188                        setVisible(true);
189                    }
190                    break;
191            }
192
193        }
194    };
195
196    /**
197     * Constructor for the {@link ZoomButtonsController}.
198     *
199     * @param ownerView The view that is being zoomed by the zoom controls. The
200     *            zoom controls will be displayed aligned with this view.
201     */
202    public ZoomButtonsController(View ownerView) {
203        mContext = ownerView.getContext();
204        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
205        mOwnerView = ownerView;
206
207        mTouchPaddingScaledSq = (int)
208                (ZOOM_CONTROLS_TOUCH_PADDING * mContext.getResources().getDisplayMetrics().density);
209        mTouchPaddingScaledSq *= mTouchPaddingScaledSq;
210
211        mContainer = createContainer();
212    }
213
214    /**
215     * Whether to enable the zoom in control.
216     *
217     * @param enabled Whether to enable the zoom in control.
218     */
219    public void setZoomInEnabled(boolean enabled) {
220        mControls.setIsZoomInEnabled(enabled);
221    }
222
223    /**
224     * Whether to enable the zoom out control.
225     *
226     * @param enabled Whether to enable the zoom out control.
227     */
228    public void setZoomOutEnabled(boolean enabled) {
229        mControls.setIsZoomOutEnabled(enabled);
230    }
231
232    /**
233     * Sets the delay between zoom callbacks as the user holds a zoom button.
234     *
235     * @param speed The delay in milliseconds between zoom callbacks.
236     */
237    public void setZoomSpeed(long speed) {
238        mControls.setZoomSpeed(speed);
239    }
240
241    private FrameLayout createContainer() {
242        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
243        // Controls are positioned BOTTOM | CENTER with respect to the owner view.
244        lp.gravity = Gravity.TOP | Gravity.START;
245        lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE |
246                LayoutParams.FLAG_NOT_FOCUSABLE |
247                LayoutParams.FLAG_LAYOUT_NO_LIMITS |
248                LayoutParams.FLAG_ALT_FOCUSABLE_IM;
249        lp.height = LayoutParams.WRAP_CONTENT;
250        lp.width = LayoutParams.MATCH_PARENT;
251        lp.type = LayoutParams.TYPE_APPLICATION_PANEL;
252        lp.format = PixelFormat.TRANSLUCENT;
253        lp.windowAnimations = com.android.internal.R.style.Animation_ZoomButtons;
254        mContainerLayoutParams = lp;
255
256        FrameLayout container = new Container(mContext);
257        container.setLayoutParams(lp);
258        container.setMeasureAllChildren(true);
259
260        LayoutInflater inflater = (LayoutInflater) mContext
261                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
262        inflater.inflate(com.android.internal.R.layout.zoom_container, container);
263
264        mControls = (ZoomControls) container.findViewById(com.android.internal.R.id.zoomControls);
265        mControls.setOnZoomInClickListener(new OnClickListener() {
266            public void onClick(View v) {
267                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
268                if (mCallback != null) mCallback.onZoom(true);
269            }
270        });
271        mControls.setOnZoomOutClickListener(new OnClickListener() {
272            public void onClick(View v) {
273                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
274                if (mCallback != null) mCallback.onZoom(false);
275            }
276        });
277
278        return container;
279    }
280
281    /**
282     * Sets the {@link OnZoomListener} listener that receives callbacks to zoom.
283     *
284     * @param listener The listener that will be told to zoom.
285     */
286    public void setOnZoomListener(OnZoomListener listener) {
287        mCallback = listener;
288    }
289
290    /**
291     * Sets whether the zoom controls should be focusable. If the controls are
292     * focusable, then trackball and arrow key interactions are possible.
293     * Otherwise, only touch interactions are possible.
294     *
295     * @param focusable Whether the zoom controls should be focusable.
296     */
297    public void setFocusable(boolean focusable) {
298        int oldFlags = mContainerLayoutParams.flags;
299        if (focusable) {
300            mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
301        } else {
302            mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
303        }
304
305        if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) {
306            mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
307        }
308    }
309
310    /**
311     * Whether the zoom controls will be automatically dismissed after showing.
312     *
313     * @return Whether the zoom controls will be auto dismissed after showing.
314     */
315    public boolean isAutoDismissed() {
316        return mAutoDismissControls;
317    }
318
319    /**
320     * Sets whether the zoom controls will be automatically dismissed after
321     * showing.
322     */
323    public void setAutoDismissed(boolean autoDismiss) {
324        if (mAutoDismissControls == autoDismiss) return;
325        mAutoDismissControls = autoDismiss;
326    }
327
328    /**
329     * Whether the zoom controls are visible to the user.
330     *
331     * @return Whether the zoom controls are visible to the user.
332     */
333    public boolean isVisible() {
334        return mIsVisible;
335    }
336
337    /**
338     * Sets whether the zoom controls should be visible to the user.
339     *
340     * @param visible Whether the zoom controls should be visible to the user.
341     */
342    public void setVisible(boolean visible) {
343
344        if (visible) {
345            if (mOwnerView.getWindowToken() == null) {
346                /*
347                 * We need a window token to show ourselves, maybe the owner's
348                 * window hasn't been created yet but it will have been by the
349                 * time the looper is idle, so post the setVisible(true) call.
350                 */
351                if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) {
352                    mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE);
353                }
354                return;
355            }
356
357            dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
358        }
359
360        if (mIsVisible == visible) {
361            return;
362        }
363        mIsVisible = visible;
364
365        if (visible) {
366            if (mContainerLayoutParams.token == null) {
367                mContainerLayoutParams.token = mOwnerView.getWindowToken();
368            }
369
370            mWindowManager.addView(mContainer, mContainerLayoutParams);
371
372            if (mPostedVisibleInitializer == null) {
373                mPostedVisibleInitializer = new Runnable() {
374                    public void run() {
375                        refreshPositioningVariables();
376
377                        if (mCallback != null) {
378                            mCallback.onVisibilityChanged(true);
379                        }
380                    }
381                };
382            }
383
384            mHandler.post(mPostedVisibleInitializer);
385
386            // Handle configuration changes when visible
387            mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter);
388
389            // Steal touches events from the owner
390            mOwnerView.setOnTouchListener(this);
391            mReleaseTouchListenerOnUp = false;
392
393        } else {
394            // Don't want to steal any more touches
395            if (mTouchTargetView != null) {
396                // We are still stealing the touch events for this touch
397                // sequence, so release the touch listener later
398                mReleaseTouchListenerOnUp = true;
399            } else {
400                mOwnerView.setOnTouchListener(null);
401            }
402
403            // No longer care about configuration changes
404            mContext.unregisterReceiver(mConfigurationChangedReceiver);
405
406            mWindowManager.removeView(mContainer);
407            mHandler.removeCallbacks(mPostedVisibleInitializer);
408
409            if (mCallback != null) {
410                mCallback.onVisibilityChanged(false);
411            }
412        }
413
414    }
415
416    /**
417     * Gets the container that is the parent of the zoom controls.
418     * <p>
419     * The client can add other views to this container to link them with the
420     * zoom controls.
421     *
422     * @return The container of the zoom controls. It will be a layout that
423     *         respects the gravity of a child's layout parameters.
424     */
425    public ViewGroup getContainer() {
426        return mContainer;
427    }
428
429    /**
430     * Gets the view for the zoom controls.
431     *
432     * @return The zoom controls view.
433     */
434    public View getZoomControls() {
435        return mControls;
436    }
437
438    private void dismissControlsDelayed(int delay) {
439        if (mAutoDismissControls) {
440            mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS);
441            mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay);
442        }
443    }
444
445    private void refreshPositioningVariables() {
446        // if the mOwnerView is detached from window then skip.
447        if (mOwnerView.getWindowToken() == null) return;
448
449        // Position the zoom controls on the bottom of the owner view.
450        int ownerHeight = mOwnerView.getHeight();
451        int ownerWidth = mOwnerView.getWidth();
452        // The gap between the top of the owner and the top of the container
453        int containerOwnerYOffset = ownerHeight - mContainer.getHeight();
454
455        // Calculate the owner view's bounds
456        mOwnerView.getLocationOnScreen(mOwnerViewRawLocation);
457        mContainerRawLocation[0] = mOwnerViewRawLocation[0];
458        mContainerRawLocation[1] = mOwnerViewRawLocation[1] + containerOwnerYOffset;
459
460        int[] ownerViewWindowLoc = mTempIntArray;
461        mOwnerView.getLocationInWindow(ownerViewWindowLoc);
462
463        // lp.x and lp.y should be relative to the owner's window top-left
464        mContainerLayoutParams.x = ownerViewWindowLoc[0];
465        mContainerLayoutParams.width = ownerWidth;
466        mContainerLayoutParams.y = ownerViewWindowLoc[1] + containerOwnerYOffset;
467        if (mIsVisible) {
468            mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
469        }
470
471    }
472
473    /* This will only be called when the container has focus. */
474    private boolean onContainerKey(KeyEvent event) {
475        int keyCode = event.getKeyCode();
476        if (isInterestingKey(keyCode)) {
477
478            if (keyCode == KeyEvent.KEYCODE_BACK) {
479                if (event.getAction() == KeyEvent.ACTION_DOWN
480                        && event.getRepeatCount() == 0) {
481                    if (mOwnerView != null) {
482                        KeyEvent.DispatcherState ds = mOwnerView.getKeyDispatcherState();
483                        if (ds != null) {
484                            ds.startTracking(event, this);
485                        }
486                    }
487                    return true;
488                } else if (event.getAction() == KeyEvent.ACTION_UP
489                        && event.isTracking() && !event.isCanceled()) {
490                    setVisible(false);
491                    return true;
492                }
493
494            } else {
495                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
496            }
497
498            // Let the container handle the key
499            return false;
500
501        } else {
502
503            ViewRootImpl viewRoot = mOwnerView.getViewRootImpl();
504            if (viewRoot != null) {
505                viewRoot.dispatchInputEvent(event);
506            }
507
508            // We gave the key to the owner, don't let the container handle this key
509            return true;
510        }
511    }
512
513    private boolean isInterestingKey(int keyCode) {
514        switch (keyCode) {
515            case KeyEvent.KEYCODE_DPAD_CENTER:
516            case KeyEvent.KEYCODE_DPAD_UP:
517            case KeyEvent.KEYCODE_DPAD_DOWN:
518            case KeyEvent.KEYCODE_DPAD_LEFT:
519            case KeyEvent.KEYCODE_DPAD_RIGHT:
520            case KeyEvent.KEYCODE_ENTER:
521            case KeyEvent.KEYCODE_BACK:
522                return true;
523            default:
524                return false;
525        }
526    }
527
528    /**
529     * @hide The ZoomButtonsController implements the OnTouchListener, but this
530     *       does not need to be shown in its public API.
531     */
532    public boolean onTouch(View v, MotionEvent event) {
533        int action = event.getAction();
534
535        if (event.getPointerCount() > 1) {
536            // ZoomButtonsController doesn't handle mutitouch. Give up control.
537            return false;
538        }
539
540        if (mReleaseTouchListenerOnUp) {
541            // The controls were dismissed but we need to throw away all events until the up
542            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
543                mOwnerView.setOnTouchListener(null);
544                setTouchTargetView(null);
545                mReleaseTouchListenerOnUp = false;
546            }
547
548            // Eat this event
549            return true;
550        }
551
552        dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
553
554        View targetView = mTouchTargetView;
555
556        switch (action) {
557            case MotionEvent.ACTION_DOWN:
558                targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY());
559                setTouchTargetView(targetView);
560                break;
561
562            case MotionEvent.ACTION_UP:
563            case MotionEvent.ACTION_CANCEL:
564                setTouchTargetView(null);
565                break;
566        }
567
568        if (targetView != null) {
569            // The upperleft corner of the target view in raw coordinates
570            int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0];
571            int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1];
572
573            MotionEvent containerEvent = MotionEvent.obtain(event);
574            // Convert the motion event into the target view's coordinates (from
575            // owner view's coordinates)
576            containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX,
577                    mOwnerViewRawLocation[1] - targetViewRawY);
578            /* Disallow negative coordinates (which can occur due to
579             * ZOOM_CONTROLS_TOUCH_PADDING) */
580            // These are floats because we need to potentially offset away this exact amount
581            float containerX = containerEvent.getX();
582            float containerY = containerEvent.getY();
583            if (containerX < 0 && containerX > -ZOOM_CONTROLS_TOUCH_PADDING) {
584                containerEvent.offsetLocation(-containerX, 0);
585            }
586            if (containerY < 0 && containerY > -ZOOM_CONTROLS_TOUCH_PADDING) {
587                containerEvent.offsetLocation(0, -containerY);
588            }
589            boolean retValue = targetView.dispatchTouchEvent(containerEvent);
590            containerEvent.recycle();
591            return retValue;
592
593        } else {
594            return false;
595        }
596    }
597
598    private void setTouchTargetView(View view) {
599        mTouchTargetView = view;
600        if (view != null) {
601            view.getLocationInWindow(mTouchTargetWindowLocation);
602        }
603    }
604
605    /**
606     * Returns the View that should receive a touch at the given coordinates.
607     *
608     * @param rawX The raw X.
609     * @param rawY The raw Y.
610     * @return The view that should receive the touches, or null if there is not one.
611     */
612    private View findViewForTouch(int rawX, int rawY) {
613        // Reverse order so the child drawn on top gets first dibs.
614        int containerCoordsX = rawX - mContainerRawLocation[0];
615        int containerCoordsY = rawY - mContainerRawLocation[1];
616        Rect frame = mTempRect;
617
618        View closestChild = null;
619        int closestChildDistanceSq = Integer.MAX_VALUE;
620
621        for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
622            View child = mContainer.getChildAt(i);
623            if (child.getVisibility() != View.VISIBLE) {
624                continue;
625            }
626
627            child.getHitRect(frame);
628            if (frame.contains(containerCoordsX, containerCoordsY)) {
629                return child;
630            }
631
632            int distanceX;
633            if (containerCoordsX >= frame.left && containerCoordsX <= frame.right) {
634                distanceX = 0;
635            } else {
636                distanceX = Math.min(Math.abs(frame.left - containerCoordsX),
637                    Math.abs(containerCoordsX - frame.right));
638            }
639            int distanceY;
640            if (containerCoordsY >= frame.top && containerCoordsY <= frame.bottom) {
641                distanceY = 0;
642            } else {
643                distanceY = Math.min(Math.abs(frame.top - containerCoordsY),
644                        Math.abs(containerCoordsY - frame.bottom));
645            }
646            int distanceSq = distanceX * distanceX + distanceY * distanceY;
647
648            if ((distanceSq < mTouchPaddingScaledSq) &&
649                    (distanceSq < closestChildDistanceSq)) {
650                closestChild = child;
651                closestChildDistanceSq = distanceSq;
652            }
653        }
654
655        return closestChild;
656    }
657
658    private void onPostConfigurationChanged() {
659        dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
660        refreshPositioningVariables();
661    }
662
663    /**
664     * Interface that will be called when the user performs an interaction that
665     * triggers some action, for example zooming.
666     */
667    public interface OnZoomListener {
668
669        /**
670         * Called when the zoom controls' visibility changes.
671         *
672         * @param visible Whether the zoom controls are visible.
673         */
674        void onVisibilityChanged(boolean visible);
675
676        /**
677         * Called when the owner view needs to be zoomed.
678         *
679         * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out.
680         */
681        void onZoom(boolean zoomIn);
682    }
683
684    private class Container extends FrameLayout {
685        public Container(Context context) {
686            super(context);
687        }
688
689        /*
690         * Need to override this to intercept the key events. Otherwise, we
691         * would attach a key listener to the container but its superclass
692         * ViewGroup gives it to the focused View instead of calling the key
693         * listener, and so we wouldn't get the events.
694         */
695        @Override
696        public boolean dispatchKeyEvent(KeyEvent event) {
697            return onContainerKey(event) ? true : super.dispatchKeyEvent(event);
698        }
699    }
700
701}
702