mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
30a888d13c
Backed out changeset 14f00ee7be7d (bug 897711) Backed out changeset 3de21c283716 (bug 815682)
553 lines
19 KiB
Java
553 lines
19 KiB
Java
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.gecko.prompts;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
import org.mozilla.gecko.GeckoAppShell;
|
|
import org.mozilla.gecko.GeckoEvent;
|
|
import org.mozilla.gecko.R;
|
|
import org.mozilla.gecko.util.ThreadUtils;
|
|
import org.mozilla.gecko.Tab;
|
|
import org.mozilla.gecko.Tabs;
|
|
|
|
import android.app.AlertDialog;
|
|
import android.content.Context;
|
|
import android.content.DialogInterface;
|
|
import android.content.DialogInterface.OnCancelListener;
|
|
import android.content.DialogInterface.OnClickListener;
|
|
import android.content.res.Resources;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.drawable.BitmapDrawable;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.widget.AdapterView;
|
|
import android.widget.AdapterView.OnItemClickListener;
|
|
import android.widget.ArrayAdapter;
|
|
import android.widget.CheckedTextView;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.ListView;
|
|
import android.widget.ScrollView;
|
|
import android.widget.TextView;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
public class Prompt implements OnClickListener, OnCancelListener, OnItemClickListener,
|
|
PromptInput.OnChangeListener, Tabs.OnTabsChangedListener {
|
|
private static final String LOGTAG = "GeckoPromptService";
|
|
|
|
private String[] mButtons;
|
|
private PromptInput[] mInputs;
|
|
private AlertDialog mDialog;
|
|
|
|
private final LayoutInflater mInflater;
|
|
private final Context mContext;
|
|
private PromptCallback mCallback;
|
|
private String mGuid;
|
|
private PromptListAdapter mAdapter;
|
|
|
|
private static boolean mInitialized;
|
|
private static int mInputPaddingSize;
|
|
|
|
private int mTabId = Tabs.INVALID_TAB_ID;
|
|
|
|
public Prompt(Context context, PromptCallback callback) {
|
|
this(context);
|
|
mCallback = callback;
|
|
}
|
|
|
|
private Prompt(Context context) {
|
|
mContext = context;
|
|
mInflater = LayoutInflater.from(mContext);
|
|
|
|
if (!mInitialized) {
|
|
Resources res = mContext.getResources();
|
|
mInputPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_inputs_padding));
|
|
mInitialized = true;
|
|
}
|
|
}
|
|
|
|
private View applyInputStyle(View view, PromptInput input) {
|
|
// Don't add padding to color picker views
|
|
if (input.canApplyInputStyle()) {
|
|
view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0);
|
|
}
|
|
return view;
|
|
}
|
|
|
|
public void show(JSONObject message) {
|
|
String title = message.optString("title");
|
|
String text = message.optString("text");
|
|
mGuid = message.optString("guid");
|
|
|
|
mButtons = getStringArray(message, "buttons");
|
|
|
|
JSONArray inputs = getSafeArray(message, "inputs");
|
|
mInputs = new PromptInput[inputs.length()];
|
|
for (int i = 0; i < mInputs.length; i++) {
|
|
try {
|
|
mInputs[i] = PromptInput.getInput(inputs.getJSONObject(i));
|
|
mInputs[i].setListener(this);
|
|
} catch(Exception ex) { }
|
|
}
|
|
|
|
PromptListItem[] menuitems = PromptListItem.getArray(message.optJSONArray("listitems"));
|
|
String selected = message.optString("choiceMode");
|
|
|
|
int choiceMode = ListView.CHOICE_MODE_NONE;
|
|
if ("single".equals(selected)) {
|
|
choiceMode = ListView.CHOICE_MODE_SINGLE;
|
|
} else if ("multiple".equals(selected)) {
|
|
choiceMode = ListView.CHOICE_MODE_MULTIPLE;
|
|
}
|
|
|
|
if (message.has("tabId")) {
|
|
mTabId = message.optInt("tabId", Tabs.INVALID_TAB_ID);
|
|
}
|
|
|
|
show(title, text, menuitems, choiceMode);
|
|
}
|
|
|
|
public void show(String title, String text, PromptListItem[] listItems, int choiceMode) {
|
|
ThreadUtils.assertOnUiThread();
|
|
|
|
try {
|
|
create(title, text, listItems, choiceMode);
|
|
} catch(IllegalStateException ex) {
|
|
Log.i(LOGTAG, "Error building dialog", ex);
|
|
return;
|
|
}
|
|
|
|
if (mTabId != Tabs.INVALID_TAB_ID) {
|
|
Tabs.registerOnTabsChangedListener(this);
|
|
|
|
final Tab tab = Tabs.getInstance().getTab(mTabId);
|
|
if (Tabs.getInstance().getSelectedTab() == tab) {
|
|
mDialog.show();
|
|
}
|
|
} else {
|
|
mDialog.show();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) {
|
|
if (tab != Tabs.getInstance().getTab(mTabId)) {
|
|
return;
|
|
}
|
|
|
|
switch(msg) {
|
|
case SELECTED:
|
|
Log.i(LOGTAG, "Selected");
|
|
mDialog.show();
|
|
break;
|
|
case UNSELECTED:
|
|
Log.i(LOGTAG, "Unselected");
|
|
mDialog.hide();
|
|
break;
|
|
case LOCATION_CHANGE:
|
|
Log.i(LOGTAG, "Location change");
|
|
mDialog.cancel();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void create(String title, String text, PromptListItem[] listItems, int choiceMode)
|
|
throws IllegalStateException {
|
|
GeckoAppShell.getLayerView().abortPanning();
|
|
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
|
|
if (!TextUtils.isEmpty(title)) {
|
|
// Long strings can delay showing the dialog, so we cap the number of characters shown to 256.
|
|
builder.setTitle(title.substring(0, Math.min(title.length(), 256)));
|
|
}
|
|
|
|
if (!TextUtils.isEmpty(text)) {
|
|
builder.setMessage(text);
|
|
}
|
|
|
|
// Because lists are currently added through the normal Android AlertBuilder interface, they're
|
|
// incompatible with also adding additional input elements to a dialog.
|
|
if (listItems != null && listItems.length > 0) {
|
|
addListItems(builder, listItems, choiceMode);
|
|
} else if (!addInputs(builder)) {
|
|
throw new IllegalStateException("Could not add inputs to dialog");
|
|
}
|
|
|
|
int length = mButtons == null ? 0 : mButtons.length;
|
|
if (length > 0) {
|
|
builder.setPositiveButton(mButtons[0], this);
|
|
if (length > 1) {
|
|
builder.setNeutralButton(mButtons[1], this);
|
|
if (length > 2) {
|
|
builder.setNegativeButton(mButtons[2], this);
|
|
}
|
|
}
|
|
}
|
|
|
|
mDialog = builder.create();
|
|
mDialog.setOnCancelListener(Prompt.this);
|
|
}
|
|
|
|
public void setButtons(String[] buttons) {
|
|
mButtons = buttons;
|
|
}
|
|
|
|
public void setInputs(PromptInput[] inputs) {
|
|
mInputs = inputs;
|
|
}
|
|
|
|
/* Adds to a result value from the lists that can be shown in dialogs.
|
|
* Will set the selected value(s) to the button attribute of the
|
|
* object that's passed in. If this is a multi-select dialog, sets a
|
|
* selected attribute to an array of booleans.
|
|
*/
|
|
private void addListResult(final JSONObject result, int which) {
|
|
if (mAdapter == null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
JSONArray selected = new JSONArray();
|
|
|
|
// If the button has already been filled in
|
|
ArrayList<Integer> selectedItems = mAdapter.getSelected();
|
|
for (Integer item : selectedItems) {
|
|
selected.put(item);
|
|
}
|
|
|
|
// If we haven't assigned a button yet, or we assigned it to -1, assign the which
|
|
// parameter to both selected and the button.
|
|
if (!result.has("button") || result.optInt("button") == -1) {
|
|
if (!selectedItems.contains(which)) {
|
|
selected.put(which);
|
|
}
|
|
|
|
result.put("button", which);
|
|
}
|
|
|
|
result.put("list", selected);
|
|
} catch(JSONException ex) { }
|
|
}
|
|
|
|
/* Adds to a result value from the inputs that can be shown in dialogs.
|
|
* Each input will set its own value in the result.
|
|
*/
|
|
private void addInputValues(final JSONObject result) {
|
|
try {
|
|
if (mInputs != null) {
|
|
for (int i = 0; i < mInputs.length; i++) {
|
|
result.put(mInputs[i].getId(), mInputs[i].getValue());
|
|
}
|
|
}
|
|
} catch(JSONException ex) { }
|
|
}
|
|
|
|
/* Adds the selected button to a result. This should only be called if there
|
|
* are no lists shown on the dialog, since they also write their results to the button
|
|
* attribute.
|
|
*/
|
|
private void addButtonResult(final JSONObject result, int which) {
|
|
int button = -1;
|
|
switch(which) {
|
|
case DialogInterface.BUTTON_POSITIVE : button = 0; break;
|
|
case DialogInterface.BUTTON_NEUTRAL : button = 1; break;
|
|
case DialogInterface.BUTTON_NEGATIVE : button = 2; break;
|
|
}
|
|
try {
|
|
result.put("button", button);
|
|
} catch(JSONException ex) { }
|
|
}
|
|
|
|
@Override
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
ThreadUtils.assertOnUiThread();
|
|
closeDialog(which);
|
|
}
|
|
|
|
/* Adds a set of list items to the prompt. This can be used for either context menu type dialogs, checked lists,
|
|
* or multiple selection lists.
|
|
*
|
|
* @param builder
|
|
* The alert builder currently building this dialog.
|
|
* @param listItems
|
|
* The items to add.
|
|
* @param choiceMode
|
|
* One of the ListView.CHOICE_MODE constants to designate whether this list shows checkmarks, radios buttons, or nothing.
|
|
*/
|
|
private void addListItems(AlertDialog.Builder builder, PromptListItem[] listItems, int choiceMode) {
|
|
switch(choiceMode) {
|
|
case ListView.CHOICE_MODE_MULTIPLE_MODAL:
|
|
case ListView.CHOICE_MODE_MULTIPLE:
|
|
addMultiSelectList(builder, listItems);
|
|
break;
|
|
case ListView.CHOICE_MODE_SINGLE:
|
|
addSingleSelectList(builder, listItems);
|
|
break;
|
|
case ListView.CHOICE_MODE_NONE:
|
|
default:
|
|
addMenuList(builder, listItems);
|
|
}
|
|
}
|
|
|
|
/* Shows a multi-select list with checkmarks on the side. Android doesn't support using an adapter for
|
|
* multi-choice lists by default so instead we insert our own custom list so that we can do fancy things
|
|
* to the rows like disabling/indenting them.
|
|
*
|
|
* @param builder
|
|
* The alert builder currently building this dialog.
|
|
* @param listItems
|
|
* The items to add.
|
|
*/
|
|
private void addMultiSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) {
|
|
ListView listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null);
|
|
listView.setOnItemClickListener(this);
|
|
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
|
|
|
mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems);
|
|
listView.setAdapter(mAdapter);
|
|
builder.setView(listView);
|
|
}
|
|
|
|
/* Shows a single-select list with radio boxes on the side.
|
|
*
|
|
* @param builder
|
|
* the alert builder currently building this dialog.
|
|
* @param listItems
|
|
* The items to add.
|
|
*/
|
|
private void addSingleSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) {
|
|
mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems);
|
|
builder.setSingleChoiceItems(mAdapter, mAdapter.getSelectedIndex(), new DialogInterface.OnClickListener() {
|
|
@Override
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
// The adapter isn't aware of single vs. multi choice lists, so manually
|
|
// clear any other selected items first.
|
|
ArrayList<Integer> selected = mAdapter.getSelected();
|
|
for (Integer sel : selected) {
|
|
mAdapter.toggleSelected(sel);
|
|
}
|
|
|
|
// Now select this item.
|
|
mAdapter.toggleSelected(which);
|
|
closeIfNoButtons(which);
|
|
}
|
|
});
|
|
}
|
|
|
|
/* Shows a single-select list.
|
|
*
|
|
* @param builder
|
|
* the alert builder currently building this dialog.
|
|
* @param listItems
|
|
* The items to add.
|
|
*/
|
|
private void addMenuList(AlertDialog.Builder builder, PromptListItem[] listItems) {
|
|
mAdapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems);
|
|
builder.setAdapter(mAdapter, this);
|
|
}
|
|
|
|
/* Wraps an input in a linearlayout. We do this so that we can set padding that appears outside the background
|
|
* drawable for the view.
|
|
*/
|
|
private View wrapInput(final PromptInput input) {
|
|
final LinearLayout linearLayout = new LinearLayout(mContext);
|
|
linearLayout.setOrientation(LinearLayout.VERTICAL);
|
|
applyInputStyle(linearLayout, input);
|
|
|
|
linearLayout.addView(input.getView(mContext));
|
|
|
|
return linearLayout;
|
|
}
|
|
|
|
/* Add the requested input elements to the dialog.
|
|
*
|
|
* @param builder
|
|
* the alert builder currently building this dialog.
|
|
* @return
|
|
* return true if the inputs were added successfully. This may fail
|
|
* if the requested input is compatible with this Android verison
|
|
*/
|
|
private boolean addInputs(AlertDialog.Builder builder) {
|
|
int length = mInputs == null ? 0 : mInputs.length;
|
|
if (length == 0) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
View root = null;
|
|
boolean scrollable = false; // If any of the innuts are scrollable, we won't wrap this in a ScrollView
|
|
|
|
if (length == 1) {
|
|
root = wrapInput(mInputs[0]);
|
|
scrollable |= mInputs[0].getScrollable();
|
|
} else if (length > 1) {
|
|
LinearLayout linearLayout = new LinearLayout(mContext);
|
|
linearLayout.setOrientation(LinearLayout.VERTICAL);
|
|
for (int i = 0; i < length; i++) {
|
|
View content = wrapInput(mInputs[i]);
|
|
linearLayout.addView(content);
|
|
scrollable |= mInputs[i].getScrollable();
|
|
}
|
|
root = linearLayout;
|
|
}
|
|
|
|
if (scrollable) {
|
|
// If we're showing some sort of scrollable list, force an inverse background.
|
|
builder.setInverseBackgroundForced(true);
|
|
builder.setView(root);
|
|
} else {
|
|
ScrollView view = new ScrollView(mContext);
|
|
view.addView(root);
|
|
builder.setView(view);
|
|
}
|
|
} catch(Exception ex) {
|
|
Log.e(LOGTAG, "Error showing prompt inputs", ex);
|
|
// We cannot display these input widgets with this sdk version,
|
|
// do not display any dialog and finish the prompt now.
|
|
cancelDialog();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* AdapterView.OnItemClickListener
|
|
* Called when a list item is clicked
|
|
*/
|
|
@Override
|
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
|
ThreadUtils.assertOnUiThread();
|
|
mAdapter.toggleSelected(position);
|
|
|
|
// If there are no buttons on this dialog, then we take selecting an item as a sign to close
|
|
// the dialog. Note that means it will be hard to select multiple things in this list, but
|
|
// given there is no way to confirm+close the dialog, it seems reasonable.
|
|
closeIfNoButtons(position);
|
|
}
|
|
|
|
private boolean closeIfNoButtons(int selected) {
|
|
ThreadUtils.assertOnUiThread();
|
|
if (mButtons == null || mButtons.length == 0) {
|
|
closeDialog(selected);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* @DialogInterface.OnCancelListener
|
|
* Called when the user hits back to cancel a dialog. The dialog will close itself when this
|
|
* ends. Setup the correct return values here.
|
|
*
|
|
* @param aDialog
|
|
* A dialog interface for the dialog that's being closed.
|
|
*/
|
|
@Override
|
|
public void onCancel(DialogInterface aDialog) {
|
|
ThreadUtils.assertOnUiThread();
|
|
cancelDialog();
|
|
}
|
|
|
|
/* Called in situations where we want to cancel the dialog . This can happen if the user hits back,
|
|
* or if the dialog can't be created because of invalid JSON.
|
|
*/
|
|
private void cancelDialog() {
|
|
JSONObject ret = new JSONObject();
|
|
try {
|
|
ret.put("button", -1);
|
|
} catch(Exception ex) { }
|
|
addInputValues(ret);
|
|
notifyClosing(ret);
|
|
}
|
|
|
|
/* Called any time we're closing the dialog to cleanup and notify listeners that the dialog
|
|
* is closing.
|
|
*/
|
|
private void closeDialog(int which) {
|
|
JSONObject ret = new JSONObject();
|
|
mDialog.dismiss();
|
|
|
|
addButtonResult(ret, which);
|
|
addListResult(ret, which);
|
|
addInputValues(ret);
|
|
|
|
notifyClosing(ret);
|
|
}
|
|
|
|
/* Called any time we're closing the dialog to cleanup and notify listeners that the dialog
|
|
* is closing.
|
|
*/
|
|
private void notifyClosing(JSONObject aReturn) {
|
|
try {
|
|
aReturn.put("guid", mGuid);
|
|
} catch(JSONException ex) { }
|
|
|
|
if (mTabId != Tabs.INVALID_TAB_ID) {
|
|
Tabs.unregisterOnTabsChangedListener(this);
|
|
}
|
|
|
|
// poke the Gecko thread in case it's waiting for new events
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent());
|
|
|
|
if (mCallback != null) {
|
|
mCallback.onPromptFinished(aReturn.toString());
|
|
}
|
|
}
|
|
|
|
// Called when the prompt inputs on the dialog change
|
|
@Override
|
|
public void onChange(PromptInput input) {
|
|
// If there are no buttons on this dialog, assuming that "changing" an input
|
|
// means something was selected and we can close. This provides a way to tap
|
|
// on a list item and close the dialog automatically.
|
|
closeIfNoButtons(-1);
|
|
}
|
|
|
|
private static JSONArray getSafeArray(JSONObject json, String key) {
|
|
try {
|
|
return json.getJSONArray(key);
|
|
} catch (Exception e) {
|
|
return new JSONArray();
|
|
}
|
|
}
|
|
|
|
public static String[] getStringArray(JSONObject aObject, String aName) {
|
|
JSONArray items = getSafeArray(aObject, aName);
|
|
int length = items.length();
|
|
String[] list = new String[length];
|
|
for (int i = 0; i < length; i++) {
|
|
try {
|
|
list[i] = items.getString(i);
|
|
} catch(Exception ex) { }
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private static boolean[] getBooleanArray(JSONObject aObject, String aName) {
|
|
JSONArray items = new JSONArray();
|
|
try {
|
|
items = aObject.getJSONArray(aName);
|
|
} catch(Exception ex) { return null; }
|
|
int length = items.length();
|
|
boolean[] list = new boolean[length];
|
|
for (int i = 0; i < length; i++) {
|
|
try {
|
|
list[i] = items.getBoolean(i);
|
|
} catch(Exception ex) { }
|
|
}
|
|
return list;
|
|
}
|
|
|
|
public interface PromptCallback {
|
|
public void onPromptFinished(String jsonResult);
|
|
}
|
|
}
|