diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStack.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStack.java index 120c0f975..f36760711 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStack.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStack.java @@ -20,6 +20,7 @@ import org.mozilla.geckoview.AllowOrDeny; import org.mozilla.geckoview.ContentBlocking; +import org.mozilla.geckoview.GeckoResponse; import org.mozilla.geckoview.GeckoResult; import org.mozilla.geckoview.GeckoRuntime; import org.mozilla.geckoview.GeckoSession; @@ -54,7 +55,7 @@ public class SessionStack implements ContentBlocking.Delegate, GeckoSession.NavigationDelegate, GeckoSession.ProgressDelegate, GeckoSession.ContentDelegate, GeckoSession.TextInputDelegate, GeckoSession.PromptDelegate, GeckoSession.MediaDelegate, GeckoSession.HistoryDelegate, - SharedPreferences.OnSharedPreferenceChangeListener { + GeckoSession.SelectionActionDelegate, SharedPreferences.OnSharedPreferenceChangeListener { private static final String LOGTAG = SystemUtils.createLogtag(SessionStack.class); // You can test a local file using: "resource://android/assets/webvr/index.html" @@ -66,6 +67,7 @@ public class SessionStack implements ContentBlocking.Delegate, GeckoSession.Navi private transient LinkedList mSessionChangeListeners; private transient LinkedList mTextInputListeners; private transient LinkedList mVideoAvailabilityListeners; + private transient LinkedList mSelectionActionListeners; private transient UserAgentOverride mUserAgentOverride; private transient GeckoSession mCurrentSession; @@ -92,6 +94,7 @@ protected SessionStack(Context context, GeckoRuntime runtime, boolean usePrivate mSessionChangeListeners = new LinkedList<>(); mTextInputListeners = new LinkedList<>(); mVideoAvailabilityListeners = new LinkedList<>(); + mSelectionActionListeners = new LinkedList<>(); if (mPrefs != null) { mPrefs.registerOnSharedPreferenceChangeListener(this); @@ -122,6 +125,7 @@ protected void shutdown() { mSessionChangeListeners.clear(); mTextInputListeners.clear(); mVideoAvailabilityListeners.clear(); + mSelectionActionListeners.clear(); if (mPrefs != null) { mPrefs.unregisterOnSharedPreferenceChangeListener(this); @@ -267,6 +271,27 @@ public void removeVideoAvailabilityListener(VideoAvailabilityListener aListener) mVideoAvailabilityListeners.remove(aListener); } + public void addSelectionActionListener(GeckoSession.SelectionActionDelegate aListener) { + mSelectionActionListeners.add(aListener); + } + + public void removeSelectionActionListener(GeckoSession.ContentDelegate aListener) { + mSelectionActionListeners.remove(aListener); + } + + private void setUpListeners(GeckoSession aSession) { + aSession.setNavigationDelegate(this); + aSession.setProgressDelegate(this); + aSession.setContentDelegate(this); + aSession.getTextInput().setDelegate(this); + aSession.setPermissionDelegate(mPermissionDelegate); + aSession.setPromptDelegate(mPromptDelegate); + aSession.setContentBlockingDelegate(this); + aSession.setMediaDelegate(this); + aSession.setHistoryDelegate(this); + aSession.setSelectionActionDelegate(this); + } + public void restore(SessionStack store, int currentSessionId) { mSessions.clear(); @@ -301,16 +326,8 @@ public void restore(SessionStack store, int currentSessionId) { } int newSessionId = state.mSession.hashCode(); + setUpListeners(state.mSession); - state.mSession.setNavigationDelegate(this); - state.mSession.setProgressDelegate(this); - state.mSession.setContentDelegate(this); - state.mSession.getTextInput().setDelegate(this); - state.mSession.setPermissionDelegate(mPermissionDelegate); - state.mSession.setPromptDelegate(mPromptDelegate); - state.mSession.setContentBlockingDelegate(this); - state.mSession.setMediaDelegate(this); - state.mSession.setHistoryDelegate(this); for (SessionChangeListener listener: mSessionChangeListeners) { listener.onNewSession(state.mSession, newSessionId); } @@ -368,15 +385,7 @@ private int createSession(@NonNull SessionSettings aSettings) { state.mSession.getSettings().setSuspendMediaWhenInactive(aSettings.isSuspendMediaWhenInactiveEnabled()); state.mSession.getSettings().setUserAgentMode(aSettings.getUserAgentMode()); state.mSession.getSettings().setUserAgentOverride(aSettings.getUserAgentOverride()); - state.mSession.setNavigationDelegate(this); - state.mSession.setProgressDelegate(this); - state.mSession.setContentDelegate(this); - state.mSession.getTextInput().setDelegate(this); - state.mSession.setPermissionDelegate(mPermissionDelegate); - state.mSession.setPromptDelegate(mPromptDelegate); - state.mSession.setContentBlockingDelegate(this); - state.mSession.setMediaDelegate(this); - state.mSession.setHistoryDelegate(this); + setUpListeners(state.mSession); for (SessionChangeListener listener: mSessionChangeListeners) { listener.onNewSession(state.mSession, result); } @@ -417,6 +426,7 @@ private void removeSession(int aSessionId) { session.setContentBlockingDelegate(null); session.setMediaDelegate(null); session.setHistoryDelegate(null); + session.setSelectionActionDelegate(this); mSessions.remove(aSessionId); for (SessionChangeListener listener: mSessionChangeListeners) { listener.onRemoveSession(session, aSessionId); @@ -1419,4 +1429,24 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin } } } + + // GeckoSession.SelectionActionDelegate + + @Override + public void onShowActionRequest(@NonNull GeckoSession aSession, @NonNull Selection selection, @NonNull String[] strings, @NonNull GeckoResponse geckoResponse) { + if (aSession == mCurrentSession) { + for (GeckoSession.SelectionActionDelegate listener : mSelectionActionListeners) { + listener.onShowActionRequest(aSession, selection, strings, geckoResponse); + } + } + } + + @Override + public void onHideAction(@NonNull GeckoSession aSession, int aHideReason) { + if (aSession == mCurrentSession) { + for (GeckoSession.SelectionActionDelegate listener : mSelectionActionListeners) { + listener.onHideAction(aSession, aHideReason); + } + } + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/ContextMenuAdapter.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/ContextMenuAdapter.java deleted file mode 100644 index 8e491e8e1..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/ContextMenuAdapter.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.mozilla.vrbrowser.ui.adapters; - -import android.graphics.drawable.Drawable; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.RecyclerView; - -import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.databinding.ContextMenuItemBinding; -import org.mozilla.vrbrowser.ui.callbacks.ContextMenuClickCallback; - -import java.util.List; - -public class ContextMenuAdapter extends RecyclerView.Adapter { - - private List mContextMenuList; - - public static class ContextMenuNode { - int position; - Drawable icon; - String title; - - public ContextMenuNode(int position, Drawable icon, String title) { - this.position = position; - this.icon = icon; - this.title = title; - } - - public Integer getPosition() { - return position; - } - - public void setPosition(int position) { - this.position = position; - } - - public Drawable getIcon() { - return icon; - } - - public void setIcon(Drawable icon) { - this.icon = icon; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Nullable - private final ContextMenuClickCallback mContextMenuItemClickCallback; - - public ContextMenuAdapter(@Nullable ContextMenuClickCallback clickCallback) { - mContextMenuItemClickCallback = clickCallback; - - setHasStableIds(true); - } - - public void setContextMenuItemList(final List contextMenuList) { - mContextMenuList = contextMenuList; - } - - @Override - public ContextMenuViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - ContextMenuItemBinding binding = DataBindingUtil - .inflate(LayoutInflater.from(parent.getContext()), R.layout.context_menu_item, - parent, false); - binding.setCallback(mContextMenuItemClickCallback); - return new ContextMenuViewHolder(binding); - } - - @Override - public void onBindViewHolder(@NonNull ContextMenuViewHolder holder, int position) { - holder.binding.setMenuItem(mContextMenuList.get(position)); - holder.binding.executePendingBindings(); - } - - @Override - public int getItemCount() { - return mContextMenuList == null ? 0 : mContextMenuList.size(); - } - - @Override - public long getItemId(int position) { - ContextMenuNode menuItem = mContextMenuList.get(position); - return menuItem.getPosition() != null ? menuItem.getPosition() : RecyclerView.NO_ID; - } - - static class ContextMenuViewHolder extends RecyclerView.ViewHolder { - - final ContextMenuItemBinding binding; - - ContextMenuViewHolder(@NonNull ContextMenuItemBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - -} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/ContextMenuClickCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/ContextMenuClickCallback.java deleted file mode 100644 index 58cc7e8c2..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/ContextMenuClickCallback.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.mozilla.vrbrowser.ui.callbacks; - -import org.mozilla.vrbrowser.ui.adapters.ContextMenuAdapter; - -public interface ContextMenuClickCallback { - void onClick(ContextMenuAdapter.ContextMenuNode contextMenuNode); -} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ContextMenu.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ContextMenu.java deleted file mode 100644 index d0e72b189..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ContextMenu.java +++ /dev/null @@ -1,104 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.vrbrowser.ui.views; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.widget.FrameLayout; - -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.databinding.ContextMenuBinding; -import org.mozilla.vrbrowser.ui.adapters.ContextMenuAdapter; -import org.mozilla.vrbrowser.ui.callbacks.ContextMenuClickCallback; - -import java.util.Arrays; -import java.util.List; - -public class ContextMenu extends FrameLayout { - - private ContextMenuBinding mBinding; - private ContextMenuAdapter mContextMenuAdapter; - private ContextMenuClickCallback mCallback; - - public ContextMenu(Context aContext) { - super(aContext); - initialize(aContext); - } - - public ContextMenu(Context aContext, AttributeSet aAttrs) { - super(aContext, aAttrs); - initialize(aContext); - } - - public ContextMenu(Context aContext, AttributeSet aAttrs, int aDefStyle) { - super(aContext, aAttrs, aDefStyle); - initialize(aContext); - } - - private void initialize(Context aContext) { - LayoutInflater inflater = LayoutInflater.from(aContext); - - mBinding = DataBindingUtil.inflate(inflater, R.layout.context_menu, this, true); - mContextMenuAdapter = new ContextMenuAdapter(mContextMenuClickCallback); - mBinding.contextMenuList.setAdapter(mContextMenuAdapter); - mBinding.contextMenuList.setLayoutManager(new LinearLayoutManager(getContext()) { - @Override - public boolean canScrollVertically() { - return false; - } - }); - mBinding.executePendingBindings(); - } - - public void setContextMenuClickCallback(ContextMenuClickCallback callback) { - mCallback = callback; - } - - private final ContextMenuClickCallback mContextMenuClickCallback = contextMenuNode -> { - if (mCallback != null) { - mCallback.onClick(contextMenuNode); - } - }; - - public void createLinkContextMenu() { - List contextMenuItems = buildBaseContextMenu(); - mContextMenuAdapter.setContextMenuItemList(contextMenuItems); - mBinding.executePendingBindings(); - } - - public void createAudioContextMenu() { - List contextMenuItems = buildBaseContextMenu(); - mContextMenuAdapter.setContextMenuItemList(contextMenuItems); - mBinding.executePendingBindings(); - } - - public void createVideoContextMenu() { - List contextMenuItems = buildBaseContextMenu(); - mContextMenuAdapter.setContextMenuItemList(contextMenuItems); - mBinding.executePendingBindings(); - } - - public void createImageContextMenu() { - List contextMenuItems = buildBaseContextMenu(); - mContextMenuAdapter.setContextMenuItemList(contextMenuItems); - mBinding.executePendingBindings(); - } - - private List buildBaseContextMenu() { - return Arrays.asList( - new ContextMenuAdapter.ContextMenuNode( - 0, - getResources().getDrawable(R.drawable.ic_context_menu_new_window, getContext().getTheme()), - "Open in a New Window") - ); - } - -} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomInlineAutocompleteEditText.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomInlineAutocompleteEditText.java new file mode 100644 index 000000000..d28ea4882 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomInlineAutocompleteEditText.java @@ -0,0 +1,40 @@ +package org.mozilla.vrbrowser.ui.views; + +import android.content.Context; +import android.util.AttributeSet; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import mozilla.components.ui.autocomplete.InlineAutocompleteEditText; + +public class CustomInlineAutocompleteEditText extends InlineAutocompleteEditText { + private OnSelectionChangedCallback mSelectionCallback; + interface OnSelectionChangedCallback { + void onSelectionChanged(int selectionStart, int selectionEnd); + } + + public CustomInlineAutocompleteEditText(@NotNull Context ctx, @Nullable AttributeSet attrs, int defStyleAttr) { + super(ctx, attrs, defStyleAttr); + } + + public CustomInlineAutocompleteEditText(@NotNull Context ctx, @Nullable AttributeSet attrs) { + super(ctx, attrs); + } + + public CustomInlineAutocompleteEditText(@NotNull Context ctx) { + super(ctx); + } + + @Override + public void onSelectionChanged(int selStart, int selEnd) { + super.onSelectionChanged(selStart, selEnd); + if (mSelectionCallback != null) { + mSelectionCallback.onSelectionChanged(selStart, selEnd); + } + } + + public void setOnSelectionChangedCallback(@Nullable OnSelectionChangedCallback aCallback) { + mSelectionCallback = aCallback; + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java index 96bcd2320..f389b983e 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java @@ -6,6 +6,8 @@ package org.mozilla.vrbrowser.ui.views; import android.annotation.SuppressLint; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.res.Resources; import android.text.Editable; @@ -30,6 +32,8 @@ import androidx.annotation.NonNull; import androidx.annotation.StringRes; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.geckoview.GeckoSession; import org.mozilla.geckoview.GeckoSessionSettings; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; @@ -38,16 +42,20 @@ import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; +import org.mozilla.vrbrowser.ui.widgets.UIWidget; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.ui.widgets.dialogs.SelectionActionWidget; import org.mozilla.vrbrowser.utils.StringUtils; import org.mozilla.vrbrowser.utils.SystemUtils; import org.mozilla.vrbrowser.utils.UIThreadExecutor; import org.mozilla.vrbrowser.utils.UrlUtils; +import org.mozilla.vrbrowser.utils.ViewUtils; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URL; import java.net.URLDecoder; +import java.util.ArrayList; import kotlin.Unit; import mozilla.components.browser.domains.autocomplete.DomainAutocompleteResult; @@ -58,7 +66,7 @@ public class NavigationURLBar extends FrameLayout { private static final String LOGTAG = SystemUtils.createLogtag(NavigationURLBar.class); - private InlineAutocompleteEditText mURL; + private CustomInlineAutocompleteEditText mURL; private UIButton mMicrophoneButton; private UIButton mUAModeButton; private ImageView mInsecureIcon; @@ -81,6 +89,10 @@ public class NavigationURLBar extends FrameLayout { private boolean mIsContextButtonsEnabled = true; private UIThreadExecutor mUIThreadExecutor = new UIThreadExecutor(); private SessionStack mSessionStack; + private SelectionActionWidget mSelectionMenu; + private boolean mWasFocusedWhenTouchBegan = false; + private boolean mLongPressed = false; + private int lastTouchDownOffset = 0; private Unit domainAutocompleteFilter(String text) { if (mURL != null) { @@ -99,9 +111,10 @@ private Unit domainAutocompleteFilter(String text) { } public interface NavigationURLBarDelegate { - void OnVoiceSearchClicked(); - void OnShowSearchPopup(); + void onVoiceSearchClicked(); + void onShowSearchPopup(); void onHideSearchPopup(); + void onLongPress(float centerX, SelectionActionWidget actionMenu); } public NavigationURLBar(Context context, AttributeSet attrs) { @@ -139,22 +152,76 @@ private void initialize(Context aContext) { updateHintFading(); mURL.setSelection(mURL.getText().length(), 0); + if (focused) { + mURL.selectAll(); + } else { + hideSelectionMenu(); + } + }); final GestureDetector gd = new GestureDetector(getContext(), new UrlGestureListener()); gd.setOnDoubleTapListener(mUrlDoubleTapListener); mURL.setOnTouchListener((view, motionEvent) -> { + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + mWasFocusedWhenTouchBegan = view.isFocused(); + lastTouchDownOffset = ViewUtils.getCursorOffset(mURL, motionEvent.getX()); + } else if (mLongPressed && motionEvent.getAction() == MotionEvent.ACTION_MOVE) { + // Selection gesture while longpressing + ViewUtils.placeSelection(mURL, lastTouchDownOffset, ViewUtils.getCursorOffset(mURL, motionEvent.getX())); + } else if (motionEvent.getAction() == MotionEvent.ACTION_UP || motionEvent.getAction() == MotionEvent.ACTION_CANCEL) { + mLongPressed = false; + } if (gd.onTouchEvent(motionEvent)) { return true; } + if (mLongPressed) { + // Do not scroll editable when selecting text after a long press. + return true; + } return view.onTouchEvent(motionEvent); }); + mURL.setOnClickListener(v -> { + if (mWasFocusedWhenTouchBegan) { + hideSelectionMenu(); + } + }); mURL.setOnLongClickListener(v -> { - mURL.requestFocus(); - return false; + if (!v.isFocused()) { + mURL.requestFocus(); + mURL.selectAll(); + } else if (!mURL.hasSelection()) { + // Place the cursor in the longpressed position. + if (lastTouchDownOffset >= 0) { + mURL.setSelection(lastTouchDownOffset); + } + mLongPressed = true; + } + // Add some delay so selection ranges are ready + ThreadUtils.postDelayedToUiThread(this::handleLongPress, 10); + return true; }); mURL.addTextChangedListener(mURLTextWatcher); + mURL.setOnSelectionChangedCallback((start, end) -> { + if (mSelectionMenu != null) { + boolean hasCopy = mSelectionMenu.hasAction(GeckoSession.SelectionActionDelegate.ACTION_COPY); + boolean showCopy = end > start; + if (hasCopy != showCopy) { + handleLongPress(); + } else { + mDelegate.onLongPress(getSelectionCenterX(), mSelectionMenu); + mSelectionMenu.updateWidget(); + } + } + }); + + mURL.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { + if (mLongPressed) { + hideSelectionMenu(); + } + }); + // Set a filter to provide domain autocomplete results mURL.setOnFilterListener(this::domainAutocompleteFilter); @@ -580,7 +647,7 @@ public void setClickable(boolean clickable) { view.requestFocusFromTouch(); if (mDelegate != null) { - mDelegate.OnVoiceSearchClicked(); + mDelegate.onVoiceSearchClicked(); } TelemetryWrapper.voiceInputEvent(); @@ -590,7 +657,6 @@ public void setClickable(boolean clickable) { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } - view.requestFocusFromTouch(); int uaMode = mSessionStack.getUaMode(); @@ -631,8 +697,9 @@ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { @Override public void afterTextChanged(Editable editable) { if (mDelegate != null) { - mDelegate.OnShowSearchPopup(); + mDelegate.onShowSearchPopup(); } + hideSelectionMenu(); } }; @@ -647,7 +714,7 @@ public boolean onDoubleTap(MotionEvent event) { GestureDetector.OnDoubleTapListener mUrlDoubleTapListener = new GestureDetector.OnDoubleTapListener() { @Override public boolean onSingleTapConfirmed(MotionEvent motionEvent) { - return false; + return true; } @Override @@ -657,9 +724,110 @@ public boolean onDoubleTap(MotionEvent motionEvent) { @Override public boolean onDoubleTapEvent(MotionEvent motionEvent) { - mURL.setSelection(mURL.getText().length(), 0); + mURL.selectAll(); return true; } }; + private void handleLongPress() { + ArrayList actions = new ArrayList<>(); + if (mURL.getSelectionEnd() > mURL.getSelectionStart()) { + actions.add(GeckoSession.SelectionActionDelegate.ACTION_CUT); + actions.add(GeckoSession.SelectionActionDelegate.ACTION_COPY); + } + ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard.hasPrimaryClip()) { + actions.add(GeckoSession.SelectionActionDelegate.ACTION_PASTE); + } + if (!StringUtils.isEmpty(mURL.getText().toString()) && + (mURL.getSelectionStart() != 0 || mURL.getSelectionEnd() != mURL.getText().toString().length())) { + actions.add(GeckoSession.SelectionActionDelegate.ACTION_SELECT_ALL); + } + + if (actions.size() == 0) { + hideSelectionMenu(); + return; + } + + String[] actionsArray = actions.toArray(new String[0]); + if (mSelectionMenu != null && !mSelectionMenu.hasSameActions(actionsArray)) { + // Release current selection menu to recreate it with different actions. + hideSelectionMenu(); + } + + if (mSelectionMenu == null) { + mSelectionMenu = new SelectionActionWidget(getContext()); + mSelectionMenu.setActions(actionsArray); + mSelectionMenu.setDelegate(new SelectionActionWidget.Delegate() { + @Override + public void onAction(String action) { + int startSelection = mURL.getSelectionStart(); + int endSelection = mURL.getSelectionEnd(); + boolean selectionValid = endSelection > startSelection; + + if (action.equals(GeckoSession.SelectionActionDelegate.ACTION_CUT) && selectionValid) { + String selectedText = mURL.getText().toString().substring(startSelection, endSelection); + clipboard.setPrimaryClip(ClipData.newPlainText("text", selectedText)); + mURL.setText(StringUtils.removeRange(mURL.getText().toString(), startSelection, endSelection)); + } else if (action.equals(GeckoSession.SelectionActionDelegate.ACTION_COPY) && selectionValid) { + String selectedText = mURL.getText().toString().substring(startSelection, endSelection); + clipboard.setPrimaryClip(ClipData.newPlainText("text", selectedText)); + } else if (action.equals(GeckoSession.SelectionActionDelegate.ACTION_PASTE) && clipboard.hasPrimaryClip()) { + ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); + if (selectionValid) { + mURL.setText(StringUtils.removeRange(mURL.getText().toString(), startSelection, endSelection)); + } + if (item != null && item.getText() != null) { + mURL.getText().insert(mURL.getSelectionStart(), item.getText()); + } else if (item != null && item.getUri() != null) { + mURL.getText().insert(mURL.getSelectionStart(), item.getUri().toString()); + } + } else if (action.equals(GeckoSession.SelectionActionDelegate.ACTION_SELECT_ALL)) { + mURL.selectAll(); + handleLongPress(); + return; + + } + hideSelectionMenu(); + } + + @Override + public void onDismiss() { + hideSelectionMenu(); + } + }); + } + + if (mDelegate != null) { + mDelegate.onLongPress(getSelectionCenterX(), mSelectionMenu); + } + + mSelectionMenu.show(UIWidget.KEEP_FOCUS); + } + + + private float getSelectionCenterX() { + float start = 0; + if (mURL.getSelectionStart() >= 0) { + start = ViewUtils.GetLetterPositionX(mURL, mURL.getSelectionStart(), true); + } + float end = start; + if (mURL.getSelectionEnd() > mURL.getSelectionStart()) { + end = ViewUtils.GetLetterPositionX(mURL, mURL.getSelectionEnd(), true); + } + if (end < start) { + end = start; + } + return start + (end - start) * 0.5f; + } + + private void hideSelectionMenu() { + if (mSelectionMenu != null) { + mSelectionMenu.setDelegate((SelectionActionWidget.Delegate) null); + mSelectionMenu.hide(UIWidget.REMOVE_WIDGET); + mSelectionMenu.releaseWidget(); + mSelectionMenu = null; + } + } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrightnessMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrightnessMenuWidget.java index fc03baeca..1bef27503 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrightnessMenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrightnessMenuWidget.java @@ -40,9 +40,9 @@ public void run() { } }; - mItems.add(new MenuItem(R.string.brightness_mode_normal, 0, action)); - mItems.add(new MenuItem(R.string.brightness_mode_dark, 0, action)); - mItems.add(new MenuItem(R.string.brightness_mode_void, 0, action)); + mItems.add(new MenuItem(getContext().getString(R.string.brightness_mode_normal), 0, action)); + mItems.add(new MenuItem(getContext().getString(R.string.brightness_mode_dark), 0, action)); + mItems.add(new MenuItem(getContext().getString(R.string.brightness_mode_void), 0, action)); super.updateMenuItems(mItems); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MenuWidget.java index 89c7ef19d..e421fd6f3 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MenuWidget.java @@ -7,7 +7,8 @@ package org.mozilla.vrbrowser.ui.widgets; import android.content.Context; -import android.graphics.Color; +import android.graphics.Point; +import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -16,16 +17,27 @@ import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.ui.views.UITextButton; +import org.mozilla.vrbrowser.utils.StringUtils; +import org.mozilla.vrbrowser.utils.ViewUtils; import java.util.ArrayList; +import static android.view.Gravity.CENTER_VERTICAL; + public abstract class MenuWidget extends UIWidget { protected MenuAdapter mAdapter; protected ListView mListView; + protected View menuContainer; public MenuWidget(Context aContext) { super(aContext); @@ -40,6 +52,7 @@ public MenuWidget(Context aContext, ArrayList aItems) { private void initialize(Context aContext, ArrayList aItems) { inflate(aContext, R.layout.menu, this); mListView = findViewById(R.id.menuListView); + menuContainer = findViewById(R.id.menuContainer); mAdapter = new MenuAdapter(aContext, aItems); @@ -73,24 +86,26 @@ public int getSelectedItem() { return mListView.getCheckedItemPosition(); } - public class MenuItem { - int mStringId; + public static class MenuItem { + String mText; int mImageId; Runnable mCallback; - MenuItem(int aStringId, int aImage, Runnable aCallback) { - mStringId = aStringId; + public MenuItem(String aString, int aImage, Runnable aCallback) { + mText = aString; mImageId = aImage; mCallback = aCallback; } } - class MenuAdapter extends BaseAdapter implements OnHoverListener { + public static class MenuAdapter extends BaseAdapter implements OnHoverListener { private Context mContext; private ArrayList mItems; private LayoutInflater mInflater; private Drawable firstItemDrawable; private Drawable lastItemDrawable; + private Drawable regularItemDrawable; + private int layoutId; MenuAdapter(Context aContext, ArrayList aItems) { mContext = aContext; @@ -98,6 +113,18 @@ class MenuAdapter extends BaseAdapter implements OnHoverListener { mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); firstItemDrawable = aContext.getDrawable(R.drawable.menu_item_background_first); lastItemDrawable = aContext.getDrawable(R.drawable.menu_item_background_last); + regularItemDrawable = aContext.getDrawable(R.drawable.menu_item_background); + layoutId = R.layout.menu_item_image_text; + } + + public void updateBackgrounds(Drawable first, Drawable last, Drawable regular) { + firstItemDrawable = first; + lastItemDrawable = last; + regularItemDrawable = regular; + } + + public void updateLayourId(int aLayoutId) { + layoutId = aLayoutId; } @@ -120,7 +147,7 @@ public long getItemId(int position) { public View getView(int position, View convertView, ViewGroup parent) { View view = convertView; if (view == null) { - view = mInflater.inflate(R.layout.menu_item_image_text, parent, false); + view = mInflater.inflate(layoutId, parent, false); view.setOnHoverListener(this); } view.setTag(R.string.position_tag, position); @@ -128,6 +155,8 @@ public View getView(int position, View convertView, ViewGroup parent) { view.setBackground(firstItemDrawable); } else if (position == mItems.size() - 1) { view.setBackground(lastItemDrawable); + } else { + view.setBackground(regularItemDrawable); } MenuItem item = mItems.get(position); @@ -135,12 +164,23 @@ public View getView(int position, View convertView, ViewGroup parent) { TextView textView = view.findViewById(R.id.listItemText); ImageView imageView = view.findViewById(R.id.listItemImage); - textView.setText(mContext.getString(item.mStringId)); - if (item.mImageId > 0) { - imageView.setImageResource(item.mImageId); - } else { - imageView.setVisibility(View.GONE); - textView.setTextAlignment(TEXT_ALIGNMENT_CENTER); + textView.setText(item.mText); + if (imageView != null) { + if (item.mImageId > 0) { + imageView.setImageResource(item.mImageId); + } else { + imageView.setVisibility(View.GONE); + textView.setTextAlignment(TEXT_ALIGNMENT_CENTER); + } + } + + if (item.mCallback == null) { + textView.setTextColor(mContext.getColor(R.color.rhino)); + } + + View separator = view.findViewById(R.id.listItemSeparator); + if (separator != null) { + separator.setVisibility(item.mCallback != null ? View.GONE : View.VISIBLE); } return view; @@ -153,25 +193,35 @@ public boolean onHover(View view, MotionEvent event) { return false; } + MenuItem item = mItems.get(position); + if (item.mCallback == null) { + return false; + } + TextView label = view.findViewById(R.id.listItemText); ImageView image = view.findViewById(R.id.listItemImage); switch (event.getActionMasked()) { case MotionEvent.ACTION_HOVER_ENTER: view.setHovered(true); label.setHovered(true); - label.setShadowLayer(label.getShadowRadius(), label.getShadowDx(), label.getShadowDy(), getContext().getColor(R.color.text_shadow_light)); - image.setHovered(true); + label.setShadowLayer(label.getShadowRadius(), label.getShadowDx(), label.getShadowDy(), mContext.getColor(R.color.text_shadow_light)); + if (image != null) { + image.setHovered(true); + } return true; case MotionEvent.ACTION_HOVER_EXIT: view.setHovered(false); - label.setShadowLayer(label.getShadowRadius(), label.getShadowDx(), label.getShadowDy(), getContext().getColor(R.color.text_shadow)); + label.setShadowLayer(label.getShadowRadius(), label.getShadowDx(), label.getShadowDy(), mContext.getColor(R.color.text_shadow)); label.setHovered(false); - image.setHovered(false); + if (image != null) { + image.setHovered(false); + } return true; } return false; } } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java index 2a69bb3c4..f1c78e64c 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java @@ -8,6 +8,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.graphics.Canvas; +import android.graphics.Rect; import android.net.Uri; import android.preference.PreferenceManager; import android.util.AttributeSet; @@ -34,6 +35,7 @@ import org.mozilla.vrbrowser.ui.views.NavigationURLBar; import org.mozilla.vrbrowser.ui.views.UIButton; import org.mozilla.vrbrowser.ui.views.UITextButton; +import org.mozilla.vrbrowser.ui.widgets.dialogs.SelectionActionWidget; import org.mozilla.vrbrowser.ui.widgets.dialogs.VoiceSearchWidget; import org.mozilla.vrbrowser.utils.AnimationHelper; import org.mozilla.vrbrowser.utils.ServoUtils; @@ -577,7 +579,7 @@ private void exitResizeMode(ResizeAction aResizeAction) { } else { AnimationHelper.fadeIn(mNavigationContainer, AnimationHelper.FADE_ANIMATION_DURATION, null); } - AnimationHelper.fadeOut(mResizeModeContainer, 0, () -> updateWidget()); + AnimationHelper.fadeOut(mResizeModeContainer, 0, () -> onWidgetUpdate(mAttachedWindow)); mWidgetManager.popBackHandler(mResizeBackHandler); mWidgetManager.setTrayVisible(!mAttachedWindow.isFullScreen()); closeFloatingMenus(); @@ -878,7 +880,7 @@ public void onCurrentSessionChange(GeckoSession aSession, int aId) { // NavigationURLBarDelegate @Override - public void OnVoiceSearchClicked() { + public void onVoiceSearchClicked() { if (mVoiceSearchWidget.isVisible()) { mVoiceSearchWidget.hide(REMOVE_WIDGET); @@ -890,7 +892,7 @@ public void OnVoiceSearchClicked() { @Override - public void OnShowSearchPopup() { + public void onShowSearchPopup() { if (mPopup == null) { mPopup = createChild(SuggestionsWidget.class); mPopup.setURLBarPopupDelegate(this); @@ -928,6 +930,18 @@ public void onHideSearchPopup() { } } + @Override + public void onLongPress(float centerX, SelectionActionWidget actionMenu) { + actionMenu.getPlacement().parentHandle = this.getHandle(); + actionMenu.getPlacement().parentAnchorY = 1.0f; + actionMenu.getPlacement().anchorY = 0.34f; + Rect offsetViewBounds = new Rect(); + mURLBar.getDrawingRect(offsetViewBounds); + offsetDescendantRectToMyCoords(mURLBar, offsetViewBounds); + float x = offsetViewBounds.left + centerX; + actionMenu.getPlacement().parentAnchorX = x / getMeasuredWidth(); + } + // VoiceSearch Delegate @Override @@ -1058,9 +1072,4 @@ private void startWidgetResize() { mWidgetManager.startWidgetResize(mAttachedWindow, maxSize.first, 4.5f, minSize.first, minSize.second); } } - - private void updateWidget() { - onWidgetUpdate(mAttachedWindow); - } - } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java index 912306e2c..0cf8eede4 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java @@ -305,10 +305,11 @@ public boolean isLayer() { return mRenderer != null && mRenderer.isLayer(); } - @IntDef(value = { REQUEST_FOCUS, CLEAR_FOCUS }) + @IntDef(value = { REQUEST_FOCUS, CLEAR_FOCUS, KEEP_FOCUS }) public @interface ShowFlags {} public static final int REQUEST_FOCUS = 0; public static final int CLEAR_FOCUS = 1; + public static final int KEEP_FOCUS = 2; public void show(@ShowFlags int aShowFlags) { if (!mWidgetPlacement.visible) { @@ -320,8 +321,7 @@ public void show(@ShowFlags int aShowFlags) { setFocusableInTouchMode(false); if (aShowFlags == REQUEST_FOCUS) { requestFocusFromTouch(); - - } else { + } else if (aShowFlags == CLEAR_FOCUS) { clearFocus(); } } @@ -375,6 +375,12 @@ public void setVisible(boolean aVisible) { } } + public void updateWidget() { + if (mWidgetManager != null) { + mWidgetManager.updateWidget(this); + } + } + @Override public int getBorderWidth() { return 0; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/VideoProjectionMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/VideoProjectionMenuWidget.java index de71e721d..f504a0447 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/VideoProjectionMenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/VideoProjectionMenuWidget.java @@ -60,47 +60,23 @@ public void setDelegate(@Nullable Delegate aDelegate) { private void createMenuItems() { mItems = new ArrayList<>(); - mItems.add(new MenuItem(R.string.video_mode_3d_side, R.drawable.ic_icon_videoplayback_3dsidebyside, new Runnable() { - @Override - public void run() { - handleClick(VIDEO_PROJECTION_3D_SIDE_BY_SIDE); - } - })); + mItems.add(new MenuItem(getContext().getString(R.string.video_mode_3d_side), + R.drawable.ic_icon_videoplayback_3dsidebyside, () -> handleClick(VIDEO_PROJECTION_3D_SIDE_BY_SIDE))); - mItems.add(new MenuItem(R.string.video_mode_360, R.drawable.ic_icon_videoplayback_360, new Runnable() { - @Override - public void run() { - handleClick(VIDEO_PROJECTION_360); - } - })); + mItems.add(new MenuItem(getContext().getString(R.string.video_mode_360), + R.drawable.ic_icon_videoplayback_360, () -> handleClick(VIDEO_PROJECTION_360))); - mItems.add(new MenuItem(R.string.video_mode_360_stereo, R.drawable.ic_icon_videoplayback_360_stereo, new Runnable() { - @Override - public void run() { - handleClick(VIDEO_PROJECTION_360_STEREO); - } - })); + mItems.add(new MenuItem(getContext().getString(R.string.video_mode_360_stereo), + R.drawable.ic_icon_videoplayback_360_stereo, () -> handleClick(VIDEO_PROJECTION_360_STEREO))); - mItems.add(new MenuItem(R.string.video_mode_180, R.drawable.ic_icon_videoplayback_180, new Runnable() { - @Override - public void run() { - handleClick(VIDEO_PROJECTION_180); - } - })); + mItems.add(new MenuItem(getContext().getString(R.string.video_mode_180), + R.drawable.ic_icon_videoplayback_180, () -> handleClick(VIDEO_PROJECTION_180))); - mItems.add(new MenuItem(R.string.video_mode_180_left_right, R.drawable.ic_icon_videoplayback_180_stereo_leftright, new Runnable() { - @Override - public void run() { - handleClick(VIDEO_PROJECTION_180_STEREO_LEFT_RIGHT); - } - })); + mItems.add(new MenuItem(getContext().getString(R.string.video_mode_180_left_right), + R.drawable.ic_icon_videoplayback_180_stereo_leftright, () -> handleClick(VIDEO_PROJECTION_180_STEREO_LEFT_RIGHT))); - mItems.add(new MenuItem(R.string.video_mode_180_top_bottom, R.drawable.ic_icon_videoplayback_180_stereo_topbottom, new Runnable() { - @Override - public void run() { - handleClick(VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM); - } - })); + mItems.add(new MenuItem(getContext().getString(R.string.video_mode_180_top_bottom), + R.drawable.ic_icon_videoplayback_180_stereo_topbottom, () -> handleClick(VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM))); super.updateMenuItems(mItems); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java index 06093f591..ae322db5c 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java @@ -9,6 +9,7 @@ import android.content.res.Resources; import android.util.DisplayMetrics; import android.util.TypedValue; +import android.view.View; import androidx.annotation.NonNull; @@ -50,6 +51,7 @@ public WidgetPlacement(Context aContext) { public float textureScale = 0.7f; // Widget will be curved if enabled. public boolean cylinder = true; + public int tintColor = 0xFFFFFFFF; public int borderColor = 0; public String name; // Color used to render the widget before the it's composited @@ -94,6 +96,7 @@ public void copyFrom(WidgetPlacement w) { this.textureScale = w.textureScale; this.cylinder = w.cylinder; this.cylinderMapRadius = w.cylinderMapRadius; + this.tintColor = w.tintColor; this.borderColor = w.borderColor; this.name = w.name; } @@ -114,6 +117,16 @@ public int viewHeight() { return (int) Math.ceil(height * density); } + public void setSizeFromMeasure(Context aContext, View aView) { + aView.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); + int border = SettingsStore.getInstance(aContext).getTransparentBorderWidth(); + int paddingH = aView.getPaddingStart() + aView.getPaddingEnd(); + int paddingV = aView.getPaddingTop() + aView.getPaddingBottom(); + width = (int)Math.ceil((aView.getMeasuredWidth() + paddingH)/density) + border * 2; + height = (int)Math.ceil((aView.getMeasuredHeight() + paddingV)/density) + border * 2; + } + public static int pixelDimension(Context aContext, int aDimensionID) { return aContext.getResources().getDimensionPixelSize(aDimensionID); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java index d4ec5ce65..8fd6ea6c7 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java @@ -7,7 +7,7 @@ import android.content.Context; import android.graphics.Canvas; -import android.graphics.Point; +import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.SurfaceTexture; @@ -28,6 +28,7 @@ import org.jetbrains.annotations.NotNull; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.geckoview.GeckoDisplay; +import org.mozilla.geckoview.GeckoResponse; import org.mozilla.geckoview.GeckoResult; import org.mozilla.geckoview.GeckoSession; import org.mozilla.geckoview.PanZoomController; @@ -51,6 +52,7 @@ import org.mozilla.vrbrowser.ui.widgets.dialogs.LibraryItemContextMenuWidget; import org.mozilla.vrbrowser.ui.widgets.dialogs.MaxWindowsWidget; import org.mozilla.vrbrowser.ui.widgets.dialogs.MessageDialogWidget; +import org.mozilla.vrbrowser.ui.widgets.dialogs.SelectionActionWidget; import org.mozilla.vrbrowser.ui.widgets.prompts.AlertPromptWidget; import org.mozilla.vrbrowser.ui.widgets.prompts.AuthPromptWidget; import org.mozilla.vrbrowser.ui.widgets.prompts.ChoicePromptWidget; @@ -75,7 +77,8 @@ public class WindowWidget extends UIWidget implements SessionChangeListener, GeckoSession.ContentDelegate, GeckoSession.PromptDelegate, GeckoSession.NavigationDelegate, VideoAvailabilityListener, - GeckoSession.HistoryDelegate, GeckoSession.ProgressDelegate { + GeckoSession.HistoryDelegate, GeckoSession.ProgressDelegate, + GeckoSession.SelectionActionDelegate { public interface HistoryViewDelegate { default void onHistoryViewShown(WindowWidget aWindow) {} @@ -107,6 +110,7 @@ default void onBookmarksHidden(WindowWidget aWindow) {} private MessageDialogWidget mAppDialog; private ClearCacheDialogWidget mClearCacheDialog; private ContextMenuWidget mContextMenu; + private SelectionActionWidget mSelectionMenu; private LibraryItemContextMenuWidget mLibraryItemContextMenu; private int mWidthBackup; private int mHeightBackup; @@ -114,7 +118,6 @@ default void onBookmarksHidden(WindowWidget aWindow) {} private Runnable mFirstDrawCallback; private boolean mIsInVRVideoMode; private View mView; - private Point mLastMouseClickPos; private SessionStack mSessionStack; private int mWindowId; private BookmarksView mBookmarksView; @@ -156,6 +159,7 @@ public WindowWidget(Context aContext, int windowId, boolean privateMode) { mSessionStack.addNavigationListener(this); mSessionStack.addProgressListener(this); mSessionStack.setHistoryDelegate(this); + mSessionStack.addSelectionActionListener(this); mSessionStack.newSession(); mBookmarksView = new BookmarksView(aContext); @@ -181,7 +185,6 @@ public WindowWidget(Context aContext, int windowId, boolean privateMode) { mTopBar = new TopBarWidget(aContext); mTopBar.attachToWindow(this); - mLastMouseClickPos = new Point(0, 0); mTitleBar = new TitleBarWidget(aContext); mTitleBar.attachToWindow(this); @@ -576,15 +579,6 @@ public void setActiveWindow(boolean active) { updateBorder(); } - private void hideContextMenus() { - if (mContextMenu != null && mContextMenu.isVisible()) { - mContextMenu.hide(REMOVE_WIDGET); - } - if (mLibraryItemContextMenu != null && mLibraryItemContextMenu.isVisible()) { - mLibraryItemContextMenu.hide(REMOVE_WIDGET); - } - } - private void updateTitleBar() { if (isBookmarksVisible()) { updateTitleBarUrl(getResources().getString(R.string.url_bookmarks_title)); @@ -733,7 +727,6 @@ public WidgetPlacement getPlacement() { @Override public void handleTouchEvent(MotionEvent aEvent) { - mLastMouseClickPos = new Point((int)aEvent.getX(), (int)aEvent.getY()); if (aEvent.getAction() == MotionEvent.ACTION_DOWN) { if (!mActive) { mClickedAfterFocus = true; @@ -882,6 +875,7 @@ public void releaseWidget() { mSessionStack.removeNavigationListener(this); mSessionStack.removeProgressListener(this); mSessionStack.setHistoryDelegate(null); + mSessionStack.removeSelectionActionListener(this); GeckoSession session = mSessionStack.getSession(mSessionId); if (mDisplay != null) { mDisplay.surfaceDestroyed(); @@ -1453,6 +1447,29 @@ public void dismiss() { return result; } + private void hideContextMenus() { + if (mContextMenu != null) { + mContextMenu.hide(REMOVE_WIDGET); + mContextMenu.releaseWidget(); + mContextMenu = null; + } + if (mSelectionMenu != null) { + mSelectionMenu.setDelegate((SelectionActionWidget.Delegate)null); + mSelectionMenu.hide(REMOVE_WIDGET); + mSelectionMenu.releaseWidget(); + mSelectionMenu = null; + } + + if (mWidgetPlacement.tintColor != 0xFFFFFFFF) { + mWidgetPlacement.tintColor = 0xFFFFFFFF; + mWidgetManager.updateWidget(this); + } + + if (mLibraryItemContextMenu != null && mLibraryItemContextMenu.isVisible()) { + mLibraryItemContextMenu.hide(REMOVE_WIDGET); + } + } + // GeckoSession.ContentDelegate @Override @@ -1463,8 +1480,12 @@ public void onContextMenu(GeckoSession session, int screenX, int screenY, Contex mContextMenu = new ContextMenuWidget(getContext()); mContextMenu.mWidgetPlacement.parentHandle = getHandle(); - mContextMenu.setContextElement(mLastMouseClickPos, element); + mContextMenu.setDismissCallback(this::hideContextMenus); + mContextMenu.setContextElement(element); mContextMenu.show(REQUEST_FOCUS); + + mWidgetPlacement.tintColor = 0x555555FF; + mWidgetManager.updateWidget(this); } @Override @@ -1586,4 +1607,39 @@ public void onSecurityChange(GeckoSession geckoSession, SecurityInformation secu } } + // GeckoSession.SelectionActionDelegate + + @Override + public void onShowActionRequest(@NonNull GeckoSession aSession, @NonNull Selection aSelection, @NonNull String[] aActions, @NonNull GeckoResponse aResponse) { + TelemetryWrapper.longPressContextMenuEvent(); + + hideContextMenus(); + mSelectionMenu = new SelectionActionWidget(getContext()); + mSelectionMenu.mWidgetPlacement.parentHandle = getHandle(); + mSelectionMenu.setActions(aActions); + Matrix matrix = new Matrix(); + aSession.getClientToSurfaceMatrix(matrix); + matrix.mapRect(aSelection.clientRect); + mSelectionMenu.setSelectionRect(aSelection.clientRect); + mSelectionMenu.setDelegate(new SelectionActionWidget.Delegate() { + @Override + public void onAction(String action) { + hideContextMenus(); + aResponse.respond(action); + } + + @Override + public void onDismiss() { + hideContextMenus(); + aResponse.respond(GeckoSession.SelectionActionDelegate.ACTION_UNSELECT); + } + }); + mSelectionMenu.show(KEEP_FOCUS); + } + + @Override + public void onHideAction(@NonNull GeckoSession aSession, int aHideReason) { + hideContextMenus(); + } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ContextMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ContextMenuWidget.java index 81d721107..b00e43cc3 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ContextMenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ContextMenuWidget.java @@ -5,99 +5,55 @@ package org.mozilla.vrbrowser.ui.widgets.dialogs; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; -import android.graphics.Point; -import android.graphics.PointF; -import android.util.AttributeSet; +import android.net.Uri; +import android.util.Log; import android.view.View; -import android.view.ViewTreeObserver; import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.browser.SettingsStore; -import org.mozilla.vrbrowser.ui.callbacks.ContextMenuClickCallback; -import org.mozilla.vrbrowser.ui.views.ContextMenu; -import org.mozilla.vrbrowser.ui.widgets.UIWidget; +import org.mozilla.vrbrowser.ui.widgets.MenuWidget; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.utils.StringUtils; import org.mozilla.vrbrowser.utils.ViewUtils; -public class ContextMenuWidget extends UIWidget implements WidgetManagerDelegate.FocusChangeListener { +import java.util.ArrayList; - private GeckoSession.ContentDelegate.ContextElement mContextElement; - private ContextMenu mContextMenu; - private int mMaxHeight; - private Point mMousePos; +public class ContextMenuWidget extends MenuWidget implements WidgetManagerDelegate.FocusChangeListener { + ArrayList mItems; + private Runnable mDismissCallback; public ContextMenuWidget(Context aContext) { super(aContext); - - mContextMenu = new ContextMenu(aContext); - initialize(); - } - - public ContextMenuWidget(Context aContext, AttributeSet aAttrs) { - super(aContext, aAttrs); - - mContextMenu = new ContextMenu(aContext, aAttrs); - initialize(); - } - - public ContextMenuWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { - super(aContext, aAttrs, aDefStyle); - - mContextMenu = new ContextMenu(aContext, aAttrs, aDefStyle); initialize(); } private void initialize() { - addView(mContextMenu); - mContextMenu.setContextMenuClickCallback(mContextMenuClickCallback); + mAdapter.updateBackgrounds(getContext().getDrawable(R.drawable.context_menu_item_background_first), + getContext().getDrawable(R.drawable.context_menu_item_background_last), + getContext().getDrawable(R.drawable.context_menu_item_background)); + mAdapter.updateLayourId(R.layout.context_menu_item); + menuContainer.setBackground(getContext().getDrawable(R.drawable.context_menu_background)); } @Override protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.visible = false; - aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.context_menu_row_width); - aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.context_menu_row_height); - aPlacement.parentAnchorX = 0.0f; - aPlacement.parentAnchorY = 1.0f; + aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.context_menu_row_width) + mBorderWidth * 2; + aPlacement.parentAnchorX = 0.5f; + aPlacement.parentAnchorY = 0.5f; aPlacement.anchorX = 0.5f; aPlacement.anchorY = 0.5f; - aPlacement.opaque = false; - aPlacement.cylinder = true; aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.context_menu_z_distance); } @Override public void show(@ShowFlags int aShowFlags) { mWidgetManager.addFocusChangeListener(ContextMenuWidget.this); - - mContextMenu.measure(View.MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mWidgetPlacement.width = (int)(mContextMenu.getMeasuredWidth()/mWidgetPlacement.density); - mWidgetPlacement.height = (int)(mContextMenu.getMeasuredHeight()/mWidgetPlacement.density); super.show(aShowFlags); - - ViewTreeObserver viewTreeObserver = mContextMenu.getViewTreeObserver(); - if (viewTreeObserver.isAlive()) { - viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - mContextMenu.getViewTreeObserver().removeOnGlobalLayoutListener(this); - PointF anchor = anchorForCurrentMousePosition(); - mWidgetPlacement.anchorX = anchor.x; - mWidgetPlacement.anchorY = anchor.y; - int paddingH = getPaddingStart() + getPaddingEnd(); - int paddingV = getPaddingTop() + getPaddingBottom(); - mWidgetPlacement.translationX = mMousePos.x * WidgetPlacement.worldToWindowRatio(getContext()); - mWidgetPlacement.translationY = -(mMousePos.y * WidgetPlacement.worldToWindowRatio(getContext())); - mWidgetPlacement.width = (int)((mContextMenu.getWidth()+paddingH*2)/mWidgetPlacement.density); - mWidgetPlacement.height = (int)((mContextMenu.getHeight()+paddingV*2)/mWidgetPlacement.density); - mWidgetManager.updateWidget(ContextMenuWidget.this); - } - }); - } } @Override @@ -109,78 +65,57 @@ public void hide(@HideFlags int aHideFlags) { @Override protected void onDismiss() { - hide(REMOVE_WIDGET); - } - - private final ContextMenuClickCallback mContextMenuClickCallback = contextMenuNode -> { - mWidgetManager.openNewWindow(mContextElement.linkUri); - hide(REMOVE_WIDGET); - }; - - public void setContextElement(Point mousePos, GeckoSession.ContentDelegate.ContextElement element) { - mMousePos = mousePos; - mContextElement = element; - - switch (mContextElement.type) { - case GeckoSession.ContentDelegate.ContextElement.TYPE_AUDIO: - mContextMenu.createAudioContextMenu(); - break; - - case GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE: - mContextMenu.createImageContextMenu(); - break; - - case GeckoSession.ContentDelegate.ContextElement.TYPE_NONE: - mContextMenu.createLinkContextMenu(); - break; - - case GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO: - mContextMenu.createVideoContextMenu(); - break; + if (mDismissCallback != null) { + mDismissCallback.run(); } } - private PointF anchorForCurrentMousePosition() { - float browserWindowWidth = SettingsStore.getInstance(getContext()).getWindowWidth(); - float browserWindowHeight = SettingsStore.getInstance(getContext()).getWindowHeight(); - float halfWidth = WidgetPlacement.convertPixelsToDp(getContext(), getWidth()); - float halfHeight = WidgetPlacement.convertPixelsToDp(getContext(), getHeight()); - if (mMousePos.x > (browserWindowWidth - halfWidth)) { - if (mMousePos.y < halfHeight) { - // Top Right - return new PointF(1.0f, 1.0f); - } else { - // Middle/Bottom Right - return new PointF(1.0f, 0.0f); - } + public void setDismissCallback(Runnable aCallback) { + mDismissCallback = aCallback; + } - } else if (mMousePos.x < (browserWindowWidth + halfWidth)) { - if (mMousePos.y < halfHeight) { - // Top Left - return new PointF(0.0f, 1.0f); - } else { - // Middle/Bottom Left - new PointF(0.0f, 0.0f); + public void setContextElement(GeckoSession.ContentDelegate.ContextElement aContextElement) { + mItems = new ArrayList<>(); + mItems.add(new MenuWidget.MenuItem(aContextElement.linkUri, 0, null)); + mItems.add(new MenuWidget.MenuItem(getContext().getString(R.string.context_menu_open_new_window), 0, () -> { + if (!StringUtils.isEmpty(aContextElement.linkUri)) { + mWidgetManager.openNewWindow(aContextElement.linkUri); } - - } else { - if (mMousePos.y < halfHeight) { - // Top Middle - return new PointF(1.0f, 1.0f); - } else if (mMousePos.y > (browserWindowHeight - halfHeight)) { - // Bottom Middle - return new PointF(0.0f, 0.0f); + onDismiss(); + })); + mItems.add(new MenuWidget.MenuItem(getContext().getString(R.string.context_menu_copy_link), 0, () -> { + ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); + if (aContextElement.linkUri != null) { + Uri uri = Uri.parse(aContextElement.linkUri); + if (uri != null) { + String label = aContextElement.title; + if (StringUtils.isEmpty(label)) { + label = aContextElement.altText; + } + if (StringUtils.isEmpty(label)) { + label = aContextElement.altText; + } + if (StringUtils.isEmpty(label)) { + label = aContextElement.linkUri; + } + ClipData clip = ClipData.newRawUri(label, uri); + clipboard.setPrimaryClip(clip); + } } - } + onDismiss(); + })); + updateMenuItems(mItems); - return new PointF(0.0f, 0.0f); + mWidgetPlacement.height = mItems.size() * WidgetPlacement.dpDimension(getContext(), R.dimen.context_menu_row_height); + mWidgetPlacement.height += mBorderWidth * 2; + mWidgetPlacement.height += 10.0f; // Link separator } // WidgetManagerDelegate.FocusChangeListener @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (!ViewUtils.isChildrenOf(mContextMenu, newFocus)) { + if (!ViewUtils.isChildrenOf(this, newFocus) && isVisible()) { onDismiss(); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SelectionActionWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SelectionActionWidget.java new file mode 100644 index 000000000..7b46f9e18 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SelectionActionWidget.java @@ -0,0 +1,193 @@ +package org.mozilla.vrbrowser.ui.widgets.dialogs; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.RectF; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.mozilla.geckoview.GeckoSession; +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.ui.views.UITextButton; +import org.mozilla.vrbrowser.ui.widgets.UIWidget; +import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; +import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.utils.StringUtils; +import org.mozilla.vrbrowser.utils.ViewUtils; + +import java.util.ArrayList; +import java.util.Arrays; + +import static android.view.Gravity.CENTER_VERTICAL; + +public class SelectionActionWidget extends UIWidget implements WidgetManagerDelegate.FocusChangeListener { + public interface Delegate { + void onAction(String action); + void onDismiss(); + } + + private Delegate mDelegate; + private Point mPosition; + private LinearLayout mContainer; + private int mMinButtonWidth; + private String[] mActions; + + public SelectionActionWidget(Context aContext) { + super(aContext); + initialize(); + } + + private void initialize() { + inflate(getContext(), R.layout.selection_action_menu, this); + mContainer = findViewById(R.id.selectionMenuContainer); + mMinButtonWidth = WidgetPlacement.pixelDimension(getContext(), R.dimen.autocompletion_widget_min_item_width); + mBackHandler = () -> { + onDismiss(); + }; + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.context_menu_row_width); + aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.context_menu_row_height); + aPlacement.parentAnchorX = 0.5f; + aPlacement.parentAnchorY = 0.5f; + aPlacement.anchorX = 0.5f; + aPlacement.anchorY = 0.5f; + aPlacement.translationX = 0.0f; + aPlacement.translationY = 0.0f; + aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.context_menu_z_distance); + aPlacement.visible = false; + } + + public void setDelegate(Delegate aDelegate) { + mDelegate = aDelegate; + } + + @Override + public void show(@ShowFlags int aShowFlags) { + mWidgetManager.addFocusChangeListener(this); + mWidgetPlacement.setSizeFromMeasure(getContext(), this); + if (mPosition != null) { + mWidgetPlacement.parentAnchorX = 0.0f; + mWidgetPlacement.parentAnchorY = 1.0f; + mWidgetPlacement.translationX = mPosition.x * WidgetPlacement.worldToWindowRatio(getContext()); + mWidgetPlacement.translationY = -mPosition.y * WidgetPlacement.worldToWindowRatio(getContext()); + mWidgetPlacement.translationY += mWidgetPlacement.height * 0.5f; + } + super.show(aShowFlags); + } + + @Override + public void hide(@HideFlags int aHideFlags) { + super.hide(aHideFlags); + mWidgetManager.removeFocusChangeListener(this); + } + + @Override + protected void onDismiss() { + if (mDelegate != null) { + mDelegate.onDismiss(); + } + } + + public void setSelectionRect(@Nullable RectF aRect) { + if (aRect != null) { + mPosition = new Point((int) aRect.centerX(), (int) aRect.top); + } else { + mPosition = null; + } + } + + public void setActions(@NonNull String[] aActions) { + mActions = aActions; + mContainer.removeAllViews(); + ArrayList buttons = new ArrayList<>(); + + if (StringUtils.contains(aActions, GeckoSession.SelectionActionDelegate.ACTION_CUT)) { + buttons.add(createButton(R.string.context_menu_cut_text, GeckoSession.SelectionActionDelegate.ACTION_CUT, this::handleAction)); + } + if (StringUtils.contains(aActions, GeckoSession.SelectionActionDelegate.ACTION_COPY)) { + buttons.add(createButton(R.string.context_menu_copy_text, GeckoSession.SelectionActionDelegate.ACTION_COPY, this::handleAction)); + } + if (StringUtils.contains(aActions, GeckoSession.SelectionActionDelegate.ACTION_PASTE)) { + buttons.add(createButton(R.string.context_menu_paste_text, GeckoSession.SelectionActionDelegate.ACTION_PASTE, this::handleAction)); + } + if (StringUtils.contains(aActions, GeckoSession.SelectionActionDelegate.ACTION_SELECT_ALL)) { + buttons.add(createButton(R.string.context_menu_select_all_text, GeckoSession.SelectionActionDelegate.ACTION_SELECT_ALL, this::handleAction)); + } + if (StringUtils.contains(aActions, GeckoSession.SelectionActionDelegate.ACTION_UNSELECT)) { + buttons.add(createButton(R.string.context_menu_unselect, GeckoSession.SelectionActionDelegate.ACTION_UNSELECT, this::handleAction)); + } + + for (int i = 0; i < buttons.size(); ++i) { + mContainer.addView(buttons.get(i)); + if (i < buttons.size() - 1) { + mContainer.addView(createSeparator()); + } + + int backgroundId = R.drawable.selection_menu_button; + if (buttons.size() == 1) { + backgroundId = R.drawable.selection_menu_button_single; + } else if (i == 0) { + backgroundId = R.drawable.selection_menu_button_first; + } else if (i == buttons.size() - 1) { + backgroundId = R.drawable.selection_menu_button_last; + } + buttons.get(i).setBackgroundDrawable(getContext().getDrawable(backgroundId)); + } + } + + public boolean hasAction(String aAction) { + return mActions != null && StringUtils.contains(mActions, aAction); + } + + public boolean hasSameActions(@NonNull String[] aActions) { + return Arrays.deepEquals(mActions, aActions); + } + + private UITextButton createButton(int aStringId, String aAction, OnClickListener aHandler) { + UITextButton button = new UITextButton(getContext(), null, R.attr.selectionActionButtonStyle); + button.setBackground(getContext().getDrawable(R.drawable.autocompletion_item_background)); + if (aHandler != null) { + button.setOnClickListener(aHandler); + } + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + button.setMinWidth(mMinButtonWidth); + params.gravity = CENTER_VERTICAL; + button.setLayoutParams(params); + button.setTag(aAction); + button.setText(getContext().getString(aStringId)); + + return button; + } + + private View createSeparator() { + View view = new View(getContext()); + float density = getContext().getResources().getDisplayMetrics().density; + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams((int)(1.5f * density), (int)(40.0f * density)); + params.gravity = CENTER_VERTICAL; + view.setLayoutParams(params); + view.setBackground(getContext().getDrawable(R.drawable.separator_background)); + return view; + } + + private void handleAction(View sender) { + if (mDelegate != null) { + mDelegate.onAction((String)sender.getTag()); + } + } + + // WidgetManagerDelegate.FocusChangeListener + + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + if (!ViewUtils.isChildrenOf(getChildAt(0), newFocus)) { + onDismiss(); + } + } +} \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/utils/StringUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/utils/StringUtils.java index 65ebf6558..4a1dee008 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/StringUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/StringUtils.java @@ -45,4 +45,26 @@ public static String removeLastCharacter(String aText) { } return ""; } + + public static String removeRange(@NonNull String aText, int aStart, int aEnd) { + String start = ""; + if (aStart > 0) { + start = aText.substring(0, aStart); + } + String end = ""; + if (aEnd < aText.length() - 1) { + end = aText.substring(aEnd); + } + return start + end; + } + + public static boolean contains(String[] aTarget, String aText) { + for (String str: aTarget) { + if (str.equals(aText)) { + return true; + } + } + + return false; + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/utils/ViewUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/utils/ViewUtils.java index 62b681221..1e06d9389 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/ViewUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/ViewUtils.java @@ -3,6 +3,7 @@ import android.graphics.Color; import android.os.Build; import android.text.Html; +import android.text.Layout; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.method.LinkMovementMethod; @@ -12,6 +13,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.widget.EditText; import android.widget.TextView; import androidx.annotation.NonNull; @@ -122,4 +124,42 @@ public static boolean isInsideView(@NotNull View view, int rx, int ry) { public static int ARGBtoRGBA(int c) { return (c & 0x00FFFFFF) << 8 | (c & 0xFF000000) >>> 24; } + + public static float GetLetterPositionX(@NonNull TextView aView, int aLetterIndex, boolean aClamp) { + Layout layout = aView.getLayout(); + if (layout == null) { + return 0; + } + float x = layout.getPrimaryHorizontal(aLetterIndex); + x += aView.getPaddingLeft(); + x -= aView.getScrollX(); + if (aClamp && x > (aView.getMeasuredWidth() - aView.getPaddingRight())) { + x = aView.getMeasuredWidth() - aView.getPaddingRight(); + } + if (aClamp && x < aView.getPaddingLeft()) { + x = aView.getPaddingLeft(); + } + return x; + } + + + public static int getCursorOffset(@NonNull EditText aView, float aX) { + Layout layout = aView.getLayout(); + if (layout != null) { + float x = aX + aView.getScrollX() - aView.getPaddingLeft(); + return layout.getOffsetForHorizontal(0, x); + } + + return -1; + } + + public static void placeSelection(@NonNull EditText aView, int offset1, int offset2) { + if (offset1 < 0 || offset2 < 0 || offset1 == offset2) { + return; + } + + int start = Math.min(offset1, offset2); + int end = Math.max(offset1, offset2); + aView.setSelection(start, end); + } } diff --git a/app/src/main/cpp/Widget.cpp b/app/src/main/cpp/Widget.cpp index 22ee3d260..515db5c5c 100644 --- a/app/src/main/cpp/Widget.cpp +++ b/app/src/main/cpp/Widget.cpp @@ -110,7 +110,7 @@ struct Widget::State { surface = vrb::TextureSurface::Create(render, name); } - vrb::Color tintColor(1.0f, 1.0f, 1.0f, 1.0f); + vrb::Color tintColor = placement->GetTintColor(); std::string customFragment; if (!placement->composited && placement->GetClearColor().Alpha() > 0.0f) { customFragment = @@ -467,6 +467,11 @@ Widget::SetPlacement(const WidgetPlacementPtr& aPlacement) { if (layer) { layer->SetName(aPlacement->name); layer->SetClearColor(aPlacement->clearColor); + layer->SetTintColor(aPlacement->tintColor); + } else if (m.quad && aPlacement->composited) { + m.quad->SetTintColor(aPlacement->GetTintColor()); + } else if (m.cylinder && aPlacement->composited) { + m.cylinder->SetTintColor(aPlacement->GetTintColor()); } } diff --git a/app/src/main/cpp/WidgetPlacement.cpp b/app/src/main/cpp/WidgetPlacement.cpp index caddfb6d4..542f387ef 100644 --- a/app/src/main/cpp/WidgetPlacement.cpp +++ b/app/src/main/cpp/WidgetPlacement.cpp @@ -70,6 +70,7 @@ WidgetPlacement::FromJava(JNIEnv* aEnv, jobject& aObject) { GET_FLOAT_FIELD(textureScale, "textureScale"); GET_BOOLEAN_FIELD(cylinder); GET_FLOAT_FIELD(cylinderMapRadius, "cylinderMapRadius"); + GET_INT_FIELD(tintColor); GET_INT_FIELD(borderColor); GET_STRING_FIELD(name); GET_INT_FIELD(clearColor); @@ -97,4 +98,9 @@ WidgetPlacement::GetClearColor() const { return vrb::Color(clearColor); } +vrb::Color +WidgetPlacement::GetTintColor() const { + return vrb::Color(tintColor); +} + } diff --git a/app/src/main/cpp/WidgetPlacement.h b/app/src/main/cpp/WidgetPlacement.h index ec52f5f09..966398a27 100644 --- a/app/src/main/cpp/WidgetPlacement.h +++ b/app/src/main/cpp/WidgetPlacement.h @@ -36,12 +36,14 @@ struct WidgetPlacement { float textureScale; bool cylinder; float cylinderMapRadius; + int tintColor; int borderColor; std::string name; int clearColor; int32_t GetTextureWidth() const; int32_t GetTextureHeight() const; + vrb::Color GetTintColor() const; vrb::Color GetClearColor() const; static const float kWorldDPIRatio; diff --git a/app/src/main/res/drawable/context_menu_background.xml b/app/src/main/res/drawable/context_menu_background.xml index 984f1f525..6c68c608a 100644 --- a/app/src/main/res/drawable/context_menu_background.xml +++ b/app/src/main/res/drawable/context_menu_background.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/context_menu_item_background.xml b/app/src/main/res/drawable/context_menu_item_background.xml new file mode 100644 index 000000000..c0f1895fc --- /dev/null +++ b/app/src/main/res/drawable/context_menu_item_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/context_menu_item_background_first.xml b/app/src/main/res/drawable/context_menu_item_background_first.xml new file mode 100644 index 000000000..fd8fccfc0 --- /dev/null +++ b/app/src/main/res/drawable/context_menu_item_background_first.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/context_menu_item_background_last.xml b/app/src/main/res/drawable/context_menu_item_background_last.xml new file mode 100644 index 000000000..9b021ccd9 --- /dev/null +++ b/app/src/main/res/drawable/context_menu_item_background_last.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/selection_menu_background.xml b/app/src/main/res/drawable/selection_menu_background.xml new file mode 100644 index 000000000..fab616db3 --- /dev/null +++ b/app/src/main/res/drawable/selection_menu_background.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selection_menu_background_triangle.xml b/app/src/main/res/drawable/selection_menu_background_triangle.xml new file mode 100644 index 000000000..916f45f60 --- /dev/null +++ b/app/src/main/res/drawable/selection_menu_background_triangle.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/selection_menu_button.xml b/app/src/main/res/drawable/selection_menu_button.xml new file mode 100644 index 000000000..0ef0d4dba --- /dev/null +++ b/app/src/main/res/drawable/selection_menu_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/selection_menu_button_first.xml b/app/src/main/res/drawable/selection_menu_button_first.xml new file mode 100644 index 000000000..dc9e4c500 --- /dev/null +++ b/app/src/main/res/drawable/selection_menu_button_first.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/selection_menu_button_last.xml b/app/src/main/res/drawable/selection_menu_button_last.xml new file mode 100644 index 000000000..5c3fdcf99 --- /dev/null +++ b/app/src/main/res/drawable/selection_menu_button_last.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/selection_menu_button_single.xml b/app/src/main/res/drawable/selection_menu_button_single.xml new file mode 100644 index 000000000..6cf0331d9 --- /dev/null +++ b/app/src/main/res/drawable/selection_menu_button_single.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/context_menu_item.xml b/app/src/main/res/layout/context_menu_item.xml index 35c5c1e64..491a60757 100644 --- a/app/src/main/res/layout/context_menu_item.xml +++ b/app/src/main/res/layout/context_menu_item.xml @@ -1,62 +1,30 @@ - - + - - - - - - - + android:textColor="@color/fog" + android:layout_marginStart="8dp" + android:singleLine="true" /> - + - - - + diff --git a/app/src/main/res/layout/menu.xml b/app/src/main/res/layout/menu.xml index 25ea87654..4fed6d5b6 100644 --- a/app/src/main/res/layout/menu.xml +++ b/app/src/main/res/layout/menu.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> - diff --git a/app/src/main/res/layout/selection_action_menu.xml b/app/src/main/res/layout/selection_action_menu.xml new file mode 100644 index 000000000..5ef13886c --- /dev/null +++ b/app/src/main/res/layout/selection_action_menu.xml @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 0b9ba0ade..dff96aa87 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -197,7 +197,7 @@ 250dp 1.25 0.15 - 0.2 + 0.1 10dp 8dp @@ -221,8 +221,9 @@ 120dp - 360dp - 72dp + 400dp + 60dp + 20dp 0.1 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13e80b23a..f0d375409 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -859,6 +859,24 @@ Open in a new window. + + Copy link + + + Cut + + + Copy + + + Paste + + + Select All + + + Unselect + Bookmark this page diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 24074507e..1122ff535 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -10,6 +10,7 @@ + @@ -137,6 +138,19 @@ @drawable/main_button_icon_color_private + +