470 lines
16 KiB
Java
470 lines
16 KiB
Java
/*
|
||
* 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.variables;
|
||
|
||
import android.annotation.SuppressLint;
|
||
import android.content.Context;
|
||
import android.content.DialogInterface;
|
||
import android.content.Intent;
|
||
import android.graphics.Typeface;
|
||
import android.os.Bundle;
|
||
import android.support.annotation.NonNull;
|
||
import android.support.design.widget.TextInputLayout;
|
||
import android.support.v4.app.FragmentActivity;
|
||
import android.support.v4.app.FragmentManager;
|
||
import android.support.v7.app.AlertDialog;
|
||
import android.text.Editable;
|
||
import android.text.InputFilter;
|
||
import android.text.SpannableStringBuilder;
|
||
import android.view.KeyEvent;
|
||
import android.view.LayoutInflater;
|
||
import android.view.View;
|
||
import android.view.ViewGroup;
|
||
import android.view.inputmethod.InputMethodManager;
|
||
import android.widget.Button;
|
||
import android.widget.EditText;
|
||
import android.widget.PopupWindow;
|
||
|
||
import org.solovyev.android.Check;
|
||
import org.solovyev.android.calculator.App;
|
||
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.Keyboard;
|
||
import org.solovyev.android.calculator.PreparedExpression;
|
||
import org.solovyev.android.calculator.R;
|
||
import org.solovyev.android.calculator.RemovalConfirmationDialog;
|
||
import org.solovyev.android.calculator.ToJsclTextProcessor;
|
||
import org.solovyev.android.calculator.VariablesRegistry;
|
||
import org.solovyev.android.calculator.functions.FunctionsRegistry;
|
||
import org.solovyev.android.calculator.keyboard.FloatingKeyboard;
|
||
import org.solovyev.android.calculator.keyboard.FloatingKeyboardWindow;
|
||
import org.solovyev.android.calculator.math.MathType;
|
||
import org.solovyev.android.calculator.view.EditTextCompat;
|
||
import org.solovyev.android.text.method.NumberInputFilter;
|
||
import org.solovyev.common.text.Strings;
|
||
|
||
import java.util.Arrays;
|
||
import java.util.List;
|
||
|
||
import javax.annotation.Nonnull;
|
||
import javax.annotation.Nullable;
|
||
import javax.inject.Inject;
|
||
|
||
import butterknife.Bind;
|
||
import butterknife.ButterKnife;
|
||
import dagger.Lazy;
|
||
import jscl.math.function.IConstant;
|
||
|
||
import static org.solovyev.android.calculator.variables.CppVariable.NO_ID;
|
||
|
||
public class EditVariableFragment extends BaseDialogFragment implements View.OnFocusChangeListener, View.OnKeyListener, View.OnClickListener {
|
||
|
||
private static final String ARG_VARIABLE = "variable";
|
||
private final static List<Character> ACCEPTABLE_CHARACTERS = Arrays.asList(Strings.toObjects(("1234567890abcdefghijklmnopqrstuvwxyzйцукенгшщзхъфывапролджэячсмитьбюё_" + GreekFloatingKeyboard.ALPHABET).toCharArray()));
|
||
@NonNull
|
||
private final KeyboardUser keyboardUser = new KeyboardUser();
|
||
@Bind(R.id.variable_name_label)
|
||
TextInputLayout nameLabel;
|
||
@Bind(R.id.variable_name)
|
||
EditTextCompat nameView;
|
||
@NonNull
|
||
private final FloatingKeyboardWindow keyboardWindow = new FloatingKeyboardWindow(new PopupWindow.OnDismissListener() {
|
||
@Override
|
||
public void onDismiss() {
|
||
nameView.setShowSoftInputOnFocusCompat(true);
|
||
}
|
||
});
|
||
@Bind(R.id.variable_keyboard_button)
|
||
Button keyboardButton;
|
||
@Bind(R.id.variable_value_label)
|
||
TextInputLayout valueLabel;
|
||
@Bind(R.id.variable_value)
|
||
EditText valueView;
|
||
@Bind(R.id.variable_exponent_button)
|
||
Button exponentButton;
|
||
@Bind(R.id.variable_description)
|
||
EditText descriptionView;
|
||
@Inject
|
||
Calculator calculator;
|
||
@Inject
|
||
Keyboard keyboard;
|
||
@Inject
|
||
Typeface typeface;
|
||
@Inject
|
||
FunctionsRegistry functionsRegistry;
|
||
@Inject
|
||
VariablesRegistry variablesRegistry;
|
||
@Inject
|
||
Lazy<ToJsclTextProcessor> toJsclTextProcessor;
|
||
@Inject
|
||
Engine engine;
|
||
@Nullable
|
||
private CppVariable variable;
|
||
|
||
public EditVariableFragment() {
|
||
}
|
||
|
||
@Nonnull
|
||
public static EditVariableFragment create(@Nullable CppVariable variable) {
|
||
final EditVariableFragment fragment = new EditVariableFragment();
|
||
if (variable != null) {
|
||
final Bundle args = new Bundle();
|
||
args.putParcelable(ARG_VARIABLE, variable);
|
||
fragment.setArguments(args);
|
||
}
|
||
return fragment;
|
||
}
|
||
|
||
public static void showDialog(@Nonnull FragmentActivity activity) {
|
||
EditVariableFragment.showDialog(null, activity.getSupportFragmentManager());
|
||
}
|
||
|
||
public static void showDialog(@Nullable CppVariable variable, @Nonnull Context context) {
|
||
if (!(context instanceof VariablesActivity)) {
|
||
final Intent intent = new Intent(context, VariablesActivity.getClass(context));
|
||
App.addIntentFlags(intent, false, context);
|
||
intent.putExtra(VariablesActivity.EXTRA_VARIABLE, variable);
|
||
context.startActivity(intent);
|
||
} else {
|
||
EditVariableFragment.showDialog(variable,
|
||
((VariablesActivity) context).getSupportFragmentManager());
|
||
}
|
||
}
|
||
|
||
public static void showDialog(@Nullable CppVariable variable, @Nonnull FragmentManager fm) {
|
||
App.showDialog(create(variable), "variable-editor", fm);
|
||
}
|
||
|
||
public boolean isValidValue(@Nonnull String value) {
|
||
try {
|
||
final PreparedExpression pe = toJsclTextProcessor.get().process(value);
|
||
return !pe.hasUndefinedVariables();
|
||
} catch (RuntimeException e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void onCreate(@android.support.annotation.Nullable Bundle savedInstanceState) {
|
||
super.onCreate(savedInstanceState);
|
||
final Bundle arguments = getArguments();
|
||
if (arguments != null) {
|
||
variable = arguments.getParcelable(ARG_VARIABLE);
|
||
}
|
||
}
|
||
|
||
@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(isNewVariable() ? R.string.c_var_create_var : R.string.c_var_edit_var);
|
||
if (!isNewVariable()) {
|
||
builder.setNeutralButton(R.string.cpp_delete, null);
|
||
}
|
||
}
|
||
|
||
private boolean isNewVariable() {
|
||
return variable == null || variable.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);
|
||
}
|
||
}
|
||
|
||
private void showRemovalDialog(@NonNull final CppVariable variable) {
|
||
RemovalConfirmationDialog.showForVariable(getActivity(), variable.name,
|
||
new DialogInterface.OnClickListener() {
|
||
@Override
|
||
public void onClick(DialogInterface dialog, int which) {
|
||
Check.isTrue(which == DialogInterface.BUTTON_POSITIVE);
|
||
variablesRegistry.remove(variable.toJsclConstant());
|
||
dismiss();
|
||
}
|
||
});
|
||
}
|
||
|
||
private void tryClose() {
|
||
if (validate() && applyData()) {
|
||
dismiss();
|
||
}
|
||
}
|
||
|
||
private boolean applyData() {
|
||
try {
|
||
final CppVariable newVariable = CppVariable.builder(nameView.getText().toString())
|
||
.withId(isNewVariable() ? NO_ID : variable.id)
|
||
.withValue(valueView.getText().toString())
|
||
.withDescription(descriptionView.getText().toString()).build();
|
||
final IConstant oldVariable = isNewVariable() ? null : variablesRegistry.getById(variable.id);
|
||
variablesRegistry.addOrUpdate(newVariable.toJsclConstant(), oldVariable);
|
||
return true;
|
||
} catch (RuntimeException e) {
|
||
setError(valueLabel, e.getLocalizedMessage());
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private boolean validate() {
|
||
return validateName() & validateValue();
|
||
}
|
||
|
||
private boolean validateValue() {
|
||
final String value = valueView.getText().toString();
|
||
if (!Strings.isEmpty(value)) {
|
||
// value is not empty => must be a number
|
||
if (!isValidValue(value)) {
|
||
setError(valueLabel, R.string.c_value_is_not_a_number);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
clearError(valueLabel);
|
||
return true;
|
||
}
|
||
|
||
private boolean validateName() {
|
||
final String name = nameView.getText().toString();
|
||
if (!Engine.isValidName(name)) {
|
||
setError(nameLabel, getString(R.string.cpp_name_contains_invalid_characters));
|
||
return false;
|
||
}
|
||
for (int i = 0; i < name.length(); i++) {
|
||
final char c = name.charAt(i);
|
||
if (!ACCEPTABLE_CHARACTERS.contains(Character.toLowerCase(c))) {
|
||
setError(nameLabel, getString(R.string.c_char_is_not_accepted, c));
|
||
return false;
|
||
}
|
||
}
|
||
final IConstant existingVariable = variablesRegistry.get(name);
|
||
if (existingVariable != null) {
|
||
if (!existingVariable.isIdDefined()) {
|
||
Check.shouldNotHappen();
|
||
setError(nameLabel, getString(R.string.c_var_already_exists));
|
||
return false;
|
||
}
|
||
if (isNewVariable()) {
|
||
// trying to create a new variable with existing name
|
||
setError(nameLabel, getString(R.string.c_var_already_exists));
|
||
return false;
|
||
}
|
||
Check.isNotNull(variable);
|
||
if (!existingVariable.getId().equals(variable.id)) {
|
||
// trying to change the name of existing variable to some other variable's name
|
||
setError(nameLabel, getString(R.string.c_var_already_exists));
|
||
return false;
|
||
}
|
||
}
|
||
|
||
final MathType.Result type = MathType.getType(name, 0, false, engine);
|
||
if (type.type != MathType.text && type.type != MathType.constant) {
|
||
setError(nameLabel, getString(R.string.c_var_name_clashes));
|
||
return false;
|
||
}
|
||
|
||
clearError(nameLabel);
|
||
return true;
|
||
}
|
||
|
||
@SuppressLint("InflateParams")
|
||
@NonNull
|
||
@Override
|
||
protected View onCreateDialogView(@NonNull Context context, @NonNull LayoutInflater inflater, @android.support.annotation.Nullable Bundle savedInstanceState) {
|
||
final View view = inflater.inflate(R.layout.fragment_variable_edit, null);
|
||
ButterKnife.bind(this, view);
|
||
|
||
if (savedInstanceState == null && variable != null) {
|
||
nameView.setText(variable.name);
|
||
valueView.setText(variable.value);
|
||
descriptionView.setText(variable.description);
|
||
}
|
||
nameView.setOnFocusChangeListener(this);
|
||
nameView.setOnKeyListener(this);
|
||
valueView.setOnFocusChangeListener(this);
|
||
valueView.setEditableFactory(new Editable.Factory() {
|
||
@Override
|
||
public Editable newEditable(CharSequence source) {
|
||
return new NumberEditable(source);
|
||
}
|
||
});
|
||
exponentButton.setOnClickListener(this);
|
||
descriptionView.setOnFocusChangeListener(this);
|
||
keyboardButton.setOnClickListener(this);
|
||
|
||
return view;
|
||
}
|
||
|
||
@Override
|
||
public void onFocusChange(View v, boolean hasFocus) {
|
||
switch (v.getId()) {
|
||
case R.id.variable_name:
|
||
if (hasFocus) {
|
||
clearError(nameLabel);
|
||
} else {
|
||
keyboardUser.done();
|
||
}
|
||
break;
|
||
case R.id.variable_value:
|
||
if (hasFocus) {
|
||
clearError(valueLabel);
|
||
} else {
|
||
validateValue();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||
if (v.getId() == R.id.variable_name) {
|
||
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK && keyboardWindow.isShown()) {
|
||
keyboardUser.done();
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
@Override
|
||
public void onClick(View v) {
|
||
switch (v.getId()) {
|
||
case R.id.variable_keyboard_button:
|
||
if (keyboardWindow.isShown()) {
|
||
keyboardUser.showIme();
|
||
} else {
|
||
showKeyboard();
|
||
}
|
||
break;
|
||
case R.id.variable_exponent_button:
|
||
final int start = Math.max(valueView.getSelectionStart(), 0);
|
||
final int end = Math.max(valueView.getSelectionEnd(), 0);
|
||
valueView.getText().replace(Math.min(start, end), Math.max(start, end), "E", 0, 1);
|
||
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(variable);
|
||
showRemovalDialog(variable);
|
||
break;
|
||
default:
|
||
super.onClick(dialog, which);
|
||
break;
|
||
}
|
||
}
|
||
|
||
private void showKeyboard() {
|
||
nameView.dontShowSoftInputOnFocusCompat();
|
||
keyboardWindow.show(new GreekFloatingKeyboard(keyboardUser), getDialog());
|
||
}
|
||
|
||
private static class NumberEditable extends SpannableStringBuilder {
|
||
public NumberEditable(CharSequence source) {
|
||
super(source);
|
||
super.setFilters(new InputFilter[]{NumberInputFilter.getInstance()});
|
||
}
|
||
|
||
@Override
|
||
public void setFilters(InputFilter[] filters) {
|
||
// we don't want filters as we want to support numbers in scientific notation
|
||
}
|
||
}
|
||
|
||
private class KeyboardUser implements FloatingKeyboard.User {
|
||
@NonNull
|
||
@Override
|
||
public Context getContext() {
|
||
return getActivity();
|
||
}
|
||
|
||
@NonNull
|
||
@Override
|
||
public EditText getEditor() {
|
||
return nameView;
|
||
}
|
||
|
||
@NonNull
|
||
@Override
|
||
public ViewGroup getKeyboard() {
|
||
return keyboardWindow.getContentView();
|
||
}
|
||
|
||
@Override
|
||
public void done() {
|
||
if (keyboardWindow.isShown()) {
|
||
keyboardWindow.hide();
|
||
}
|
||
validateName();
|
||
}
|
||
|
||
@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 isVibrateOnKeypress() {
|
||
return keyboard.isVibrateOnKeypress();
|
||
}
|
||
|
||
@NonNull
|
||
@Override
|
||
public Typeface getTypeface() {
|
||
return typeface;
|
||
}
|
||
}
|
||
}
|