/* * Copyright 2013 serso aka se.solovyev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Contact details * * Email: se.solovyev@gmail.com * Site: http://se.solovyev.org */ package org.solovyev.android.calculator.functions; import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; import android.graphics.Typeface; import android.os.Bundle; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.design.widget.TextInputLayout; import android.support.v7.app.AlertDialog; import android.text.Editable; import android.text.TextUtils; import android.view.ContextMenu; import android.view.KeyEvent; import android.view.LayoutInflater; 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.EditText; import org.solovyev.android.Check; import org.solovyev.android.calculator.AppComponent; import org.solovyev.android.calculator.BaseDialogFragment; import org.solovyev.android.calculator.Calculator; import org.solovyev.android.calculator.Engine; import org.solovyev.android.calculator.FloatingCalculatorKeyboard; import org.solovyev.android.calculator.Keyboard; import org.solovyev.android.calculator.ParseException; import org.solovyev.android.calculator.PreparedExpression; import org.solovyev.android.calculator.R; import org.solovyev.android.calculator.VariablesRegistry; import org.solovyev.android.calculator.keyboard.FloatingKeyboardWindow; import org.solovyev.android.calculator.view.EditTextCompat; 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; import javax.inject.Inject; import butterknife.Bind; import butterknife.ButterKnife; import jscl.math.function.IConstant; import static org.solovyev.android.calculator.functions.CppFunction.NO_ID; public abstract class BaseFunctionFragment extends BaseDialogFragment implements View.OnClickListener, View.OnFocusChangeListener, View.OnKeyListener { protected static final String ARG_FUNCTION = "function"; private static final int MENU_FUNCTION = Menu.FIRST; private static final int MENU_CONSTANT = Menu.FIRST + 1; private static final int MENU_CATEGORY = Menu.FIRST + 2; @NonNull private final FloatingKeyboardWindow keyboardWindow = new FloatingKeyboardWindow(null); @NonNull private final KeyboardUser keyboardUser = new KeyboardUser(); @Bind(R.id.function_params) public FunctionParamsView paramsView; @Bind(R.id.function_name_label) TextInputLayout nameLabel; @Bind(R.id.function_name) public EditText nameView; @Bind(R.id.function_body_label) public TextInputLayout bodyLabel; @Bind(R.id.function_body) public EditTextCompat bodyView; @Bind(R.id.function_description_label) public TextInputLayout descriptionLabel; @Bind(R.id.function_description) public EditText descriptionView; @Inject Calculator calculator; @Inject Keyboard keyboard; @Inject Typeface typeface; @Inject FunctionsRegistry functionsRegistry; @Inject VariablesRegistry variablesRegistry; @Nullable protected CppFunction function; @LayoutRes private final int layout; protected BaseFunctionFragment(@LayoutRes int layout) { this.layout = layout; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Bundle arguments = getArguments(); if (arguments != null) { function = arguments.getParcelable(ARG_FUNCTION); } } @Override protected void inject(@NonNull AppComponent component) { super.inject(component); component.inject(this); } @Override protected void onPrepareDialog(@NonNull AlertDialog.Builder builder) { builder.setNegativeButton(R.string.cpp_cancel, null); builder.setPositiveButton(R.string.cpp_done, null); builder.setTitle(isNewFunction() ? R.string.function_create_function : R.string.function_edit_function); if (!isNewFunction()) { builder.setNeutralButton(R.string.cpp_delete, null); } } protected final boolean isNewFunction() { return function == null || function.id == NO_ID; } @NonNull @Override public AlertDialog onCreateDialog(Bundle savedInstanceState) { final AlertDialog dialog = super.onCreateDialog(savedInstanceState); dialog.setCanceledOnTouchOutside(false); return dialog; } @Override protected void onShowDialog(@NonNull AlertDialog dialog, boolean firstTime) { if (firstTime) { nameView.selectAll(); showIme(nameView); } } @Override public void onFocusChange(View v, boolean hasFocus) { 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(new FloatingCalculatorKeyboard(keyboardUser, collectParameters()), getDialog()); } @Nonnull protected final List<String> collectParameters() { final List<String> parameters = new ArrayList<>(); for (String parameter : paramsView.getParams()) { if (!TextUtils.isEmpty(parameter)) { parameters.add(parameter); } } return parameters; } @Override public void onClick(View v) { switch (v.getId()) { case R.id.function_body: showKeyboard(); break; default: super.onClick(v); break; } } @Override public void onClick(DialogInterface dialog, int which) { switch (which) { case DialogInterface.BUTTON_POSITIVE: tryClose(); break; case DialogInterface.BUTTON_NEUTRAL: Check.isNotNull(function); showRemovalDialog(function); break; default: super.onClick(dialog, which); break; } } protected abstract void showRemovalDialog(@NonNull CppFunction function); @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (v.getId() == R.id.function_body) { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK && keyboardWindow.isShown()) { keyboardWindow.hide(); return true; } } return false; } protected void tryClose() { if (!validate()) { return; } final CppFunction function = collectData(); if (function == null) { return; } if (applyData(function)) { dismiss(); } } @Nullable private CppFunction collectData() { try { final String body = calculator.prepare(bodyView.getText().toString()).getValue(); return CppFunction.builder(nameView.getText().toString(), body) .withId(isNewFunction() ? NO_ID : function.id) .withParameters(collectParameters()) .withDescription(descriptionView.getText().toString()).build(); } catch (RuntimeException e) { setError(bodyLabel, e.getLocalizedMessage()); } return null; } private boolean validate() { return validateName() & validateParameters() & validateBody(); } protected boolean validateName() { return true; } private boolean validateBody() { final String body = bodyView.getText().toString(); if (TextUtils.isEmpty(body)) { setError(bodyLabel, getString(R.string.cpp_field_cannot_be_empty)); return false; } try { final PreparedExpression pe = calculator.prepare(body); if (pe.hasUndefinedVariables()) { // check that all undefined variables are actually function parameters final List<String> parameters = collectParameters(); for (IConstant undefinedVariable : pe.getUndefinedVariables()) { if (!parameters.contains(undefinedVariable.getName())) { setError(bodyLabel, getString(R.string.c_error)); return false; } } } clearError(bodyLabel); return true; } catch (ParseException e) { setError(bodyLabel, e.getLocalizedMessage()); return false; } } private boolean validateParameters() { boolean valid = true; final List<String> parameters = paramsView.getParams(); final Set<String> 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 (!Engine.isValidName(parameter)) { valid = false; setError(paramLabel, getString(R.string.cpp_name_contains_invalid_characters)); } else if (usedParameters.contains(parameter)) { valid = false; setError(paramLabel, getString(R.string.cpp_duplicate_parameter, parameter)); } else { usedParameters.add(parameter); clearError(paramLabel); } } return valid; } @SuppressLint("InflateParams") @NonNull @Override protected View onCreateDialogView(@NonNull Context context, @NonNull LayoutInflater inflater, @Nullable Bundle savedInstanceState) { final View view = inflater.inflate(layout, null); ButterKnife.bind(this, view); 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); bodyView.dontShowSoftInputOnFocusCompat(); descriptionView.setOnFocusChangeListener(this); return view; } private class KeyboardUser implements FloatingCalculatorKeyboard.User, MenuItem.OnMenuItemClickListener { @NonNull @Override public Context getContext() { return getActivity(); } @NonNull @Override public EditText getEditor() { return bodyView; } @NonNull @Override public ViewGroup getKeyboard() { return keyboardWindow.getContentView(); } @Override public void insertOperator(char operator) { insertOperator(String.valueOf(operator)); } public int clampSelection(int selection) { return selection < 0 ? 0 : selection; } @Override public void insertOperator(@NonNull String operator) { final int start = clampSelection(bodyView.getSelectionStart()); final int end = clampSelection(bodyView.getSelectionEnd()); final Editable e = bodyView.getText(); e.replace(start, end, getOperator(start, end, e, operator)); } @NonNull private String getOperator(int start, int end, @NonNull Editable e, @NonNull CharSequence operator) { boolean spaceBefore = true; boolean spaceAfter = true; if (start > 0 && Character.isSpaceChar(e.charAt(start - 1))) { spaceBefore = false; } if (end < e.length() && Character.isSpaceChar(e.charAt(end))) { spaceAfter = false; } if (spaceBefore && spaceAfter) { return " " + operator + " "; } if (spaceBefore) { return " " + operator; } if (spaceAfter) { return operator + " "; } return String.valueOf(operator); } @Override public void showConstants(@NonNull View v) { bodyView.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { final int id = v.getId(); if (id == R.id.function_body) { menu.clear(); addEntities(menu, getNamesSorted(variablesRegistry), MENU_CONSTANT); unregisterForContextMenu(bodyView); } } }); bodyView.showContextMenu(); } @Nonnull private List<String> getNamesSorted(@NonNull MathRegistry<?> registry) { final List<String> names = new ArrayList<>(registry.getNames()); Collections.sort(names); return names; } @Override public void showFunctions(@NonNull View v) { bodyView.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { final int id = v.getId(); if (id == R.id.function_body) { menu.clear(); addEntities(menu, getNamesSorted(functionsRegistry), MENU_FUNCTION); unregisterForContextMenu(bodyView); } } }); bodyView.showContextMenu(); } private void addEntities(@NonNull Menu menu, @NonNull List<String> entities, int groupId) { for (String entity : entities) { menu.add(groupId, Menu.NONE, Menu.NONE, entity).setOnMenuItemClickListener(KeyboardUser.this); } } @Override public void showFunctionsConstants(@NonNull View v) { bodyView.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { final int id = v.getId(); if (id == R.id.function_body) { menu.clear(); // can't use sub-menus as AlertDialog doesn't support them menu.add(MENU_CATEGORY, MENU_CONSTANT, Menu.NONE, R.string.cpp_vars_and_constants).setOnMenuItemClickListener(KeyboardUser.this); menu.add(MENU_CATEGORY, MENU_FUNCTION, Menu.NONE, R.string.c_functions).setOnMenuItemClickListener(KeyboardUser.this); unregisterForContextMenu(bodyView); } } }); bodyView.showContextMenu(); } @Override public void insertText(@NonNull CharSequence text, int selectionOffset) { EditTextCompat.insert(text, bodyView); if (selectionOffset != 0) { final int selection = clampSelection(bodyView.getSelectionEnd()); final int newSelection = selection + selectionOffset; if (newSelection >= 0 && newSelection < bodyView.getText().length()) { bodyView.setSelection(newSelection); } } // if a parameter has been inserted - update the parameter's list if (TextUtils.equals(text, "x") || TextUtils.equals(text, "y")) { final String possibleParam = text.toString(); if (!collectParameters().contains(possibleParam)) { paramsView.addParam(possibleParam); } } } @Override public boolean isVibrateOnKeypress() { return keyboard.isVibrateOnKeypress(); } @NonNull @Override public Typeface getTypeface() { return typeface; } @Override public void done() { keyboardWindow.hide(); validateBody(); } @Override public void showIme() { final InputMethodManager keyboard = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); keyboard.showSoftInput(getEditor(), InputMethodManager.SHOW_FORCED); keyboardWindow.hide(); } @Override public boolean onMenuItemClick(final MenuItem item) { final int groupId = item.getGroupId(); final CharSequence title = item.getTitle(); switch (groupId) { case MENU_FUNCTION: final int argsListIndex = title.toString().indexOf("("); if (argsListIndex < 0) { keyboardUser.insertText(title + "()", -1); } else { keyboardUser.insertText(title.subSequence(0, argsListIndex) + "()", -1); } return true; case MENU_CONSTANT: keyboardUser.insertText(title.toString(), 0); return true; case MENU_CATEGORY: bodyView.post(new Runnable() { @Override public void run() { final int itemId = item.getItemId(); if (itemId == MENU_FUNCTION) { showFunctions(bodyView); } else if (itemId == MENU_CONSTANT) { showConstants(bodyView); } } }); return true; } return false; } } protected abstract boolean applyData(@NonNull CppFunction function); }