/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see .
*/
package com.itsaky.androidide.utils;
import static com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder;
import android.content.Context;
import android.content.DialogInterface;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.colorpicker.ColorPickerView;
import com.itsaky.androidide.databinding.LayoutDimensionAttrEditorBinding;
import com.itsaky.androidide.databinding.LayoutTextAttrEditorBinding;
import com.itsaky.toaster.Toaster;
import java.util.ArrayList;
import java.util.regex.Pattern;
/**
* Helper class for creating dialogs for editing view attributes.
*
* TODO Should we merge this into {@link DialogUtils}?
* TODO An attribute can accept multiple types of values (dimension, enum, reference, etc)
* Can we somehow combine the editors according to attribute format?
* @author Akash Yadav
*/
public class AttributeDialogs {
private static final String DIMENSION_MATCH = "match_parent";
private static final String DIMENSION_WRAP = "wrap_content";
private static final Logger LOG = Logger.instance ("AttributeDialogs");
/**
* Context that will be used for creating dialogs.
* This must be initialized by calling {@link #init(Context)}
* and the reference must be released by calling {@link #release()}
*/
private static Context mContext;
/**
* Initialize this class with the provided context.
* @param ctx The context that will be used for creating dialogs.
* @see #release()
*/
public static void init (Context ctx) {
mContext = ctx;
}
/**
* Release the context instance stored in this class.
*
* @see #init(Context)
*/
public static void release () {
mContext = null;
}
@NonNull
public static AlertDialog booleanEditor (OnClickListener listener) {
final var items = new String [] {"true", "false"};
final var builder = newMaterialDialogBuilder (mContext);
builder.setTitle (mContext.getString(R.string.msg_choose_new_val));
builder.setItems (items, (dialog, which) -> {
dialog.dismiss ();
final var val = items [which];
if (listener != null) {
listener.onClick (dialog, which, val);
}
});
return builder.create ();
}
/**
* Create a new dialog for editing an attribute with format STRING.
* @param listener The listener that will be invoked when the user clicks the
* positive button.
* @return The newly created dialog instance.
*/
@NonNull
public static AlertDialog stringEditor (OnClickListener listener) {
final var binding = LayoutTextAttrEditorBinding.inflate (LayoutInflater.from (mContext));
final var builder = newMaterialDialogBuilder (mContext);
builder.setTitle (mContext.getString(R.string.msg_new_str_value));
builder.setView (binding.getRoot ());
builder.setPositiveButton (android.R.string.ok, (dialog, which) -> {
final var newValue = binding.valueField.getEditText ().getText ().toString ().trim ();
if (newValue.length () <= 0) {
StudioApp.getInstance ().toast (mContext.getString (R.string.msg_invalid_attr_value), Toaster.Type.ERROR);
return;
}
if (listener != null) {
listener.onClick (dialog, which, newValue);
}
});
builder.setNegativeButton (android.R.string.cancel, (dialog, which) -> dialog.dismiss ());
return builder.create ();
}
/**
* Create a new dialog for editing an attribute which accepts dimension values.
* @param value The current value of the attribute. Used for determining which item must be selected by default.
* @param listener A listener that will be invoked when the user presses the positive button of the dialog.
* This listener is invoked only when the user passes a valid dimension value.
* @return The newly created dialog instance. Note that this dialog is not shown by default.
*/
@NonNull
public static AlertDialog dimensionEditor (CharSequence value, OnClickListener listener) {
final var binding = LayoutDimensionAttrEditorBinding.inflate(LayoutInflater.from (mContext));
binding.choices.setOnCheckedChangeListener ((group, checkedId) -> binding.otherValue.setVisibility (binding.other.isChecked () ? View.VISIBLE : View.GONE));
if (DIMENSION_MATCH.contentEquals (value)) {
binding.choices.check (R.id.match);
} else if (DIMENSION_WRAP.contentEquals (value)) {
binding.choices.check (R.id.wrap);
} else {
binding.choices.check (R.id.other);
}
final var builder = newMaterialDialogBuilder (mContext);
builder.setView (binding.getRoot ());
builder.setPositiveButton (android.R.string.ok, ((dialog, which) -> {
var val = "";
if (binding.choices.getCheckedRadioButtonId () == R.id.match) {
val = DIMENSION_MATCH;
} else if (binding.choices.getCheckedRadioButtonId () == R.id.wrap) {
val = DIMENSION_WRAP;
} else {
if (binding.otherValue.getEditText () != null) {
val = binding.otherValue.getEditText ().getText ().toString ().trim ();
} else {
val = "";
}
}
if (val.length () <= 0) {
StudioApp.getInstance ().toast (mContext.getString(R.string.msg_invalid_attr_value), Toaster.Type.ERROR);
return;
}
dialog.dismiss ();
if (listener != null) {
listener.onClick (dialog, which, val);
}
}));
builder.setNegativeButton (android.R.string.cancel, (dialog, which) -> dialog.dismiss ());
return builder.create ();
}
/**
* @see #enumEditor(CharSequence[], int, OnClickListener)
*/
public static AlertDialog enumEditor (CharSequence[] values, CharSequence value, OnClickListener listener) {
return enumEditor (values, findIndexOf (values, value), listener);
}
private static int findIndexOf (@NonNull CharSequence[] values, CharSequence value) {
for (int i=0;i {
dialog.dismiss ();
final var val = values [which].toString ();
if (listener != null) {
listener.onClick (dialog, which, val);
}
});
return builder.show ();
}
/**
* Creates a new attribute editor dialog to edit flag attributes.
* @param values The possible values.
* @param value The current value of the attribute.
* @param listener The listener that will be invoked when the user presses the positive button.
* @return The newly created dialog instance.
*/
@NonNull
public static AlertDialog flagEditor (@NonNull CharSequence[] values, @NonNull CharSequence value, OnClickListener listener) {
final var checked = new boolean [values.length];
final var vals = value.toString ().split (Pattern.quote ("|"));
for (var i=0;i ();
for (var i=0;i {
if (isChecked) {
checked.add (values[which]);
} else {
checked.remove (values[which]);
}
});
builder.setPositiveButton (android.R.string.ok, (dialog, which) -> {
if (checked.size () <= 0) {
StudioApp.getInstance ().toast (mContext.getString(R.string.msg_select_one_flag), Toaster.Type.ERROR);
return;
}
final var val = TextUtils.join ("|", checked);
if (listener != null) {
listener.onClick (dialog, which, val);
}
});
builder.setNegativeButton (android.R.string.cancel, (dialog, which) -> dialog.dismiss ());
return builder.create ();
}
@NonNull
public static AlertDialog colorPicker (final ColorPickerView.OnPickListener listener) {
final var view = new ColorPickerView (mContext);
final var builder = newMaterialDialogBuilder (mContext);
builder.setView (view);
final var dialog = builder.create ();
dialog.getWindow ().setSoftInputMode (WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
view.setOnPickListener ((color, hexCode) -> {
dialog.dismiss ();
if (listener != null) {
listener.onPick (color, hexCode);
}
});
return dialog;
}
public interface OnClickListener {
void onClick (DialogInterface dialog, int which, String newValue);
}
}