diff --git a/app/src/main/java/org/solovyev/android/calculator/BaseDialogFragment.java b/app/src/main/java/org/solovyev/android/calculator/BaseDialogFragment.java index 118df5c7..8d4223ad 100644 --- a/app/src/main/java/org/solovyev/android/calculator/BaseDialogFragment.java +++ b/app/src/main/java/org/solovyev/android/calculator/BaseDialogFragment.java @@ -1,6 +1,7 @@ package org.solovyev.android.calculator; import android.content.Context; +import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; import android.support.annotation.NonNull; @@ -11,6 +12,8 @@ import android.support.v4.app.FragmentActivity; import android.support.v7.app.AlertDialog; import android.view.LayoutInflater; import android.view.View; +import android.view.Window; +import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import javax.inject.Inject; @@ -41,7 +44,17 @@ public abstract class BaseDialogFragment extends DialogFragment { final AlertDialog.Builder b = new AlertDialog.Builder(context, theme.alertDialogTheme); b.setView(view, spacing, spacing, spacing, spacing); onPrepareDialog(b); - return b.create(); + final AlertDialog dialog = b.create(); + dialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface d) { + onShowDialog(dialog); + } + }); + return dialog; + } + + protected void onShowDialog(@NonNull AlertDialog dialog) { } protected abstract void onPrepareDialog(@NonNull AlertDialog.Builder builder); diff --git a/app/src/main/java/org/solovyev/android/calculator/KeyboardUi.java b/app/src/main/java/org/solovyev/android/calculator/KeyboardUi.java index 3c387b00..7f872aef 100644 --- a/app/src/main/java/org/solovyev/android/calculator/KeyboardUi.java +++ b/app/src/main/java/org/solovyev/android/calculator/KeyboardUi.java @@ -6,6 +6,7 @@ import android.content.res.Resources; import android.graphics.Color; import android.graphics.PointF; import android.os.Build; +import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; import android.support.annotation.IdRes; import android.support.annotation.NonNull; @@ -42,9 +43,13 @@ public class KeyboardUi { private final List parameterNames; @NonNull private final SimpleDragListener dragListener; + @ColorInt private final int textColor; + @ColorInt private final int textColorSecondary; private final int sidePadding; + @DrawableRes + private final int buttonBackground; @SuppressWarnings("deprecation") public KeyboardUi(@NonNull User user, @NonNull List parameterNames) { @@ -55,6 +60,7 @@ public class KeyboardUi { textColor = resources.getColor(R.color.cpp_button_text); textColorSecondary = resources.getColor(R.color.cpp_button_text); sidePadding = resources.getDimensionPixelSize(R.dimen.cpp_button_padding); + buttonBackground = App.getTheme().light ? R.drawable.material_button_light : R.drawable.material_button_dark; } public void makeView() { @@ -148,7 +154,7 @@ public class KeyboardUi { private void fillButton(@NonNull View button, @IdRes int id) { button.setOnClickListener(buttonHandler); button.setId(id); - button.setBackgroundResource(R.drawable.material_button_light); + button.setBackgroundResource(buttonBackground); button.setPadding(sidePadding, 1, sidePadding, 1); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { button.setStateListAnimator(null); diff --git a/app/src/main/java/org/solovyev/android/calculator/KeyboardWindow.java b/app/src/main/java/org/solovyev/android/calculator/KeyboardWindow.java index a17d5632..eb8178f5 100644 --- a/app/src/main/java/org/solovyev/android/calculator/KeyboardWindow.java +++ b/app/src/main/java/org/solovyev/android/calculator/KeyboardWindow.java @@ -53,7 +53,7 @@ public class KeyboardWindow { final Context context = editor.getContext(); final LinearLayout view = new LinearLayout(context); view.setOrientation(LinearLayout.VERTICAL); - final int buttonSize = context.getResources().getDimensionPixelSize(R.dimen.cpp_kb_button_size); + final int buttonSize = context.getResources().getDimensionPixelSize(R.dimen.cpp_clickable_area_size); final int keyboardSize = 5 * buttonSize; window = new PopupWindow(view, keyboardSize, keyboardSize); window.setClippingEnabled(false); @@ -98,6 +98,9 @@ public class KeyboardWindow { return; } final Window window = dialog.getWindow(); + if (window == null) { + return; + } final WindowManager.LayoutParams lp = window.getAttributes(); lp.gravity = gravity; window.setAttributes(lp); diff --git a/app/src/main/java/org/solovyev/android/calculator/function/CppFunction.java b/app/src/main/java/org/solovyev/android/calculator/function/CppFunction.java index dbb3f5aa..f46b4e3b 100644 --- a/app/src/main/java/org/solovyev/android/calculator/function/CppFunction.java +++ b/app/src/main/java/org/solovyev/android/calculator/function/CppFunction.java @@ -47,9 +47,9 @@ public class CppFunction implements Jsonable, Parcelable { private static final String JSON_BODY = "b"; private static final String JSON_PARAMETERS = "ps"; private static final String JSON_DESCRIPTION = "d"; - protected final int id; @Nonnull protected final List parameters = new ArrayList<>(); + protected int id; @Nonnull protected String name; @Nonnull @@ -230,6 +230,13 @@ public class CppFunction implements Jsonable, Parcelable { return this; } + @Nonnull + public Builder withId(int id) { + Check.isTrue(!built); + function.id = id; + return this; + } + @Nonnull public CppFunction build() { built = true; diff --git a/app/src/main/java/org/solovyev/android/calculator/function/EditFunctionFragment.java b/app/src/main/java/org/solovyev/android/calculator/function/EditFunctionFragment.java index 177c50cf..0c067dd4 100644 --- a/app/src/main/java/org/solovyev/android/calculator/function/EditFunctionFragment.java +++ b/app/src/main/java/org/solovyev/android/calculator/function/EditFunctionFragment.java @@ -43,10 +43,12 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; +import org.solovyev.android.Check; import org.solovyev.android.calculator.App; import org.solovyev.android.calculator.AppComponent; import org.solovyev.android.calculator.BaseDialogFragment; @@ -62,8 +64,11 @@ import org.solovyev.android.calculator.math.edit.FunctionsFragment; import org.solovyev.android.calculator.math.edit.VarEditorSaver; import org.solovyev.common.math.MathRegistry; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -75,6 +80,8 @@ import jscl.math.function.CustomFunction; import jscl.math.function.Function; import jscl.math.function.IConstant; +import static org.solovyev.android.calculator.function.CppFunction.NO_ID; + public class EditFunctionFragment extends BaseDialogFragment implements View.OnClickListener, View.OnFocusChangeListener, View.OnKeyListener { private static final String ARG_FUNCTION = "function"; @@ -101,12 +108,12 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC EditText bodyView; @Bind(R.id.function_description) EditText descriptionView; - private CppFunction function; - @Inject Calculator calculator; @Inject FunctionsRegistry registry; + @Nullable + private CppFunction function; @Nonnull private static EditFunctionFragment create(@Nullable CppFunction function) { @@ -141,7 +148,10 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - function = getArguments().getParcelable(ARG_FUNCTION); + final Bundle arguments = getArguments(); + if (arguments != null) { + function = arguments.getParcelable(ARG_FUNCTION); + } } @Override @@ -165,31 +175,32 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC public AlertDialog onCreateDialog(Bundle savedInstanceState) { final AlertDialog dialog = super.onCreateDialog(savedInstanceState); dialog.setCanceledOnTouchOutside(false); - dialog.setOnShowListener(new DialogInterface.OnShowListener() { - @Override - public void onShow(DialogInterface d) { - nameView.selectAll(); - showIme(nameView); + return dialog; + } - final Button ok = dialog.getButton(AlertDialog.BUTTON_POSITIVE); - ok.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - tryClose(); - } - }); - if (function != null) { - final Button neutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); - neutral.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - tryRemoveFunction(function, false); - } - }); - } + @Override + protected void onShowDialog(@NonNull AlertDialog dialog) { + super.onShowDialog(dialog); + + nameView.selectAll(); + showIme(nameView); + + final Button ok = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + ok.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + tryClose(); } }); - return dialog; + if (function != null) { + final Button neutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + neutral.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + tryRemoveFunction(function, false); + } + }); + } } private void tryRemoveFunction(@NonNull CppFunction function, boolean confirmed) { @@ -220,19 +231,58 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC @Override public void onFocusChange(View v, boolean hasFocus) { - if (v.getId() == R.id.function_body) { - if (hasFocus) { - keyboardWindow.show(keyboardUser, getDialog(), paramsView.getParams()); - } else { - keyboardWindow.hide(); + if (v instanceof EditText && FunctionParamsView.PARAM_VIEW_TAG.equals(v.getTag())) { + final ViewParent parentView = v.getParent(); + if (parentView instanceof TextInputLayout) { + if (hasFocus) { + clearError((TextInputLayout) parentView); + } else { + validateParameters(); + } + } + return; + } + + final int id = v.getId(); + switch (id) { + case R.id.function_name: + if (hasFocus) { + clearError(nameLabel); + } else { + validateName(); + } + break; + case R.id.function_body: + if (hasFocus) { + clearError(bodyLabel); + showKeyboard(); + } else { + keyboardWindow.hide(); + validateBody(); + } + break; + } + } + + private void showKeyboard() { + keyboardWindow.show(keyboardUser, getDialog(), collectParameters()); + } + + @Nonnull + private List collectParameters() { + final List parameters = new ArrayList<>(); + for (String parameter : paramsView.getParams()) { + if (!TextUtils.isEmpty(parameter)) { + parameters.add(parameter); } } + return parameters; } @Override public void onClick(View v) { if (v.getId() == R.id.function_body) { - keyboardWindow.show(keyboardUser, getDialog(), paramsView.getParams()); + showKeyboard(); } } @@ -255,38 +305,16 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC } private void applyData() { - + final CppFunction newFunction = CppFunction.builder(nameView.getText().toString(), bodyView.getText().toString()) + .withId(function == null ? NO_ID : function.id) + .withParameters(collectParameters()) + .withDescription(descriptionView.getText().toString()).build(); + final Function oldFunction = (function == null || function.id == NO_ID) ? null : registry.getById(function.id); + registry.add(newFunction.toCustomFunctionBuilder(), oldFunction); } private boolean validate() { - if (!validateName()) { - return false; - } - if (!validateParameters()) { - return false; - } - if (!validateBody()) { - return false; - } - - final CppFunction newFunction = CppFunction.builder(nameView.getText().toString(), bodyView.getText().toString()) - .withParameters(paramsView.getParams()) - .withDescription(descriptionView.getText().toString()).build(); - final Function oldFunction = function.id == CppFunction.NO_ID ? null : registry.getById(function.id); - registry.add(newFunction.toCustomFunctionBuilder(), oldFunction); - return true; - } - - private boolean validateParameters(@Nonnull List parameterNames) { - for (String parameterName : parameterNames) { - if (!VarEditorSaver.isValidName(parameterName)) { - return false; - } - } - // error = R.string.function_param_not_empty; - - - return true; + return validateName() & validateParameters() & validateBody(); } private boolean validateName() { @@ -296,9 +324,16 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC return false; } final Function existingFunction = registry.get(name); - if (existingFunction != null && (!existingFunction.isIdDefined() || !existingFunction.getId().equals(function.getId()))) { - setError(nameLabel, getString(R.string.function_already_exists)); - return false; + if (existingFunction != null) { + if (!existingFunction.isIdDefined()) { + Check.shouldNotHappen(); + setError(nameLabel, getString(R.string.function_already_exists)); + return false; + } + if (function != null && !existingFunction.getId().equals(function.getId())) { + setError(nameLabel, getString(R.string.function_already_exists)); + return false; + } } clearError(nameLabel); return true; @@ -315,13 +350,26 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC } private boolean validateParameters() { + boolean valid = true; final List parameters = paramsView.getParams(); - /*if (TextUtils.isEmpty(body)) { - setError(bodyLabel, getString(R.string.function_is_empty)); - return false; - }*/ - //clearError(bodyLabel); - return true; + final Set usedParameters = new HashSet<>(); + for (int i = 0; i < parameters.size(); i++) { + final String parameter = parameters.get(i); + final TextInputLayout paramLabel = paramsView.getParamLabel(i); + if (TextUtils.isEmpty(parameter)) { + clearError(paramLabel); + } else if (!VarEditorSaver.isValidName(parameter)) { + valid = false; + setError(paramLabel, getString(R.string.invalid_name)); + } else if (usedParameters.contains(parameter)) { + valid = false; + setError(paramLabel, getString(R.string.function_duplicate_parameter)); + } else { + usedParameters.add(parameter); + clearError(paramLabel); + } + } + return valid; } @SuppressLint("InflateParams") @@ -331,15 +379,18 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC final View view = inflater.inflate(R.layout.fragment_function_edit, null); ButterKnife.bind(this, view); - if (savedInstanceState == null) { + if (savedInstanceState == null && function != null) { paramsView.addParams(function.getParameters()); nameView.setText(function.getName()); descriptionView.setText(function.getDescription()); bodyView.setText(function.getBody()); } + nameView.setOnFocusChangeListener(this); + paramsView.setOnFocusChangeListener(this); bodyView.setOnClickListener(this); bodyView.setOnFocusChangeListener(this); bodyView.setOnKeyListener(this); + descriptionView.setOnFocusChangeListener(this); return view; } diff --git a/app/src/main/java/org/solovyev/android/calculator/function/FunctionParamsView.java b/app/src/main/java/org/solovyev/android/calculator/function/FunctionParamsView.java index f7aee1bb..fd4bfdc1 100644 --- a/app/src/main/java/org/solovyev/android/calculator/function/FunctionParamsView.java +++ b/app/src/main/java/org/solovyev/android/calculator/function/FunctionParamsView.java @@ -27,9 +27,9 @@ import android.content.Context; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; -import android.text.Editable; -import android.text.TextUtils; +import android.support.design.widget.TextInputLayout; import android.util.AttributeSet; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; @@ -53,9 +53,13 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; public class FunctionParamsView extends LinearLayout { + @Nonnull + public static final String PARAM_VIEW_TAG = "param-view"; + private static final int HEADERS = 1; private static final int PARAM_VIEW_INDEX = 3; private static final int START_ROW_ID = App.generateViewId(); private int maxRowId = START_ROW_ID; + private final int clickableAreaSize = getResources().getDimensionPixelSize(R.dimen.cpp_clickable_area_size); public FunctionParamsView(Context context) { super(context); @@ -86,8 +90,8 @@ public class FunctionParamsView extends LinearLayout { addParam(null); } }); - headerView.addView(addButton, new LayoutParams(0, WRAP_CONTENT, 1)); - headerView.addView(new View(context), new LayoutParams(0, WRAP_CONTENT, 5)); + headerView.addView(addButton, new LayoutParams(clickableAreaSize, WRAP_CONTENT)); + headerView.addView(new View(context), new LayoutParams(0, WRAP_CONTENT, 1)); addView(headerView, new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); } @@ -95,10 +99,11 @@ public class FunctionParamsView extends LinearLayout { private LinearLayout makeRowView(@Nonnull Context context) { final LinearLayout rowView = new LinearLayout(context); rowView.setOrientation(HORIZONTAL); + rowView.setMinimumHeight(clickableAreaSize); + rowView.setGravity(Gravity.CENTER_VERTICAL); return rowView; } - public void addParams(@Nonnull List params) { for (String param : params) { addParam(param); @@ -121,7 +126,7 @@ public class FunctionParamsView extends LinearLayout { } }); removeButton.setText("−"); - rowView.addView(removeButton, new LayoutParams(0, WRAP_CONTENT, 1)); + rowView.addView(removeButton, new LayoutParams(clickableAreaSize, WRAP_CONTENT)); final Button upButton = new Button(context); upButton.setOnClickListener(new OnClickListener() { @@ -131,7 +136,7 @@ public class FunctionParamsView extends LinearLayout { } }); upButton.setText("↑"); - rowView.addView(upButton, new LayoutParams(0, WRAP_CONTENT, 1)); + rowView.addView(upButton, new LayoutParams(clickableAreaSize, WRAP_CONTENT)); final Button downButton = new Button(context); downButton.setOnClickListener(new OnClickListener() { @@ -141,16 +146,21 @@ public class FunctionParamsView extends LinearLayout { } }); downButton.setText("↓"); - rowView.addView(downButton, new LayoutParams(0, WRAP_CONTENT, 1)); + rowView.addView(downButton, new LayoutParams(clickableAreaSize, WRAP_CONTENT)); + final TextInputLayout paramLabel = new TextInputLayout(context); final EditText paramView = new EditText(context); if (param != null) { paramView.setText(param); } + paramView.setOnFocusChangeListener(getOnFocusChangeListener()); paramView.setInputType(EditorInfo.TYPE_CLASS_TEXT); paramView.setId(id); + paramView.setTag(PARAM_VIEW_TAG); paramView.setHint(R.string.c_function_parameter); - rowView.addView(paramView, new LayoutParams(0, WRAP_CONTENT, 3)); + paramLabel.addView(paramView, new TextInputLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); + + rowView.addView(paramLabel, new LayoutParams(0, WRAP_CONTENT, 1)); addView(rowView, new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); } @@ -158,20 +168,20 @@ public class FunctionParamsView extends LinearLayout { private void downRow(@Nonnull ViewGroup row) { final int index = indexOfChild(row); if (index < getChildCount() - 1) { - swap(row, getRowByIndex(index + 1)); + swap(row, getRow(index + 1)); } } private void upRow(@Nonnull ViewGroup row) { final int index = indexOfChild(row); if (index > 1) { - swap(row, getRowByIndex(index - 1)); + swap(row, getRow(index - 1)); } } private void swap(@Nonnull ViewGroup l, @Nonnull ViewGroup r) { - final EditText lParam = (EditText) l.getChildAt(PARAM_VIEW_INDEX); - final EditText rParam = (EditText) r.getChildAt(PARAM_VIEW_INDEX); + final EditText lParam = getParamView(l); + final EditText rParam = getParamView(r); swap(lParam, rParam); } @@ -183,7 +193,7 @@ public class FunctionParamsView extends LinearLayout { } @Nonnull - private ViewGroup getRowByIndex(int index) { + private ViewGroup getRow(int index) { Check.isTrue(index >= 0 && index < getChildCount()); return (ViewGroup) getChildAt(index); } @@ -196,18 +206,26 @@ public class FunctionParamsView extends LinearLayout { public List getParams() { final List params = new ArrayList<>(getChildCount()); - for (int i = 1; i < getChildCount(); i++) { - final ViewGroup row = getRowByIndex(i); - final EditText paramView = (EditText) row.getChildAt(PARAM_VIEW_INDEX); - final Editable param = paramView.getText(); - if (!TextUtils.isEmpty(param)) { - params.add(param.toString()); - } + for (int i = HEADERS; i < getChildCount(); i++) { + final ViewGroup row = getRow(i); + final EditText paramView = getParamView(row); + params.add(paramView.getText().toString()); } return params; } + @Nonnull + private EditText getParamView(@Nonnull ViewGroup row) { + final TextInputLayout paramLabel = getParamLabel(row); + return (EditText) paramLabel.getChildAt(0); + } + + @Nonnull + private TextInputLayout getParamLabel(@Nonnull ViewGroup row) { + return (TextInputLayout) row.getChildAt(PARAM_VIEW_INDEX); + } + @Override protected Parcelable onSaveInstanceState() { final Parcelable superState = super.onSaveInstanceState(); @@ -218,9 +236,9 @@ public class FunctionParamsView extends LinearLayout { private int[] getRowIds() { final int childCount = getChildCount(); final int[] rowIds = new int[childCount - 1]; - for (int i = 1; i < childCount; i++) { - final ViewGroup row = getRowByIndex(i); - final EditText paramView = (EditText) row.getChildAt(PARAM_VIEW_INDEX); + for (int i = HEADERS; i < childCount; i++) { + final ViewGroup row = getRow(i); + final EditText paramView = getParamView(row); rowIds[i - 1] = paramView.getId(); } return rowIds; @@ -243,6 +261,11 @@ public class FunctionParamsView extends LinearLayout { super.onRestoreInstanceState(state.getSuperState()); } + @Nonnull + public TextInputLayout getParamLabel(int param) { + return getParamLabel(getRow(param + HEADERS)); + } + public static final class SavedState extends BaseSavedState { public static final Parcelable.Creator CREATOR = diff --git a/app/src/main/res/layout/fragment_function_edit.xml b/app/src/main/res/layout/fragment_function_edit.xml index ec6db2ea..714ed30c 100644 --- a/app/src/main/res/layout/fragment_function_edit.xml +++ b/app/src/main/res/layout/fragment_function_edit.xml @@ -23,7 +23,7 @@ --> + a:layout_height="wrap_content" + a:orientation="vertical" /> 20sp 20sp - 40dp + 40dp \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 56d56c2e..5e53a389 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -39,5 +39,6 @@ 1dp 20dp - 50dp + 50dp + 400dp \ No newline at end of file diff --git a/app/src/main/res/values/text_strings.xml b/app/src/main/res/values/text_strings.xml index d5bf268c..3e01574d 100644 --- a/app/src/main/res/values/text_strings.xml +++ b/app/src/main/res/values/text_strings.xml @@ -155,7 +155,8 @@ Name of function is not valid: name must start with a letter, can contain letters, digits and underscore. Function with the same name already exists! Function body could not be empty! - Function parameter should not be empty! + Invalid name + There is already a parameter with the same name Do you really want to delete \'%s\' function? Unable to create empty function! Do not show this message until next session