Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko.toolbar;
8 import java.util.ArrayList;
9 import java.util.Arrays;
10 import java.util.EnumSet;
11 import java.util.List;
13 import org.json.JSONObject;
14 import org.mozilla.gecko.BrowserApp;
15 import org.mozilla.gecko.GeckoAppShell;
16 import org.mozilla.gecko.GeckoApplication;
17 import org.mozilla.gecko.GeckoProfile;
18 import org.mozilla.gecko.LightweightTheme;
19 import org.mozilla.gecko.R;
20 import org.mozilla.gecko.Tab;
21 import org.mozilla.gecko.Tabs;
22 import org.mozilla.gecko.Telemetry;
23 import org.mozilla.gecko.TelemetryContract;
24 import org.mozilla.gecko.animation.PropertyAnimator;
25 import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
26 import org.mozilla.gecko.animation.ViewHelper;
27 import org.mozilla.gecko.menu.GeckoMenu;
28 import org.mozilla.gecko.menu.MenuPopup;
29 import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnStopListener;
30 import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnTitleChangeListener;
31 import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.UpdateFlags;
32 import org.mozilla.gecko.util.Clipboard;
33 import org.mozilla.gecko.util.GeckoEventListener;
34 import org.mozilla.gecko.util.HardwareUtils;
35 import org.mozilla.gecko.util.MenuUtils;
36 import org.mozilla.gecko.widget.ThemedImageButton;
37 import org.mozilla.gecko.widget.ThemedImageView;
38 import org.mozilla.gecko.widget.ThemedRelativeLayout;
40 import android.content.Context;
41 import android.content.res.Resources;
42 import android.graphics.Bitmap;
43 import android.graphics.drawable.BitmapDrawable;
44 import android.graphics.drawable.Drawable;
45 import android.graphics.drawable.StateListDrawable;
46 import android.os.Build;
47 import android.text.TextUtils;
48 import android.util.AttributeSet;
49 import android.util.Log;
50 import android.view.ContextMenu;
51 import android.view.KeyEvent;
52 import android.view.LayoutInflater;
53 import android.view.MenuInflater;
54 import android.view.MotionEvent;
55 import android.view.View;
56 import android.view.ViewGroup;
57 import android.view.animation.AccelerateInterpolator;
58 import android.view.animation.Interpolator;
59 import android.view.inputmethod.InputMethodManager;
60 import android.widget.Button;
61 import android.widget.ImageButton;
62 import android.widget.ImageView;
63 import android.widget.LinearLayout;
64 import android.widget.PopupWindow;
65 import android.widget.RelativeLayout;
67 /**
68 * {@code BrowserToolbar} is single entry point for users of the toolbar
69 * subsystem i.e. this should be the only import outside the 'toolbar'
70 * package.
71 *
72 * {@code BrowserToolbar} serves at the single event bus for all
73 * sub-components in the toolbar. It tracks tab events and gecko messages
74 * and update the state of its inner components accordingly.
75 *
76 * It has two states, display and edit, which are controlled by
77 * ToolbarEditLayout and ToolbarDisplayLayout. In display state, the toolbar
78 * displays the current state for the selected tab. In edit state, it shows
79 * a text entry for searching bookmarks/history. {@code BrowserToolbar}
80 * provides public API to enter, cancel, and commit the edit state as well
81 * as a set of listeners to allow {@code BrowserToolbar} users to react
82 * to state changes accordingly.
83 */
84 public class BrowserToolbar extends ThemedRelativeLayout
85 implements Tabs.OnTabsChangedListener,
86 GeckoMenu.ActionItemBarPresenter,
87 GeckoEventListener {
88 private static final String LOGTAG = "GeckoToolbar";
90 public interface OnActivateListener {
91 public void onActivate();
92 }
94 public interface OnCommitListener {
95 public void onCommit();
96 }
98 public interface OnDismissListener {
99 public void onDismiss();
100 }
102 public interface OnFilterListener {
103 public void onFilter(String searchText, AutocompleteHandler handler);
104 }
106 public interface OnStartEditingListener {
107 public void onStartEditing();
108 }
110 public interface OnStopEditingListener {
111 public void onStopEditing();
112 }
114 private enum UIMode {
115 EDIT,
116 DISPLAY
117 }
119 enum ForwardButtonAnimation {
120 SHOW,
121 HIDE
122 }
124 private ToolbarDisplayLayout urlDisplayLayout;
125 private ToolbarEditLayout urlEditLayout;
126 private View urlBarEntry;
127 private RelativeLayout.LayoutParams urlBarEntryDefaultLayoutParams;
128 private RelativeLayout.LayoutParams urlBarEntryShrunkenLayoutParams;
129 private ImageView urlBarTranslatingEdge;
130 private boolean isSwitchingTabs;
131 private ShapedButton tabsButton;
132 private ImageButton backButton;
133 private ImageButton forwardButton;
135 private ToolbarProgressView progressBar;
136 private TabCounter tabsCounter;
137 private ThemedImageButton menuButton;
138 private ThemedImageView menuIcon;
139 private LinearLayout actionItemBar;
140 private MenuPopup menuPopup;
141 private List<View> focusOrder;
143 private final ThemedImageView editCancel;
145 private boolean shouldShrinkURLBar = false;
147 private OnActivateListener activateListener;
148 private OnFocusChangeListener focusChangeListener;
149 private OnStartEditingListener startEditingListener;
150 private OnStopEditingListener stopEditingListener;
152 private final BrowserApp activity;
153 private boolean hasSoftMenuButton;
155 private UIMode uiMode;
156 private boolean isAnimatingEntry;
158 private int urlBarViewOffset;
159 private int defaultForwardMargin;
161 private static final Interpolator buttonsInterpolator = new AccelerateInterpolator();
163 private static final int FORWARD_ANIMATION_DURATION = 450;
165 private final LightweightTheme theme;
167 public BrowserToolbar(Context context) {
168 this(context, null);
169 }
171 public BrowserToolbar(Context context, AttributeSet attrs) {
172 super(context, attrs);
173 theme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
175 // BrowserToolbar is attached to BrowserApp only.
176 activity = (BrowserApp) context;
178 // Inflate the content.
179 LayoutInflater.from(context).inflate(R.layout.browser_toolbar, this);
181 Tabs.registerOnTabsChangedListener(this);
182 isSwitchingTabs = true;
183 isAnimatingEntry = false;
185 registerEventListener("Reader:Click");
186 registerEventListener("Reader:LongClick");
188 final Resources res = getResources();
189 urlBarViewOffset = res.getDimensionPixelSize(R.dimen.url_bar_offset_left);
190 defaultForwardMargin = res.getDimensionPixelSize(R.dimen.forward_default_offset);
191 urlDisplayLayout = (ToolbarDisplayLayout) findViewById(R.id.display_layout);
192 urlBarEntry = findViewById(R.id.url_bar_entry);
193 urlEditLayout = (ToolbarEditLayout) findViewById(R.id.edit_layout);
195 urlBarEntryDefaultLayoutParams = (RelativeLayout.LayoutParams) urlBarEntry.getLayoutParams();
196 // API level 19 adds a RelativeLayout.LayoutParams copy constructor, so we explicitly cast
197 // to ViewGroup.MarginLayoutParams to ensure consistency across platforms.
198 urlBarEntryShrunkenLayoutParams = new RelativeLayout.LayoutParams(
199 (ViewGroup.MarginLayoutParams) urlBarEntryDefaultLayoutParams);
200 urlBarEntryShrunkenLayoutParams.addRule(RelativeLayout.ALIGN_RIGHT, R.id.edit_layout);
201 urlBarEntryShrunkenLayoutParams.rightMargin = 0;
203 // This will clip the translating edge's image at 60% of its width
204 urlBarTranslatingEdge = (ImageView) findViewById(R.id.url_bar_translating_edge);
205 if (urlBarTranslatingEdge != null) {
206 urlBarTranslatingEdge.getDrawable().setLevel(6000);
207 }
209 tabsButton = (ShapedButton) findViewById(R.id.tabs);
210 tabsCounter = (TabCounter) findViewById(R.id.tabs_counter);
211 if (Build.VERSION.SDK_INT >= 11) {
212 tabsCounter.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
213 }
215 backButton = (ImageButton) findViewById(R.id.back);
216 setButtonEnabled(backButton, false);
217 forwardButton = (ImageButton) findViewById(R.id.forward);
218 setButtonEnabled(forwardButton, false);
220 menuButton = (ThemedImageButton) findViewById(R.id.menu);
221 menuIcon = (ThemedImageView) findViewById(R.id.menu_icon);
222 actionItemBar = (LinearLayout) findViewById(R.id.menu_items);
223 hasSoftMenuButton = !HardwareUtils.hasMenuButton();
225 editCancel = (ThemedImageView) findViewById(R.id.edit_cancel);
227 // We use different layouts on phones and tablets, so adjust the focus
228 // order appropriately.
229 focusOrder = new ArrayList<View>();
230 if (HardwareUtils.isTablet()) {
231 focusOrder.addAll(Arrays.asList(tabsButton, backButton, forwardButton, this));
232 focusOrder.addAll(urlDisplayLayout.getFocusOrder());
233 focusOrder.addAll(Arrays.asList(actionItemBar, menuButton));
234 } else {
235 focusOrder.add(this);
236 focusOrder.addAll(urlDisplayLayout.getFocusOrder());
237 focusOrder.addAll(Arrays.asList(tabsButton, menuButton));
238 }
240 setUIMode(UIMode.DISPLAY);
241 }
243 @Override
244 public void onAttachedToWindow() {
245 super.onAttachedToWindow();
247 setOnClickListener(new Button.OnClickListener() {
248 @Override
249 public void onClick(View v) {
250 if (activateListener != null) {
251 activateListener.onActivate();
252 }
253 }
254 });
256 setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
257 @Override
258 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
259 // We don't the context menu while editing
260 if (isEditing()) {
261 return;
262 }
264 // NOTE: Use MenuUtils.safeSetVisible because some actions might
265 // be on the Page menu
267 MenuInflater inflater = activity.getMenuInflater();
268 inflater.inflate(R.menu.titlebar_contextmenu, menu);
270 String clipboard = Clipboard.getText();
271 if (TextUtils.isEmpty(clipboard)) {
272 menu.findItem(R.id.pasteandgo).setVisible(false);
273 menu.findItem(R.id.paste).setVisible(false);
274 }
276 Tab tab = Tabs.getInstance().getSelectedTab();
277 if (tab != null) {
278 String url = tab.getURL();
279 if (url == null) {
280 menu.findItem(R.id.copyurl).setVisible(false);
281 menu.findItem(R.id.add_to_launcher).setVisible(false);
282 MenuUtils.safeSetVisible(menu, R.id.share, false);
283 }
285 MenuUtils.safeSetVisible(menu, R.id.subscribe, tab.hasFeeds());
286 MenuUtils.safeSetVisible(menu, R.id.add_search_engine, tab.hasOpenSearch());
287 } else {
288 // if there is no tab, remove anything tab dependent
289 menu.findItem(R.id.copyurl).setVisible(false);
290 menu.findItem(R.id.add_to_launcher).setVisible(false);
291 MenuUtils.safeSetVisible(menu, R.id.share, false);
292 MenuUtils.safeSetVisible(menu, R.id.subscribe, false);
293 MenuUtils.safeSetVisible(menu, R.id.add_search_engine, false);
294 }
296 MenuUtils.safeSetVisible(menu, R.id.share, !GeckoProfile.get(getContext()).inGuestMode());
297 }
298 });
300 urlDisplayLayout.setOnStopListener(new OnStopListener() {
301 @Override
302 public Tab onStop() {
303 final Tab tab = Tabs.getInstance().getSelectedTab();
304 if (tab != null) {
305 tab.doStop();
306 return tab;
307 }
309 return null;
310 }
311 });
313 urlDisplayLayout.setOnTitleChangeListener(new OnTitleChangeListener() {
314 @Override
315 public void onTitleChange(CharSequence title) {
316 final String contentDescription;
317 if (title != null) {
318 contentDescription = title.toString();
319 } else {
320 contentDescription = activity.getString(R.string.url_bar_default_text);
321 }
323 // The title and content description should
324 // always be sync.
325 setContentDescription(contentDescription);
326 }
327 });
329 urlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() {
330 @Override
331 public void onFocusChange(View v, boolean hasFocus) {
332 // This will select the url bar when entering editing mode.
333 setSelected(hasFocus);
334 if (focusChangeListener != null) {
335 focusChangeListener.onFocusChange(v, hasFocus);
336 }
337 }
338 });
340 tabsButton.setOnClickListener(new Button.OnClickListener() {
341 @Override
342 public void onClick(View v) {
343 toggleTabs();
344 }
345 });
346 tabsButton.setImageLevel(0);
348 backButton.setOnClickListener(new Button.OnClickListener() {
349 @Override
350 public void onClick(View view) {
351 Tabs.getInstance().getSelectedTab().doBack();
352 }
353 });
354 backButton.setOnLongClickListener(new Button.OnLongClickListener() {
355 @Override
356 public boolean onLongClick(View view) {
357 return Tabs.getInstance().getSelectedTab().showBackHistory();
358 }
359 });
361 forwardButton.setOnClickListener(new Button.OnClickListener() {
362 @Override
363 public void onClick(View view) {
364 Tabs.getInstance().getSelectedTab().doForward();
365 }
366 });
367 forwardButton.setOnLongClickListener(new Button.OnLongClickListener() {
368 @Override
369 public boolean onLongClick(View view) {
370 return Tabs.getInstance().getSelectedTab().showForwardHistory();
371 }
372 });
374 if (editCancel != null) {
375 editCancel.setOnClickListener(new OnClickListener() {
376 @Override
377 public void onClick(View v) {
378 // If we exit editing mode during the animation,
379 // we're put into an inconsistent state (bug 1017276).
380 if (!isAnimatingEntry) {
381 Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL,
382 TelemetryContract.Method.ACTIONBAR,
383 getResources().getResourceEntryName(editCancel.getId()));
384 cancelEdit();
385 }
386 }
387 });
388 }
390 if (hasSoftMenuButton) {
391 menuButton.setVisibility(View.VISIBLE);
392 menuIcon.setVisibility(View.VISIBLE);
394 menuButton.setOnClickListener(new Button.OnClickListener() {
395 @Override
396 public void onClick(View view) {
397 activity.openOptionsMenu();
398 }
399 });
400 }
401 }
403 public void setProgressBar(ToolbarProgressView progressBar) {
404 this.progressBar = progressBar;
405 }
407 public void refresh() {
408 urlDisplayLayout.dismissSiteIdentityPopup();
409 }
411 public boolean onBackPressed() {
412 // If we exit editing mode during the animation,
413 // we're put into an inconsistent state (bug 1017276).
414 if (isEditing() && !isAnimatingEntry) {
415 Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL,
416 TelemetryContract.Method.BACK);
417 cancelEdit();
418 return true;
419 }
421 return urlDisplayLayout.dismissSiteIdentityPopup();
422 }
424 @Override
425 public boolean onTouchEvent(MotionEvent event) {
426 // If the motion event has occured below the toolbar (due to the scroll
427 // offset), let it pass through to the page.
428 if (event != null && event.getY() > getHeight() + ViewHelper.getTranslationY(this)) {
429 return false;
430 }
432 return super.onTouchEvent(event);
433 }
435 @Override
436 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
437 super.onSizeChanged(w, h, oldw, oldh);
439 if (h != oldh) {
440 // Post this to happen outside of onSizeChanged, as this may cause
441 // a layout change and relayouts within a layout change don't work.
442 post(new Runnable() {
443 @Override
444 public void run() {
445 activity.refreshToolbarHeight();
446 }
447 });
448 }
449 }
451 @Override
452 public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
453 Log.d(LOGTAG, "onTabChanged: " + msg);
454 final Tabs tabs = Tabs.getInstance();
456 // These conditions are split into three phases:
457 // * Always do first
458 // * Handling specific to the selected tab
459 // * Always do afterwards.
461 switch (msg) {
462 case ADDED:
463 case CLOSED:
464 updateTabCount(tabs.getDisplayCount());
465 break;
466 case RESTORED:
467 // TabCount fixup after OOM
468 case SELECTED:
469 urlDisplayLayout.dismissSiteIdentityPopup();
470 updateTabCount(tabs.getDisplayCount());
471 isSwitchingTabs = true;
472 break;
473 }
475 if (tabs.isSelectedTab(tab)) {
476 final EnumSet<UpdateFlags> flags = EnumSet.noneOf(UpdateFlags.class);
478 // Progress-related handling
479 switch (msg) {
480 case START:
481 updateProgressVisibility(tab, Tab.LOAD_PROGRESS_INIT);
482 // Fall through.
483 case ADDED:
484 case LOCATION_CHANGE:
485 case LOAD_ERROR:
486 case LOADED:
487 case STOP:
488 flags.add(UpdateFlags.PROGRESS);
489 if (progressBar.getVisibility() == View.VISIBLE) {
490 progressBar.animateProgress(tab.getLoadProgress());
491 }
492 break;
494 case SELECTED:
495 flags.add(UpdateFlags.PROGRESS);
496 updateProgressVisibility();
497 break;
498 }
500 switch (msg) {
501 case STOP:
502 // Reset the title in case we haven't navigated
503 // to a new page yet.
504 flags.add(UpdateFlags.TITLE);
505 // Fall through.
506 case START:
507 case CLOSED:
508 case ADDED:
509 updateBackButton(tab);
510 updateForwardButton(tab);
511 break;
513 case SELECTED:
514 flags.add(UpdateFlags.PRIVATE_MODE);
515 setPrivateMode(tab.isPrivate());
516 // Fall through.
517 case LOAD_ERROR:
518 flags.add(UpdateFlags.TITLE);
519 // Fall through.
520 case LOCATION_CHANGE:
521 // A successful location change will cause Tab to notify
522 // us of a title change, so we don't update the title here.
523 flags.add(UpdateFlags.FAVICON);
524 flags.add(UpdateFlags.SITE_IDENTITY);
526 updateBackButton(tab);
527 updateForwardButton(tab);
528 break;
530 case TITLE:
531 flags.add(UpdateFlags.TITLE);
532 break;
534 case FAVICON:
535 flags.add(UpdateFlags.FAVICON);
536 break;
538 case SECURITY_CHANGE:
539 flags.add(UpdateFlags.SITE_IDENTITY);
540 break;
541 }
543 if (!flags.isEmpty()) {
544 updateDisplayLayout(tab, flags);
545 }
546 }
548 switch (msg) {
549 case SELECTED:
550 case LOAD_ERROR:
551 case LOCATION_CHANGE:
552 isSwitchingTabs = false;
553 }
554 }
556 private void updateProgressVisibility() {
557 final Tab selectedTab = Tabs.getInstance().getSelectedTab();
558 updateProgressVisibility(selectedTab, selectedTab.getLoadProgress());
559 }
561 private void updateProgressVisibility(Tab selectedTab, int progress) {
562 if (!isEditing() && selectedTab.getState() == Tab.STATE_LOADING) {
563 progressBar.setProgress(progress);
564 progressBar.setVisibility(View.VISIBLE);
565 } else {
566 progressBar.setVisibility(View.GONE);
567 }
568 }
570 private boolean isVisible() {
571 return ViewHelper.getTranslationY(this) == 0;
572 }
574 @Override
575 public void setNextFocusDownId(int nextId) {
576 super.setNextFocusDownId(nextId);
577 tabsButton.setNextFocusDownId(nextId);
578 backButton.setNextFocusDownId(nextId);
579 forwardButton.setNextFocusDownId(nextId);
580 urlDisplayLayout.setNextFocusDownId(nextId);
581 menuButton.setNextFocusDownId(nextId);
582 }
584 private int getUrlBarEntryTranslation() {
585 if (editCancel == null) {
586 // We are on tablet, and there is no animation so return a translation of 0.
587 return 0;
588 }
590 // Find the distance from the right-edge of the url bar (where we're translating from) to
591 // the left-edge of the cancel button (where we're translating to; note that the cancel
592 // button must be laid out, i.e. not View.GONE).
593 final LayoutParams lp = (LayoutParams) urlEditLayout.getLayoutParams();
594 return editCancel.getLeft() - lp.leftMargin - urlBarEntry.getRight();
595 }
597 private int getUrlBarCurveTranslation() {
598 return getWidth() - tabsButton.getLeft();
599 }
601 private boolean canDoBack(Tab tab) {
602 return (tab.canDoBack() && !isEditing());
603 }
605 private boolean canDoForward(Tab tab) {
606 return (tab.canDoForward() && !isEditing());
607 }
609 private void addTab() {
610 activity.addTab();
611 }
613 private void toggleTabs() {
614 if (activity.areTabsShown()) {
615 if (activity.hasTabsSideBar())
616 activity.hideTabs();
617 } else {
618 // hide the virtual keyboard
619 InputMethodManager imm =
620 (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
621 imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0);
623 Tab tab = Tabs.getInstance().getSelectedTab();
624 if (tab != null) {
625 if (!tab.isPrivate())
626 activity.showNormalTabs();
627 else
628 activity.showPrivateTabs();
629 }
630 }
631 }
633 private void updateTabCountAndAnimate(int count) {
634 // Don't animate if the toolbar is hidden.
635 if (!isVisible()) {
636 updateTabCount(count);
637 return;
638 }
640 // If toolbar is in edit mode on a phone, this means the entry is expanded
641 // and the tabs button is translated offscreen. Don't trigger tabs counter
642 // updates until the tabs button is back on screen.
643 // See stopEditing()
644 if (!isEditing() || HardwareUtils.isTablet()) {
645 tabsCounter.setCount(count);
647 tabsButton.setContentDescription((count > 1) ?
648 activity.getString(R.string.num_tabs, count) :
649 activity.getString(R.string.one_tab));
650 }
651 }
653 private void updateTabCount(int count) {
654 // If toolbar is in edit mode on a phone, this means the entry is expanded
655 // and the tabs button is translated offscreen. Don't trigger tabs counter
656 // updates until the tabs button is back on screen.
657 // See stopEditing()
658 if (isEditing() && !HardwareUtils.isTablet()) {
659 return;
660 }
662 // Set TabCounter based on visibility
663 if (isVisible() && ViewHelper.getAlpha(tabsCounter) != 0 && !isEditing()) {
664 tabsCounter.setCountWithAnimation(count);
665 } else {
666 tabsCounter.setCount(count);
667 }
669 // Update A11y information
670 tabsButton.setContentDescription((count > 1) ?
671 activity.getString(R.string.num_tabs, count) :
672 activity.getString(R.string.one_tab));
673 }
675 private void updateDisplayLayout(Tab tab, EnumSet<UpdateFlags> flags) {
676 if (isSwitchingTabs) {
677 flags.add(UpdateFlags.DISABLE_ANIMATIONS);
678 }
680 urlDisplayLayout.updateFromTab(tab, flags);
682 if (flags.contains(UpdateFlags.TITLE)) {
683 if (!isEditing()) {
684 urlEditLayout.setText(tab.getURL());
685 }
686 }
688 if (flags.contains(UpdateFlags.PROGRESS)) {
689 updateFocusOrder();
690 }
691 }
693 private void updateFocusOrder() {
694 View prevView = null;
696 // If the element that has focus becomes disabled or invisible, focus
697 // is given to the URL bar.
698 boolean needsNewFocus = false;
700 for (View view : focusOrder) {
701 if (view.getVisibility() != View.VISIBLE || !view.isEnabled()) {
702 if (view.hasFocus()) {
703 needsNewFocus = true;
704 }
705 continue;
706 }
708 if (view == actionItemBar) {
709 final int childCount = actionItemBar.getChildCount();
710 for (int child = 0; child < childCount; child++) {
711 View childView = actionItemBar.getChildAt(child);
712 if (prevView != null) {
713 childView.setNextFocusLeftId(prevView.getId());
714 prevView.setNextFocusRightId(childView.getId());
715 }
716 prevView = childView;
717 }
718 } else {
719 if (prevView != null) {
720 view.setNextFocusLeftId(prevView.getId());
721 prevView.setNextFocusRightId(view.getId());
722 }
723 prevView = view;
724 }
725 }
727 if (needsNewFocus) {
728 requestFocus();
729 }
730 }
732 public void onEditSuggestion(String suggestion) {
733 if (!isEditing()) {
734 return;
735 }
737 urlEditLayout.onEditSuggestion(suggestion);
738 }
740 public void setTitle(CharSequence title) {
741 urlDisplayLayout.setTitle(title);
742 }
744 public void prepareTabsAnimation(PropertyAnimator animator, boolean tabsAreShown) {
745 if (!tabsAreShown) {
746 PropertyAnimator buttonsAnimator =
747 new PropertyAnimator(animator.getDuration(), buttonsInterpolator);
749 buttonsAnimator.attach(tabsCounter,
750 PropertyAnimator.Property.ALPHA,
751 1.0f);
753 if (hasSoftMenuButton && !HardwareUtils.isTablet()) {
754 buttonsAnimator.attach(menuIcon,
755 PropertyAnimator.Property.ALPHA,
756 1.0f);
757 }
759 buttonsAnimator.start();
761 return;
762 }
764 ViewHelper.setAlpha(tabsCounter, 0.0f);
766 if (hasSoftMenuButton && !HardwareUtils.isTablet()) {
767 ViewHelper.setAlpha(menuIcon, 0.0f);
768 }
769 }
771 public void finishTabsAnimation(boolean tabsAreShown) {
772 if (tabsAreShown) {
773 return;
774 }
776 PropertyAnimator animator = new PropertyAnimator(150);
778 animator.attach(tabsCounter,
779 PropertyAnimator.Property.ALPHA,
780 1.0f);
782 if (hasSoftMenuButton && !HardwareUtils.isTablet()) {
783 animator.attach(menuIcon,
784 PropertyAnimator.Property.ALPHA,
785 1.0f);
786 }
788 animator.start();
789 }
791 public void setOnActivateListener(OnActivateListener listener) {
792 activateListener = listener;
793 }
795 public void setOnCommitListener(OnCommitListener listener) {
796 urlEditLayout.setOnCommitListener(listener);
797 }
799 public void setOnDismissListener(OnDismissListener listener) {
800 urlEditLayout.setOnDismissListener(listener);
801 }
803 public void setOnFilterListener(OnFilterListener listener) {
804 urlEditLayout.setOnFilterListener(listener);
805 }
807 public void setOnFocusChangeListener(OnFocusChangeListener listener) {
808 focusChangeListener = listener;
809 }
811 public void setOnStartEditingListener(OnStartEditingListener listener) {
812 startEditingListener = listener;
813 }
815 public void setOnStopEditingListener(OnStopEditingListener listener) {
816 stopEditingListener = listener;
817 }
819 private void showUrlEditLayout() {
820 setUrlEditLayoutVisibility(true, null);
821 }
823 private void showUrlEditLayout(PropertyAnimator animator) {
824 setUrlEditLayoutVisibility(true, animator);
825 }
827 private void hideUrlEditLayout() {
828 setUrlEditLayoutVisibility(false, null);
829 }
831 private void hideUrlEditLayout(PropertyAnimator animator) {
832 setUrlEditLayoutVisibility(false, animator);
833 }
835 private void setUrlEditLayoutVisibility(final boolean showEditLayout, PropertyAnimator animator) {
836 if (showEditLayout) {
837 urlEditLayout.prepareShowAnimation(animator);
838 }
840 if (animator == null) {
841 final View viewToShow = (showEditLayout ? urlEditLayout : urlDisplayLayout);
842 final View viewToHide = (showEditLayout ? urlDisplayLayout : urlEditLayout);
844 viewToHide.setVisibility(View.GONE);
845 viewToShow.setVisibility(View.VISIBLE);
847 final int cancelVisibility = (showEditLayout ? View.VISIBLE : View.INVISIBLE);
848 setCancelVisibility(cancelVisibility);
849 return;
850 }
852 animator.addPropertyAnimationListener(new PropertyAnimationListener() {
853 @Override
854 public void onPropertyAnimationStart() {
855 if (!showEditLayout) {
856 urlEditLayout.setVisibility(View.GONE);
857 urlDisplayLayout.setVisibility(View.VISIBLE);
859 setCancelVisibility(View.INVISIBLE);
860 }
861 }
863 @Override
864 public void onPropertyAnimationEnd() {
865 if (showEditLayout) {
866 urlDisplayLayout.setVisibility(View.GONE);
867 urlEditLayout.setVisibility(View.VISIBLE);
869 setCancelVisibility(View.VISIBLE);
870 }
871 }
872 });
873 }
875 private void setCancelVisibility(final int visibility) {
876 if (editCancel != null) {
877 editCancel.setVisibility(visibility);
878 }
879 }
881 /**
882 * Disables and dims all toolbar elements which are not
883 * related to editing mode.
884 */
885 private void updateChildrenForEditing() {
886 // This is for the tablet UI only
887 if (!HardwareUtils.isTablet()) {
888 return;
889 }
891 // Disable toolbar elemens while in editing mode
892 final boolean enabled = !isEditing();
894 // This alpha value has to be in sync with the one used
895 // in setButtonEnabled().
896 final float alpha = (enabled ? 1.0f : 0.24f);
898 if (!enabled) {
899 tabsCounter.onEnterEditingMode();
900 }
902 tabsButton.setEnabled(enabled);
903 ViewHelper.setAlpha(tabsCounter, alpha);
904 menuButton.setEnabled(enabled);
905 ViewHelper.setAlpha(menuIcon, alpha);
907 final int actionItemsCount = actionItemBar.getChildCount();
908 for (int i = 0; i < actionItemsCount; i++) {
909 actionItemBar.getChildAt(i).setEnabled(enabled);
910 }
911 ViewHelper.setAlpha(actionItemBar, alpha);
913 final Tab tab = Tabs.getInstance().getSelectedTab();
914 if (tab != null) {
915 setButtonEnabled(backButton, canDoBack(tab));
916 setButtonEnabled(forwardButton, canDoForward(tab));
918 // Once the editing mode is finished, we have to ensure that the
919 // forward button slides away if necessary. This is because we might
920 // have only disabled it (without hiding it) when the toolbar entered
921 // editing mode.
922 if (!isEditing()) {
923 animateForwardButton(canDoForward(tab) ?
924 ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE);
925 }
926 }
927 }
929 private void setUIMode(final UIMode uiMode) {
930 this.uiMode = uiMode;
931 urlEditLayout.setEnabled(uiMode == UIMode.EDIT);
932 }
934 /**
935 * Returns whether or not the URL bar is in editing mode (url bar is expanded, hiding the new
936 * tab button). Note that selection state is independent of editing mode.
937 */
938 public boolean isEditing() {
939 return (uiMode == UIMode.EDIT);
940 }
942 public boolean isAnimating() {
943 return isAnimatingEntry;
944 }
946 public void startEditing(String url, PropertyAnimator animator) {
947 if (isEditing()) {
948 return;
949 }
951 urlEditLayout.setText(url != null ? url : "");
953 setUIMode(UIMode.EDIT);
954 updateChildrenForEditing();
956 updateProgressVisibility();
958 if (startEditingListener != null) {
959 startEditingListener.onStartEditing();
960 }
962 final int curveTranslation = getUrlBarCurveTranslation();
963 final int entryTranslation = getUrlBarEntryTranslation();
964 shouldShrinkURLBar = (entryTranslation < 0);
966 if (urlBarTranslatingEdge != null) {
967 urlBarTranslatingEdge.setVisibility(View.VISIBLE);
968 if (shouldShrinkURLBar) {
969 urlBarEntry.setLayoutParams(urlBarEntryShrunkenLayoutParams);
970 }
971 }
973 if (Build.VERSION.SDK_INT < 11) {
974 showEditingWithoutAnimation(entryTranslation, curveTranslation);
975 } else if (HardwareUtils.isTablet()) {
976 // No animation.
977 showUrlEditLayout();
978 } else {
979 showEditingWithPhoneAnimation(animator, entryTranslation, curveTranslation);
980 }
981 }
983 private void showEditingWithoutAnimation(final int entryTranslation,
984 final int curveTranslation) {
985 showUrlEditLayout();
987 if (urlBarTranslatingEdge != null) {
988 ViewHelper.setTranslationX(urlBarTranslatingEdge, entryTranslation);
989 }
991 // Prevent taps through the editing mode cancel button (bug 1001243).
992 tabsButton.setEnabled(false);
994 ViewHelper.setTranslationX(tabsButton, curveTranslation);
995 ViewHelper.setTranslationX(tabsCounter, curveTranslation);
996 ViewHelper.setTranslationX(actionItemBar, curveTranslation);
998 if (hasSoftMenuButton) {
999 // Prevent tabs through the editing mode cancel button (bug 1001243).
1000 menuButton.setEnabled(false);
1002 ViewHelper.setTranslationX(menuButton, curveTranslation);
1003 ViewHelper.setTranslationX(menuIcon, curveTranslation);
1004 }
1005 }
1007 private void showEditingWithPhoneAnimation(final PropertyAnimator animator,
1008 final int entryTranslation, final int curveTranslation) {
1009 if (isAnimatingEntry)
1010 return;
1012 urlDisplayLayout.prepareStartEditingAnimation();
1014 // Slide toolbar elements.
1015 if (urlBarTranslatingEdge != null) {
1016 animator.attach(urlBarTranslatingEdge,
1017 PropertyAnimator.Property.TRANSLATION_X,
1018 entryTranslation);
1019 }
1021 animator.attach(tabsButton,
1022 PropertyAnimator.Property.TRANSLATION_X,
1023 curveTranslation);
1024 animator.attach(tabsCounter,
1025 PropertyAnimator.Property.TRANSLATION_X,
1026 curveTranslation);
1027 animator.attach(actionItemBar,
1028 PropertyAnimator.Property.TRANSLATION_X,
1029 curveTranslation);
1031 if (hasSoftMenuButton) {
1032 animator.attach(menuButton,
1033 PropertyAnimator.Property.TRANSLATION_X,
1034 curveTranslation);
1036 animator.attach(menuIcon,
1037 PropertyAnimator.Property.TRANSLATION_X,
1038 curveTranslation);
1039 }
1041 showUrlEditLayout(animator);
1043 animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
1044 @Override
1045 public void onPropertyAnimationStart() {
1046 }
1048 @Override
1049 public void onPropertyAnimationEnd() {
1050 isAnimatingEntry = false;
1051 }
1052 });
1054 isAnimatingEntry = true;
1055 }
1057 /**
1058 * Exits edit mode without updating the toolbar title.
1059 *
1060 * @return the url that was entered
1061 */
1062 public String cancelEdit() {
1063 Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN);
1064 return stopEditing();
1065 }
1067 /**
1068 * Exits edit mode, updating the toolbar title with the url that was just entered.
1069 *
1070 * @return the url that was entered
1071 */
1072 public String commitEdit() {
1073 final String url = stopEditing();
1074 if (!TextUtils.isEmpty(url)) {
1075 setTitle(url);
1076 }
1077 return url;
1078 }
1080 private String stopEditing() {
1081 final String url = urlEditLayout.getText();
1082 if (!isEditing()) {
1083 return url;
1084 }
1085 setUIMode(UIMode.DISPLAY);
1087 updateChildrenForEditing();
1089 if (stopEditingListener != null) {
1090 stopEditingListener.onStopEditing();
1091 }
1093 updateProgressVisibility();
1095 // The animation looks cleaner if the text in the URL bar is
1096 // not selected so clear the selection by clearing focus.
1097 urlEditLayout.clearFocus();
1099 if (Build.VERSION.SDK_INT < 11) {
1100 stopEditingWithoutAnimation();
1101 } else if (HardwareUtils.isTablet()) {
1102 // No animation.
1103 hideUrlEditLayout();
1104 } else {
1105 stopEditingWithPhoneAnimation();
1106 }
1108 return url;
1109 }
1111 private void stopEditingWithoutAnimation() {
1112 hideUrlEditLayout();
1114 updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount());
1116 if (urlBarTranslatingEdge != null) {
1117 urlBarTranslatingEdge.setVisibility(View.INVISIBLE);
1118 ViewHelper.setTranslationX(urlBarTranslatingEdge, 0);
1119 if (shouldShrinkURLBar) {
1120 urlBarEntry.setLayoutParams(urlBarEntryDefaultLayoutParams);
1121 }
1122 }
1124 tabsButton.setEnabled(true);
1126 ViewHelper.setTranslationX(tabsButton, 0);
1127 ViewHelper.setTranslationX(tabsCounter, 0);
1128 ViewHelper.setTranslationX(actionItemBar, 0);
1130 if (hasSoftMenuButton) {
1131 menuButton.setEnabled(true);
1133 ViewHelper.setTranslationX(menuButton, 0);
1134 ViewHelper.setTranslationX(menuIcon, 0);
1135 }
1136 }
1138 private void stopEditingWithPhoneAnimation() {
1139 final PropertyAnimator contentAnimator = new PropertyAnimator(250);
1140 contentAnimator.setUseHardwareLayer(false);
1142 // Slide the toolbar back to its original size.
1143 if (urlBarTranslatingEdge != null) {
1144 contentAnimator.attach(urlBarTranslatingEdge,
1145 PropertyAnimator.Property.TRANSLATION_X,
1146 0);
1147 }
1149 contentAnimator.attach(tabsButton,
1150 PropertyAnimator.Property.TRANSLATION_X,
1151 0);
1152 contentAnimator.attach(tabsCounter,
1153 PropertyAnimator.Property.TRANSLATION_X,
1154 0);
1155 contentAnimator.attach(actionItemBar,
1156 PropertyAnimator.Property.TRANSLATION_X,
1157 0);
1159 if (hasSoftMenuButton) {
1160 contentAnimator.attach(menuButton,
1161 PropertyAnimator.Property.TRANSLATION_X,
1162 0);
1164 contentAnimator.attach(menuIcon,
1165 PropertyAnimator.Property.TRANSLATION_X,
1166 0);
1167 }
1169 hideUrlEditLayout(contentAnimator);
1171 contentAnimator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
1172 @Override
1173 public void onPropertyAnimationStart() {
1174 }
1176 @Override
1177 public void onPropertyAnimationEnd() {
1178 if (urlBarTranslatingEdge != null) {
1179 urlBarTranslatingEdge.setVisibility(View.INVISIBLE);
1180 if (shouldShrinkURLBar) {
1181 urlBarEntry.setLayoutParams(urlBarEntryDefaultLayoutParams);
1182 }
1183 }
1185 PropertyAnimator buttonsAnimator = new PropertyAnimator(300);
1186 urlDisplayLayout.prepareStopEditingAnimation(buttonsAnimator);
1187 buttonsAnimator.start();
1189 isAnimatingEntry = false;
1191 // Trigger animation to update the tabs counter once the
1192 // tabs button is back on screen.
1193 updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount());
1194 }
1195 });
1197 isAnimatingEntry = true;
1198 contentAnimator.start();
1199 }
1201 private void setButtonEnabled(ImageButton button, boolean enabled) {
1202 final Drawable drawable = button.getDrawable();
1203 if (drawable != null) {
1204 // This alpha value has to be in sync with the one used
1205 // in updateChildrenForEditing().
1206 drawable.setAlpha(enabled ? 255 : 61);
1207 }
1209 button.setEnabled(enabled);
1210 }
1212 public void updateBackButton(Tab tab) {
1213 setButtonEnabled(backButton, canDoBack(tab));
1214 }
1216 private void animateForwardButton(final ForwardButtonAnimation animation) {
1217 // If the forward button is not visible, we must be
1218 // in the phone UI.
1219 if (forwardButton.getVisibility() != View.VISIBLE) {
1220 return;
1221 }
1223 final boolean showing = (animation == ForwardButtonAnimation.SHOW);
1225 // if the forward button's margin is non-zero, this means it has already
1226 // been animated to be visible¸ and vice-versa.
1227 MarginLayoutParams fwdParams = (MarginLayoutParams) forwardButton.getLayoutParams();
1228 if ((fwdParams.leftMargin > defaultForwardMargin && showing) ||
1229 (fwdParams.leftMargin == defaultForwardMargin && !showing)) {
1230 return;
1231 }
1233 // We want the forward button to show immediately when switching tabs
1234 final PropertyAnimator forwardAnim =
1235 new PropertyAnimator(isSwitchingTabs ? 10 : FORWARD_ANIMATION_DURATION);
1236 final int width = forwardButton.getWidth() / 2;
1238 forwardAnim.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
1239 @Override
1240 public void onPropertyAnimationStart() {
1241 if (!showing) {
1242 // Set the margin before the transition when hiding the forward button. We
1243 // have to do this so that the favicon isn't clipped during the transition
1244 MarginLayoutParams layoutParams =
1245 (MarginLayoutParams) urlDisplayLayout.getLayoutParams();
1246 layoutParams.leftMargin = 0;
1248 // Do the same on the URL edit container
1249 layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams();
1250 layoutParams.leftMargin = 0;
1252 requestLayout();
1253 // Note, we already translated the favicon, site security, and text field
1254 // in prepareForwardAnimation, so they should appear to have not moved at
1255 // all at this point.
1256 }
1257 }
1259 @Override
1260 public void onPropertyAnimationEnd() {
1261 if (showing) {
1262 MarginLayoutParams layoutParams =
1263 (MarginLayoutParams) urlDisplayLayout.getLayoutParams();
1264 layoutParams.leftMargin = urlBarViewOffset;
1266 layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams();
1267 layoutParams.leftMargin = urlBarViewOffset;
1268 }
1270 urlDisplayLayout.finishForwardAnimation();
1272 MarginLayoutParams layoutParams = (MarginLayoutParams) forwardButton.getLayoutParams();
1273 layoutParams.leftMargin = defaultForwardMargin + (showing ? width : 0);
1274 ViewHelper.setTranslationX(forwardButton, 0);
1276 requestLayout();
1277 }
1278 });
1280 prepareForwardAnimation(forwardAnim, animation, width);
1281 forwardAnim.start();
1282 }
1284 public void updateForwardButton(Tab tab) {
1285 final boolean enabled = canDoForward(tab);
1286 if (forwardButton.isEnabled() == enabled)
1287 return;
1289 // Save the state on the forward button so that we can skip animations
1290 // when there's nothing to change
1291 setButtonEnabled(forwardButton, enabled);
1292 animateForwardButton(enabled ? ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE);
1293 }
1295 private void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) {
1296 if (animation == ForwardButtonAnimation.HIDE) {
1297 anim.attach(forwardButton,
1298 PropertyAnimator.Property.TRANSLATION_X,
1299 -width);
1300 anim.attach(forwardButton,
1301 PropertyAnimator.Property.ALPHA,
1302 0);
1304 } else {
1305 anim.attach(forwardButton,
1306 PropertyAnimator.Property.TRANSLATION_X,
1307 width);
1308 anim.attach(forwardButton,
1309 PropertyAnimator.Property.ALPHA,
1310 1);
1311 }
1313 urlDisplayLayout.prepareForwardAnimation(anim, animation, width);
1314 }
1316 @Override
1317 public boolean addActionItem(View actionItem) {
1318 actionItemBar.addView(actionItem);
1319 return true;
1320 }
1322 @Override
1323 public void removeActionItem(View actionItem) {
1324 actionItemBar.removeView(actionItem);
1325 }
1327 @Override
1328 public void setPrivateMode(boolean isPrivate) {
1329 super.setPrivateMode(isPrivate);
1331 tabsButton.setPrivateMode(isPrivate);
1332 menuButton.setPrivateMode(isPrivate);
1333 menuIcon.setPrivateMode(isPrivate);
1334 urlEditLayout.setPrivateMode(isPrivate);
1336 if (backButton instanceof BackButton) {
1337 ((BackButton) backButton).setPrivateMode(isPrivate);
1338 }
1340 if (forwardButton instanceof ForwardButton) {
1341 ((ForwardButton) forwardButton).setPrivateMode(isPrivate);
1342 }
1343 }
1345 public void show() {
1346 setVisibility(View.VISIBLE);
1347 }
1349 public void hide() {
1350 setVisibility(View.GONE);
1351 }
1353 public View getDoorHangerAnchor() {
1354 return urlDisplayLayout.getDoorHangerAnchor();
1355 }
1357 public void onDestroy() {
1358 Tabs.unregisterOnTabsChangedListener(this);
1360 unregisterEventListener("Reader:Click");
1361 unregisterEventListener("Reader:LongClick");
1362 }
1364 public boolean openOptionsMenu() {
1365 if (!hasSoftMenuButton) {
1366 return false;
1367 }
1369 // Initialize the popup.
1370 if (menuPopup == null) {
1371 View panel = activity.getMenuPanel();
1372 menuPopup = new MenuPopup(activity);
1373 menuPopup.setPanelView(panel);
1375 menuPopup.setOnDismissListener(new PopupWindow.OnDismissListener() {
1376 @Override
1377 public void onDismiss() {
1378 activity.onOptionsMenuClosed(null);
1379 }
1380 });
1381 }
1383 GeckoAppShell.getGeckoInterface().invalidateOptionsMenu();
1384 if (!menuPopup.isShowing()) {
1385 menuPopup.showAsDropDown(menuButton);
1386 }
1388 return true;
1389 }
1391 public boolean closeOptionsMenu() {
1392 if (!hasSoftMenuButton) {
1393 return false;
1394 }
1396 if (menuPopup != null && menuPopup.isShowing()) {
1397 menuPopup.dismiss();
1398 }
1400 return true;
1401 }
1403 private void registerEventListener(String event) {
1404 GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
1405 }
1407 private void unregisterEventListener(String event) {
1408 GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this);
1409 }
1411 @Override
1412 public void handleMessage(String event, JSONObject message) {
1413 Log.d(LOGTAG, "handleMessage: " + event);
1414 if (event.equals("Reader:Click")) {
1415 Tab tab = Tabs.getInstance().getSelectedTab();
1416 if (tab != null) {
1417 tab.toggleReaderMode();
1418 }
1419 } else if (event.equals("Reader:LongClick")) {
1420 Tab tab = Tabs.getInstance().getSelectedTab();
1421 if (tab != null) {
1422 tab.addToReadingList();
1423 }
1424 }
1425 }
1427 @Override
1428 public void onLightweightThemeChanged() {
1429 Drawable drawable = theme.getDrawable(this);
1430 if (drawable == null)
1431 return;
1433 StateListDrawable stateList = new StateListDrawable();
1434 stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.background_private));
1435 stateList.addState(EMPTY_STATE_SET, drawable);
1437 setBackgroundDrawable(stateList);
1439 if (editCancel != null) {
1440 editCancel.onLightweightThemeChanged();
1441 }
1442 }
1444 @Override
1445 public void onLightweightThemeReset() {
1446 setBackgroundResource(R.drawable.url_bar_bg);
1447 if (editCancel != null) {
1448 editCancel.onLightweightThemeReset();
1449 }
1450 }
1451 }