diff --git a/app/build.gradle b/app/build.gradle index 67b8e287..e77f8274 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,7 +85,7 @@ dependencies { exclude(module: 'xercesImpl') } compile 'org.solovyev.android:checkout:0.7.2@aar' - compile 'org.solovyev.android:material:0.1.3@aar' + compile 'org.solovyev.android:material:0.1.4@aar' compile 'com.google.android.gms:play-services-ads:8.4.0' compile 'com.google.android.gms:play-services-base:8.4.0' compile 'com.google.android.gms:play-services-analytics:8.4.0' diff --git a/app/misc/libs/plotter.aar b/app/misc/libs/plotter.aar index a6e9dfd4..77e83555 100644 Binary files a/app/misc/libs/plotter.aar and b/app/misc/libs/plotter.aar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0cc1970e..de3810c4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -117,6 +117,10 @@ android:label="@string/c_vars_and_constants" android:theme="@style/Cpp.Theme.Dialog" /> + + P getParcelable(@NonNull Bundle bundle, + @NonNull String key) { + final P parcelable = bundle.getParcelable(key); + Check.isNotNull(parcelable); + return parcelable; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -40,7 +57,8 @@ public abstract class BaseFragment extends Fragment { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { final View view = inflater.inflate(layout, container, false); adUi.onCreateView(view); return view; diff --git a/app/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java b/app/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java index ccd6ca26..afc0ba7c 100644 --- a/app/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java +++ b/app/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java @@ -22,6 +22,15 @@ package org.solovyev.android.calculator; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; +import static org.solovyev.android.calculator.Preferences.Gui.preventScreenFromFading; +import static org.solovyev.android.calculator.release.ReleaseNotes.hasReleaseNotes; +import static org.solovyev.android.wizard.WizardUi.continueWizard; +import static org.solovyev.android.wizard.WizardUi.createLaunchIntent; +import static org.solovyev.android.wizard.WizardUi.startWizard; + import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; @@ -33,11 +42,15 @@ import android.support.v7.app.AlertDialog; import android.support.v7.widget.CardView; import android.support.v7.widget.Toolbar; import android.text.method.LinkMovementMethod; -import android.view.*; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; import android.widget.FrameLayout; import android.widget.TextView; -import butterknife.Bind; -import butterknife.ButterKnife; + import org.solovyev.android.Activities; import org.solovyev.android.Android; import org.solovyev.android.calculator.converter.ConverterFragment; @@ -49,17 +62,13 @@ import org.solovyev.android.wizard.Wizard; import org.solovyev.android.wizard.Wizards; import org.solovyev.common.Objects; +import butterknife.Bind; +import butterknife.ButterKnife; + import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.inject.Inject; -import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; -import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; -import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; -import static org.solovyev.android.calculator.Preferences.Gui.preventScreenFromFading; -import static org.solovyev.android.calculator.release.ReleaseNotes.hasReleaseNotes; -import static org.solovyev.android.wizard.WizardUi.*; - public class CalculatorActivity extends BaseActivity implements SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener { @Inject @@ -278,7 +287,7 @@ public class CalculatorActivity extends BaseActivity implements SharedPreference launcher.showHistory(); return true; case R.id.menu_plotter: - Locator.getInstance().getPlotter().plot(); + launcher.showPlotter(); return true; case R.id.menu_conversion_tool: ConverterFragment.show(this); diff --git a/app/src/main/java/org/solovyev/android/calculator/functions/BaseFunctionFragment.java b/app/src/main/java/org/solovyev/android/calculator/functions/BaseFunctionFragment.java new file mode 100644 index 00000000..5c23a2e0 --- /dev/null +++ b/app/src/main/java/org/solovyev/android/calculator/functions/BaseFunctionFragment.java @@ -0,0 +1,546 @@ +/* + * 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 static org.solovyev.android.calculator.functions.CppFunction.NO_ID; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +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.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.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 butterknife.Bind; +import butterknife.ButterKnife; + +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; + +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) + 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) + EditText descriptionView; + @Inject + Calculator calculator; + @Inject + Keyboard keyboard; + @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.c_cancel, null); + builder.setPositiveButton(R.string.ok, null); + builder.setTitle(isNewFunction() ? R.string.function_create_function : + R.string.function_edit_function); + } + + 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 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) { + 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; + default: + super.onClick(dialog, which); + break; + } + } + + @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() { + final String name = nameView.getText().toString(); + if (!Engine.isValidName(name)) { + setError(nameLabel, getString(R.string.function_name_is_not_valid)); + return false; + } + clearError(nameLabel); + return true; + } + + private boolean validateBody() { + final String body = bodyView.getText().toString(); + if (TextUtils.isEmpty(body)) { + setError(bodyLabel, getString(R.string.function_is_empty)); + return false; + } + try { + calculator.prepare(body); + clearError(bodyLabel); + return true; + } catch (ParseException e) { + setError(bodyLabel, e.getLocalizedMessage()); + return false; + } + } + + private boolean validateParameters() { + boolean valid = true; + final List parameters = paramsView.getParams(); + 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 (!Engine.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") + @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 Resources getResources() { + return BaseFunctionFragment.this.getResources(); + } + + @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 getNamesSorted(@NonNull MathRegistry registry) { + final List 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 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.c_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) { + final int start = clampSelection(bodyView.getSelectionStart()); + final int end = clampSelection(bodyView.getSelectionEnd()); + final Editable e = bodyView.getText(); + e.replace(start, end, text); + if (selectionOffset != 0) { + final int selection = clampSelection(bodyView.getSelectionEnd()); + final int newSelection = selection + selectionOffset; + if (newSelection >= 0 && newSelection < e.length()) { + bodyView.setSelection(newSelection); + } + } + } + + @Override + public boolean isVibrateOnKeypress() { + return keyboard.isVibrateOnKeypress(); + } + + @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); +} diff --git a/app/src/main/java/org/solovyev/android/calculator/functions/EditFunctionFragment.java b/app/src/main/java/org/solovyev/android/calculator/functions/EditFunctionFragment.java index 4fcc5033..38dca236 100644 --- a/app/src/main/java/org/solovyev/android/calculator/functions/EditFunctionFragment.java +++ b/app/src/main/java/org/solovyev/android/calculator/functions/EditFunctionFragment.java @@ -1,131 +1,33 @@ -/* - * 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 static org.solovyev.android.calculator.functions.CppFunction.NO_ID; - -import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.res.Resources; 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.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.Activities; 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.FloatingCalculatorKeyboard; -import org.solovyev.android.calculator.Keyboard; -import org.solovyev.android.calculator.ParseException; import org.solovyev.android.calculator.R; -import org.solovyev.android.calculator.VariablesRegistry; import org.solovyev.android.calculator.entities.EntityRemovalDialog; -import org.solovyev.android.calculator.keyboard.FloatingKeyboardWindow; -import org.solovyev.android.calculator.view.EditTextCompat; -import org.solovyev.common.math.MathRegistry; -import butterknife.Bind; -import butterknife.ButterKnife; import jscl.math.function.Function; -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; -public class EditFunctionFragment extends BaseDialogFragment implements View.OnClickListener, View.OnFocusChangeListener, View.OnKeyListener { +public class EditFunctionFragment extends BaseFunctionFragment { - private 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) - FunctionParamsView paramsView; - @Bind(R.id.function_name_label) - TextInputLayout nameLabel; - @Bind(R.id.function_name) - EditText nameView; - @Bind(R.id.function_body_label) - TextInputLayout bodyLabel; - @Bind(R.id.function_body) - EditTextCompat bodyView; - @Bind(R.id.function_description) - EditText descriptionView; - @Inject - Calculator calculator; - @Inject - Keyboard keyboard; - @Inject - FunctionsRegistry functionsRegistry; - @Inject - VariablesRegistry variablesRegistry; - @Nullable - private CppFunction function; - - @Nonnull - private static EditFunctionFragment create(@Nullable CppFunction function) { - final EditFunctionFragment fragment = new EditFunctionFragment(); - if (function != null) { - final Bundle args = new Bundle(); - args.putParcelable(ARG_FUNCTION, function); - fragment.setArguments(args); - } - return fragment; + public EditFunctionFragment() { + super(R.layout.fragment_function_edit); } public static void show(@Nonnull FragmentActivity activity) { - EditFunctionFragment.show(null, activity.getSupportFragmentManager()); + show(null, activity.getSupportFragmentManager()); } public static void show(@Nullable CppFunction function, @Nonnull Context context) { @@ -135,8 +37,7 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC intent.putExtra(FunctionsActivity.EXTRA_FUNCTION, function); context.startActivity(intent); } else { - EditFunctionFragment.show(function, - ((FunctionsActivity) context).getSupportFragmentManager()); + show(function, ((FunctionsActivity) context).getSupportFragmentManager()); } } @@ -144,133 +45,40 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC App.showDialog(create(function), "function-editor", fm); } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Bundle arguments = getArguments(); - if (arguments != null) { - function = arguments.getParcelable(ARG_FUNCTION); + @Nonnull + private static BaseFunctionFragment create(@Nullable CppFunction function) { + final BaseFunctionFragment fragment = new EditFunctionFragment(); + if (function != null) { + final Bundle args = new Bundle(); + args.putParcelable(ARG_FUNCTION, function); + fragment.setArguments(args); } - } - - @Override - protected void inject(@NonNull AppComponent component) { - super.inject(component); - component.inject(this); + return fragment; } @Override protected void onPrepareDialog(@NonNull AlertDialog.Builder builder) { - builder.setNegativeButton(R.string.c_cancel, null); - builder.setPositiveButton(R.string.ok, null); - builder.setTitle(isNewFunction() ? R.string.function_create_function : - R.string.function_edit_function); + super.onPrepareDialog(builder); if (!isNewFunction()) { builder.setNeutralButton(R.string.c_remove, null); } } - private 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); - } - } - private void showRemovalDialog(@NonNull final CppFunction function) { EntityRemovalDialog.showForFunction(getActivity(), function.name, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Check.isTrue(which == DialogInterface.BUTTON_POSITIVE); - functionsRegistry.remove(function.toJsclBuilder().create()); - dismiss(); - } - }); - } - - @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(); + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Check.isTrue(which == DialogInterface.BUTTON_POSITIVE); + functionsRegistry.remove(function.toJsclBuilder().create()); + dismiss(); } - } - 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 - 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) { - 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); @@ -282,32 +90,10 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC } @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; - } - - private void tryClose() { - if (validate() && applyData()) { - dismiss(); - } - } - - private boolean applyData() { + protected boolean applyData(@Nonnull @NonNull CppFunction function) { try { - final String body = calculator.prepare(bodyView.getText().toString()).getValue(); - - final CppFunction newFunction = CppFunction.builder(nameView.getText().toString(), body) - .withId(isNewFunction() ? NO_ID : function.id) - .withParameters(collectParameters()) - .withDescription(descriptionView.getText().toString()).build(); final Function oldFunction = isNewFunction() ? null : functionsRegistry.getById(function.id); - functionsRegistry.add(newFunction.toJsclBuilder(), oldFunction); + functionsRegistry.add(function.toJsclBuilder(), oldFunction); return true; } catch (RuntimeException e) { setError(bodyLabel, e.getLocalizedMessage()); @@ -315,16 +101,12 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC return false; } - private boolean validate() { - return validateName() & validateParameters() & validateBody(); - } - - private boolean validateName() { - final String name = nameView.getText().toString(); - if (!Engine.isValidName(name)) { - setError(nameLabel, getString(R.string.function_name_is_not_valid)); + @Override + protected boolean validateName() { + if (!super.validateName()) { return false; } + final String name = nameView.getText().toString(); final Function existingFunction = functionsRegistry.get(name); if (existingFunction != null) { if (!existingFunction.isIdDefined()) { @@ -347,263 +129,4 @@ public class EditFunctionFragment extends BaseDialogFragment implements View.OnC clearError(nameLabel); return true; } - - private boolean validateBody() { - final String body = bodyView.getText().toString(); - if (TextUtils.isEmpty(body)) { - setError(bodyLabel, getString(R.string.function_is_empty)); - return false; - } - try { - calculator.prepare(body); - clearError(bodyLabel); - return true; - } catch (ParseException e) { - setError(bodyLabel, e.getLocalizedMessage()); - return false; - } - } - - private boolean validateParameters() { - boolean valid = true; - final List parameters = paramsView.getParams(); - 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 (!Engine.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") - @NonNull - @Override - protected View onCreateDialogView(@NonNull Context context, @NonNull LayoutInflater inflater, @Nullable Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.fragment_function_edit, 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 Resources getResources() { - return EditFunctionFragment.this.getResources(); - } - - @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 getNamesSorted(@NonNull MathRegistry registry) { - final List 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 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.c_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) { - final int start = clampSelection(bodyView.getSelectionStart()); - final int end = clampSelection(bodyView.getSelectionEnd()); - final Editable e = bodyView.getText(); - e.replace(start, end, text); - if (selectionOffset != 0) { - final int selection = clampSelection(bodyView.getSelectionEnd()); - final int newSelection = selection + selectionOffset; - if (newSelection >= 0 && newSelection < e.length()) { - bodyView.setSelection(newSelection); - } - } - } - - @Override - public boolean isVibrateOnKeypress() { - return keyboard.isVibrateOnKeypress(); - } - - @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; - } - } } diff --git a/app/src/main/java/org/solovyev/android/calculator/functions/FunctionsFragment.java b/app/src/main/java/org/solovyev/android/calculator/functions/FunctionsFragment.java index d84355ff..651e5e23 100644 --- a/app/src/main/java/org/solovyev/android/calculator/functions/FunctionsFragment.java +++ b/app/src/main/java/org/solovyev/android/calculator/functions/FunctionsFragment.java @@ -26,11 +26,15 @@ import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.FragmentActivity; -import android.view.*; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + import com.squareup.otto.Bus; import com.squareup.otto.Subscribe; -import jscl.math.function.Function; -import jscl.math.function.IFunction; + import org.solovyev.android.Check; import org.solovyev.android.calculator.AppComponent; import org.solovyev.android.calculator.Calculator; @@ -39,11 +43,15 @@ import org.solovyev.android.calculator.entities.BaseEntitiesFragment; import org.solovyev.android.calculator.entities.Category; import org.solovyev.android.calculator.entities.EntityRemovalDialog; +import jscl.math.function.Function; +import jscl.math.function.IFunction; + +import java.util.ArrayList; +import java.util.List; + import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.inject.Inject; -import java.util.ArrayList; -import java.util.List; public class FunctionsFragment extends BaseEntitiesFragment { @@ -85,7 +93,8 @@ public class FunctionsFragment extends BaseEntitiesFragment { return true; case R.string.c_edit: if (function instanceof IFunction) { - EditFunctionFragment.show(CppFunction.builder((IFunction) function).build(), activity.getSupportFragmentManager()); + EditFunctionFragment.show(CppFunction.builder((IFunction) function).build(), + activity.getSupportFragmentManager()); } return true; case R.string.c_remove: diff --git a/app/src/main/java/org/solovyev/android/calculator/plot/ExpressionFunction.java b/app/src/main/java/org/solovyev/android/calculator/plot/ExpressionFunction.java new file mode 100644 index 00000000..c1f6ca0c --- /dev/null +++ b/app/src/main/java/org/solovyev/android/calculator/plot/ExpressionFunction.java @@ -0,0 +1,130 @@ +package org.solovyev.android.calculator.plot; + +import org.solovyev.android.plotter.Function; + +import jscl.math.Expression; +import jscl.math.Generic; +import jscl.math.JsclInteger; +import jscl.math.NumericWrapper; +import jscl.math.function.Constant; +import jscl.math.numeric.Complex; +import jscl.math.numeric.Numeric; +import jscl.math.numeric.Real; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class ExpressionFunction extends Function { + private static final Complex NaN = Complex.valueOf(Double.NaN, 0d); + + @Nonnull + public final jscl.math.function.Function function; + public final Constant xVariable; + public final Constant yVariable; + public final boolean imaginary; + public final int arity; + + public ExpressionFunction(@Nonnull jscl.math.function.Function function, @Nullable Constant x, + @Nullable Constant y, boolean imaginary) { + super(imaginary ? "Im(" + function.toString() + ")" : function.toString()); + this.function = function; + this.xVariable = x; + this.yVariable = y; + this.imaginary = imaginary; + this.arity = countArity(x, y); + } + + private static int countArity(@Nullable Constant x, @Nullable Constant y) { + if (x != null && y != null) { + return 2; + } else if (x == null && y == null) { + return 0; + } + return 1; + } + + @Override + public int getArity() { + return arity; + } + + @Override + public float evaluate() { + final Complex value = calculate(function); + if (imaginary) { + return (float) value.imaginaryPart(); + } + return (float) value.realPart(); + } + + @Override + public float evaluate(float x) { + final Complex value = calculate(function, xVariable, x); + if (imaginary) { + return (float) value.imaginaryPart(); + } + return (float) value.realPart(); + } + + @Override + public float evaluate(float x, float y) { + final Complex value = calculate(function, xVariable, x, yVariable, y); + if (imaginary) { + return (float) value.imaginaryPart(); + } + return (float) value.realPart(); + } + + @Nonnull + public static Complex calculate(jscl.math.function.Function function, Constant xVar, + float x, Constant yVar, float y) { + try { + Generic tmp = function.substitute(xVar, Expression.valueOf((double) x)); + tmp = tmp.substitute(yVar, Expression.valueOf((double) y)); + return unwrap(tmp.numeric()); + } catch (RuntimeException e) { + return NaN; + } + } + + @Nonnull + public static Complex calculate(jscl.math.function.Function function, Constant xVar, + float x) { + try { + return unwrap(function.substitute(xVar, Expression.valueOf((double) x)).numeric()); + } catch (RuntimeException e) { + return NaN; + } + } + + @Nonnull + public static Complex calculate(jscl.math.function.Function function) { + try { + return unwrap(function.numeric()); + } catch (RuntimeException e) { + return NaN; + } + } + + @Nonnull + public static Complex unwrap(Generic numeric) { + if (numeric instanceof JsclInteger) { + return Complex.valueOf(((JsclInteger) numeric).intValue(), 0d); + } else if (numeric instanceof NumericWrapper) { + return unwrap(((NumericWrapper) numeric).content()); + } else { + return NaN; + } + } + + @Nonnull + public static Complex unwrap(Numeric content) { + if (content instanceof Real) { + return Complex.valueOf(((Real) content).doubleValue(), 0d); + } else if (content instanceof Complex) { + return ((Complex) content); + } else { + throw new ArithmeticException(); + } + } +} diff --git a/app/src/main/java/org/solovyev/android/calculator/plot/PlotActivity.java b/app/src/main/java/org/solovyev/android/calculator/plot/PlotActivity.java new file mode 100644 index 00000000..8a226369 --- /dev/null +++ b/app/src/main/java/org/solovyev/android/calculator/plot/PlotActivity.java @@ -0,0 +1,109 @@ +package org.solovyev.android.calculator.plot; + +import android.os.Bundle; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import org.solovyev.android.calculator.AppComponent; +import org.solovyev.android.calculator.BaseActivity; +import org.solovyev.android.calculator.BaseFragment; +import org.solovyev.android.calculator.R; +import org.solovyev.android.plotter.Dimensions; +import org.solovyev.android.plotter.PlotViewFrame; +import org.solovyev.android.plotter.Plotter; + +import butterknife.Bind; +import butterknife.ButterKnife; + +import javax.annotation.Nonnull; +import javax.inject.Inject; + +public class PlotActivity extends BaseActivity { + + public static class MyFragment extends BaseFragment implements PlotViewFrame.Listener { + + @Inject + Plotter plotter; + @Bind(R.id.plot_view_frame) + PlotViewFrame plotView; + + public MyFragment() { + super(R.layout.fragment_plot); + } + + @Override + protected void inject(@Nonnull AppComponent component) { + super.inject(component); + component.inject(this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + ButterKnife.bind(this, view); + + plotView.addControlView(R.id.plot_add_function); + plotView.addControlView(R.id.plot_functions); + plotView.addControlView(R.id.plot_dimensions); + plotView.setPlotter(plotter); + plotView.setListener(this); + + return view; + } + + @Override + public void onPause() { + plotView.onPause(); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + plotView.onResume(); + } + + @Override + public boolean onButtonPressed(int id) { + if (id == R.id.plot_dimensions) { + final Dimensions dimensions = plotter.getDimensions(); + PlotDimensionsFragment.show(dimensions.graph.makeBounds(), plotter.is3d(), + getActivity().getSupportFragmentManager()); + return true; + } else if (id == R.id.plot_functions) { + PlotFunctionsFragment.show(getActivity().getSupportFragmentManager()); + return true; + } else if (id == R.id.plot_add_function) { + //App.getBus().post(new AddFunctionDialog.ShowEvent()); + return true; + } + return false; + } + + @Override + public void unableToZoom(boolean in) { + Toast.makeText(getActivity(), "Can't zoom anymore", Toast.LENGTH_SHORT).show(); + } + } + + public PlotActivity() { + super(R.layout.activity_empty); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + final FragmentManager fm = getSupportFragmentManager(); + final FragmentTransaction t = fm.beginTransaction(); + t.add(R.id.main, new MyFragment(), "plotter"); + t.commit(); + } + } +} diff --git a/app/src/main/java/org/solovyev/android/calculator/plot/PlotDimensionsFragment.java b/app/src/main/java/org/solovyev/android/calculator/plot/PlotDimensionsFragment.java new file mode 100644 index 00000000..6cff7f49 --- /dev/null +++ b/app/src/main/java/org/solovyev/android/calculator/plot/PlotDimensionsFragment.java @@ -0,0 +1,288 @@ +package org.solovyev.android.calculator.plot; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.RectF; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.design.widget.TextInputLayout; +import android.support.v4.app.FragmentManager; +import android.support.v7.app.AlertDialog; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.TextView; + +import org.solovyev.android.calculator.App; +import org.solovyev.android.calculator.AppComponent; +import org.solovyev.android.calculator.BaseDialogFragment; +import org.solovyev.android.calculator.BaseFragment; +import org.solovyev.android.calculator.R; +import org.solovyev.android.plotter.Check; +import org.solovyev.android.plotter.Plot; +import org.solovyev.android.plotter.Plotter; + +import butterknife.Bind; +import butterknife.ButterKnife; + +import javax.annotation.Nonnull; +import javax.inject.Inject; + +public class PlotDimensionsFragment extends BaseDialogFragment + implements TextView.OnEditorActionListener { + private static final String ARG_BOUNDS = "arg-bounds"; + private static final String ARG_3D = "arg-3d"; + + private class MyTextWatcher implements TextWatcher { + @NonNull + private final TextInputLayout input; + private final boolean x; + + private MyTextWatcher(@NonNull TextInputLayout input, boolean x) { + this.input = input; + this.x = x; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + if (TextUtils.isEmpty(input.getError())) { + return; + } + + final RectF bounds = collectData(); + if (x) { + validXBounds(bounds); + } else { + validYBounds(bounds); + } + } + } + + @Inject + Plotter plotter; + @Bind(R.id.plot_x_min) + EditText xMin; + @Bind(R.id.plot_x_min_label) + TextInputLayout xMinLabel; + @Bind(R.id.plot_x_max) + EditText xMax; + @Bind(R.id.plot_x_max_label) + TextInputLayout xMaxLabel; + @Bind(R.id.plot_y_min) + EditText yMin; + @Bind(R.id.plot_y_min_label) + TextInputLayout yMinLabel; + @Bind(R.id.plot_y_max) + EditText yMax; + @Bind(R.id.plot_y_max_label) + TextInputLayout yMaxLabel; + @Bind(R.id.y_bounds) + View yBounds; + @NonNull + private RectF bounds = new RectF(); + private boolean d3; + + public PlotDimensionsFragment() { + } + + public static void show(@NonNull RectF bounds, boolean d3, @Nonnull FragmentManager fm) { + App.showDialog(create(bounds, d3), "plot-dimensions", fm); + } + + @NonNull + private static PlotDimensionsFragment create(@NonNull RectF bounds, boolean d3) { + final PlotDimensionsFragment dialog = new PlotDimensionsFragment(); + final Bundle args = new Bundle(); + args.putParcelable(ARG_BOUNDS, bounds); + args.putBoolean(ARG_3D, d3); + dialog.setArguments(args); + return dialog; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Bundle arguments = getArguments(); + Check.isNotNull(arguments); + bounds = BaseFragment.getParcelable(arguments, ARG_BOUNDS); + d3 = arguments.getBoolean(ARG_3D); + } + + @Override + protected void inject(@NonNull AppComponent component) { + super.inject(component); + component.inject(this); + } + + @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) { + super.onShowDialog(dialog, firstTime); + if (firstTime) { + final InputMethodManager imm = (InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(xMin, InputMethodManager.SHOW_IMPLICIT); + } + } + + @Override + protected void onPrepareDialog(@NonNull AlertDialog.Builder builder) { + builder.setTitle("Dimensions"); + builder.setPositiveButton(android.R.string.ok, null); + } + + @NonNull + @Override + protected View onCreateDialogView(@NonNull Context context, @NonNull LayoutInflater inflater, + Bundle savedInstanceState) { + @SuppressLint("InflateParams") final View view = + LayoutInflater.from(context).inflate(R.layout.fragment_plot_dimensions, null); + ButterKnife.bind(this, view); + + setDimension(xMin, bounds.left); + setDimension(xMax, bounds.right); + setDimension(yMin, bounds.top); + setDimension(yMax, bounds.bottom); + xMin.addTextChangedListener(new MyTextWatcher(xMinLabel, true)); + xMax.addTextChangedListener(new MyTextWatcher(xMaxLabel, true)); + yMin.addTextChangedListener(new MyTextWatcher(yMinLabel, false)); + yMax.addTextChangedListener(new MyTextWatcher(yMaxLabel, false)); + if (d3) { + yBounds.setVisibility(View.GONE); + } + return view; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + tryClose(); + return; + default: + super.onClick(dialog, which); + return; + } + } + + private void setDimension(@NonNull EditText view, float value) { + view.setOnEditorActionListener(this); + view.setText(String.format("%.2f", value)); + } + + private void tryClose() { + if (validate()) { + applyData(); + dismiss(); + } + } + + private boolean validate() { + final RectF bounds = collectData(); + if (!validXBounds(bounds) | !validYBounds(bounds)) { + return false; + } + return true; + } + + private boolean validYBounds(@NonNull RectF bounds) { + if (validNumbers(this.bounds.top, this.bounds.bottom, yMinLabel, yMaxLabel)) { + return false; + } + if (bounds.top >= bounds.bottom) { + setError(yMinLabel, " "); + setError(yMaxLabel, "max ≯ min"); + return false; + } + clearError(yMinLabel); + clearError(yMaxLabel); + return true; + } + + private boolean validXBounds(@NonNull RectF bounds) { + if (validNumbers(bounds.left, bounds.right, xMinLabel, xMaxLabel)) { + return false; + } + if (bounds.left >= bounds.right) { + setError(xMinLabel, " "); + setError(xMaxLabel, "max ≯ min"); + return false; + } + clearError(xMinLabel); + clearError(xMaxLabel); + return true; + } + + private boolean validNumbers(float l, float r, @NonNull TextInputLayout lInput, @NonNull + TextInputLayout rInput) { + final boolean nanLeft = Float.isNaN(l); + final boolean nanRight = Float.isNaN(r); + if (nanLeft || nanRight) { + if (nanLeft) { + setError(lInput, " "); + } else { + clearError(lInput); + } + if (nanRight) { + setError(rInput, " "); + } else { + clearError(rInput); + } + return true; + } + return false; + } + + @NonNull + private RectF collectData() { + return new RectF(getDimension(xMin), getDimension(yMin), getDimension(xMax), + getDimension(yMax)); + } + + private void applyData() { + final RectF bounds = collectData(); + Plot.setGraphBounds(null, plotter, bounds, d3); + } + + private float getDimension(@NonNull EditText view) { + try { + return Float.parseFloat(view.getText().toString().replace(",", ".").replace("−", "-")); + } catch (NumberFormatException e) { + Log.w(Plot.getTag("MainActivity"), e.getMessage(), e); + return Float.NaN; + } + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + tryClose(); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/solovyev/android/calculator/plot/PlotEditFunctionFragment.java b/app/src/main/java/org/solovyev/android/calculator/plot/PlotEditFunctionFragment.java new file mode 100644 index 00000000..964deaf1 --- /dev/null +++ b/app/src/main/java/org/solovyev/android/calculator/plot/PlotEditFunctionFragment.java @@ -0,0 +1,201 @@ +package org.solovyev.android.calculator.plot; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentManager; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.google.common.base.Strings; + +import org.solovyev.android.calculator.App; +import org.solovyev.android.calculator.AppComponent; +import org.solovyev.android.calculator.R; +import org.solovyev.android.calculator.functions.BaseFunctionFragment; +import org.solovyev.android.calculator.functions.CppFunction; +import org.solovyev.android.plotter.Color; +import org.solovyev.android.plotter.PlotFunction; +import org.solovyev.android.plotter.PlotIconView; +import org.solovyev.android.plotter.Plotter; +import org.solovyev.android.plotter.meshes.MeshSpec; + +import butterknife.Bind; +import jscl.math.function.Constant; +import jscl.math.function.CustomFunction; +import uz.shift.colorpicker.LineColorPicker; +import uz.shift.colorpicker.OnColorChangedListener; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.inject.Inject; + +public class PlotEditFunctionFragment extends BaseFunctionFragment + implements SeekBar.OnSeekBarChangeListener { + @Inject + Plotter plotter; + @Bind(R.id.fn_meshspec_views) + View meshSpecViews; + @Bind(R.id.fn_color_label) + TextView colorLabel; + @Bind(R.id.fn_color_picker) + LineColorPicker colorPicker; + @Bind(R.id.fn_linewidth_label) + TextView lineWidthLabel; + @Bind(R.id.fn_linewidth_seekbar) + SeekBar lineWidthSeekBar; + @Bind(R.id.fn_iconview) + PlotIconView iconView; + private PlotFunction plotFunction; + + public PlotEditFunctionFragment() { + super(R.layout.fragment_plot_function_edit); + } + + public static void show(@Nullable PlotFunction function, @Nonnull + FragmentManager fm) { + App.showDialog(create(function), "plot-function-editor", fm); + } + + @NonNull + public static PlotEditFunctionFragment create(@Nullable PlotFunction pf) { + final PlotEditFunctionFragment fragment = new PlotEditFunctionFragment(); + if (pf != null && pf.function instanceof ExpressionFunction) { + final Bundle args = new Bundle(); + final String name = + pf.function.hasName() ? Strings.nullToEmpty(pf.function.getName()) : ""; + final List parameters = new ArrayList<>(); + final ExpressionFunction ef = (ExpressionFunction) pf.function; + if (ef.xVariable != null) { + parameters.add(ef.xVariable.getName()); + } + if (ef.yVariable != null) { + parameters.add(ef.yVariable.getName()); + } + args.putParcelable(ARG_FUNCTION, CppFunction + .builder(name, + ((CustomFunction) ef.function).getContent()) + .withParameters(parameters) + .withId(pf.function.getId()) + .build()); + fragment.setArguments(args); + } + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (function != null) { + plotFunction = plotter.getPlotData().get(function.getId()); + if (plotFunction == null) { + dismiss(); + } + } + } + + @Override + protected void inject(@NonNull AppComponent component) { + super.inject(component); + component.inject(this); + } + + @NonNull + @Override + protected View onCreateDialogView(@NonNull Context context, @NonNull LayoutInflater inflater, + @Nullable Bundle savedInstanceState) { + final View view = super.onCreateDialogView(context, inflater, savedInstanceState); + colorPicker.setOnColorChangedListener(new OnColorChangedListener() { + @Override + public void onColorChanged(int c) { + iconView.setMeshSpec(applyMeshSpec()); + } + }); + lineWidthSeekBar.setMax(MeshSpec.MAX_WIDTH - MeshSpec.MIN_WIDTH); + lineWidthSeekBar.setOnSeekBarChangeListener(this); + + final int[] colors = MeshSpec.LightColors.asIntArray(); + colorPicker.setColors(colors); + if (savedInstanceState == null) { + if (plotFunction != null) { + setupViews(plotFunction.meshSpec); + } else { + setupViews(); + } + } + return view; + } + + private void setupViews(@NonNull MeshSpec meshSpec) { + final int color = meshSpec.color.toInt(); + final int[] colors = colorPicker.getColors(); + final int i = indexOf(colors, color); + colorPicker.setSelectedColorPosition(Math.max(0, i)); + lineWidthSeekBar.setProgress(meshSpec.width - MeshSpec.MIN_WIDTH); + iconView.setMeshSpec(meshSpec); + } + + private void setupViews() { + colorPicker.setSelectedColorPosition(0); + lineWidthSeekBar.setProgress(MeshSpec.defaultWidth(getActivity()) - MeshSpec.MIN_WIDTH); + iconView.setMeshSpec(applyMeshSpec()); + } + + private static int indexOf(int[] integers, int integer) { + for (int i = 0; i < integers.length; i++) { + if (integers[i] == integer) { + return i; + } + } + return -1; + } + + @NonNull + protected MeshSpec applyMeshSpec() { + final Color color = Color.create(colorPicker.getColor()); + final int width = MeshSpec.MIN_WIDTH + lineWidthSeekBar.getProgress(); + return MeshSpec.create(color, width); + } + + protected boolean applyData(@Nonnull CppFunction function) { + try { + final List parameters = function.getParameters(); + final Constant x = parameters.size() > 0 ? new Constant(parameters.get(0)) : null; + final Constant y = parameters.size() > 1 ? new Constant(parameters.get(1)) : null; + final ExpressionFunction expressionFunction = + new ExpressionFunction(function.toJsclBuilder().create(), x, y, false); + final PlotFunction plotFunction = PlotFunction.create(expressionFunction, + applyMeshSpec()); + final int id = function.getId(); + if (id != CppFunction.NO_ID) { + plotter.update(id, plotFunction); + } else { + plotter.add(plotFunction); + } + return true; + } catch (RuntimeException e) { + setError(bodyLabel, e.getLocalizedMessage()); + } + return false; + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + iconView.setMeshSpec(applyMeshSpec()); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } +} diff --git a/app/src/main/java/org/solovyev/android/calculator/plot/PlotFunctionsFragment.java b/app/src/main/java/org/solovyev/android/calculator/plot/PlotFunctionsFragment.java new file mode 100644 index 00000000..30157d33 --- /dev/null +++ b/app/src/main/java/org/solovyev/android/calculator/plot/PlotFunctionsFragment.java @@ -0,0 +1,232 @@ +package org.solovyev.android.calculator.plot; + +import static android.support.v7.widget.LinearLayoutManager.VERTICAL; +import static android.view.Menu.NONE; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentManager; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.RecyclerView; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.solovyev.android.calculator.App; +import org.solovyev.android.calculator.AppComponent; +import org.solovyev.android.calculator.BaseDialogFragment; +import org.solovyev.android.calculator.R; +import org.solovyev.android.plotter.BasePlotterListener; +import org.solovyev.android.plotter.PlotFunction; +import org.solovyev.android.plotter.PlotIconView; +import org.solovyev.android.plotter.Plotter; +import org.solovyev.android.views.llm.DividerItemDecoration; +import org.solovyev.android.views.llm.LinearLayoutManager; + +import butterknife.Bind; +import butterknife.ButterKnife; + +import java.util.List; + +import javax.annotation.Nonnull; +import javax.inject.Inject; + +public class PlotFunctionsFragment extends BaseDialogFragment { + + @Inject + Plotter plotter; + @NonNull + private final PlotterListener plotterListener = new PlotterListener(); + private Adapter adapter; + + public PlotFunctionsFragment() { + } + + public static void show(@Nonnull FragmentManager fm) { + App.showDialog(new PlotFunctionsFragment(), "plot-functions", fm); + } + + @Override + protected void inject(@NonNull AppComponent component) { + super.inject(component); + component.inject(this); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + plotter.addListener(plotterListener); + return view; + } + + @NonNull + protected RecyclerView onCreateDialogView(@NonNull Context context, @NonNull LayoutInflater inflater, Bundle savedInstanceState) { + @SuppressLint("InflateParams") final RecyclerView view = (RecyclerView) inflater.inflate(R.layout.dialog_functions, null); + + final LinearLayoutManager layoutManager = new LinearLayoutManager(context, VERTICAL, false); + final int itemHeight = context.getResources().getDimensionPixelSize(R.dimen.list_item_height); + layoutManager.setChildSize(itemHeight + getDividerHeight(context)); + view.setLayoutManager(layoutManager); + + view.addItemDecoration(new DividerItemDecoration(context, null)); + adapter = new Adapter(plotter.getPlotData().functions); + view.setAdapter(adapter); + return view; + } + + private int getDividerHeight(@NonNull Context context) { + final TypedArray a = context.obtainStyledAttributes(null, new int[]{android.R.attr.listDivider}); + final Drawable divider = a.getDrawable(0); + final int dividerHeight = divider == null ? 0 : divider.getIntrinsicHeight(); + a.recycle(); + return dividerHeight; + } + + @Override + public void onDestroyView() { + plotter.removeListener(plotterListener); + super.onDestroyView(); + } + + protected void onPrepareDialog(@NonNull AlertDialog.Builder builder) { + builder.setPositiveButton(android.R.string.ok, null); + builder.setNeutralButton("Add", null); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEUTRAL: + PlotEditFunctionFragment.show(null, getActivity().getSupportFragmentManager()); + return; + default: + super.onClick(dialog, which); + return; + } + } + + public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnCreateContextMenuListener, MenuItem.OnMenuItemClickListener { + + @Bind(R.id.function_icon) + PlotIconView icon; + + @Bind(R.id.fn_name_edittext) + TextView name; + private PlotFunction function; + + private ViewHolder(@NonNull View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + itemView.setOnClickListener(this); + itemView.setOnCreateContextMenuListener(this); + } + + void bind(@NonNull PlotFunction function) { + this.function = function; + name.setText(function.function.getName()); + icon.setMeshSpec(function.meshSpec); + } + + @Override + public void onClick(View v) { + PlotEditFunctionFragment.show(function, getActivity().getSupportFragmentManager()); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + menu.add(NONE, R.string.c_remove, NONE, R.string.c_remove).setOnMenuItemClickListener(this); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (function != null && item.getItemId() == R.string.c_remove) { + plotter.remove(function); + return true; + } + return false; + } + } + + private class Adapter extends RecyclerView.Adapter { + @NonNull + private final List list; + + public Adapter(@NonNull List list) { + this.list = list; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + return new ViewHolder(inflater.inflate(R.layout.dialog_functions_function, parent, false)); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + ((ViewHolder) holder).bind(list.get(position)); + } + + @Override + public int getItemCount() { + return list.size(); + } + + public void remove(@NonNull PlotFunction function) { + final int i = list.indexOf(function); + if (i >= 0) { + list.remove(i); + notifyItemRemoved(i); + } + } + + public void update(int id, @NonNull PlotFunction function) { + final int i = find(id); + if (i >= 0) { + list.set(i, function); + notifyItemChanged(i); + } + } + + private int find(int id) { + for (int i = 0; i < list.size(); i++) { + final PlotFunction function = list.get(i); + if (function.function.getId() == id) { + return i; + } + } + return -1; + } + + public void add(@NonNull PlotFunction function) { + list.add(function); + notifyItemInserted(list.size() - 1); + } + } + + private class PlotterListener extends BasePlotterListener { + @Override + public void onFunctionAdded(@NonNull PlotFunction function) { + adapter.add(function); + } + + @Override + public void onFunctionUpdated(int id, @NonNull PlotFunction function) { + adapter.update(id, function); + } + + @Override + public void onFunctionRemoved(@NonNull PlotFunction function) { + adapter.remove(function); + } + } +} diff --git a/app/src/main/java/uz/shift/colorpicker/LineColorPicker.java b/app/src/main/java/uz/shift/colorpicker/LineColorPicker.java new file mode 100644 index 00000000..34020f83 --- /dev/null +++ b/app/src/main/java/uz/shift/colorpicker/LineColorPicker.java @@ -0,0 +1,405 @@ +package uz.shift.colorpicker; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import org.solovyev.android.calculator.R; + + +public class LineColorPicker extends View { + + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + int[] colors = isInEditMode() ? Palette.DEFAULT : new int[1]; + // indicate if nothing selected + boolean isColorSelected = false; + private Paint paint; + private Rect rect = new Rect(); + private int selectedColor = colors[0]; + private OnColorChangedListener onColorChanged; + private int cellSize; + private int mOrientation = HORIZONTAL; + private boolean isClick = false; + private int screenW; + private int screenH; + + public LineColorPicker(Context context, AttributeSet attrs) { + super(context, attrs); + + paint = new Paint(); + paint.setStyle(Style.FILL); + + final TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.LineColorPicker, 0, 0); + + try { + mOrientation = a.getInteger(R.styleable.LineColorPicker_lcp_orientation, HORIZONTAL); + + if (!isInEditMode()) { + final int colorsArrayResId = a.getResourceId(R.styleable.LineColorPicker_lcp_colors, -1); + + if (colorsArrayResId > 0) { + final int[] colors = context.getResources().getIntArray(colorsArrayResId); + setColors(colors); + } + } + + final int selected = a.getInteger(R.styleable.LineColorPicker_lcp_selectedColorIndex, -1); + + if (selected != -1) { + final int[] currentColors = getColors(); + + final int currentColorsLength = currentColors != null ? currentColors.length : 0; + + if (selected < currentColorsLength) { + setSelectedColorPosition(selected); + } + } + } finally { + a.recycle(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mOrientation == HORIZONTAL) { + drawHorizontalPicker(canvas); + } else { + drawVerticalPicker(canvas); + } + + } + + private void drawVerticalPicker(Canvas canvas) { + rect.left = 0; + rect.top = 0; + rect.right = getWidth(); + rect.bottom = 0; + + // 8% + int margin = Math.round(getWidth() * 0.08f); + + for (int i = 0; i < colors.length; i++) { + + paint.setColor(colors[i]); + + rect.top = rect.bottom; + rect.bottom += cellSize; + + if (isColorSelected && colors[i] == selectedColor) { + rect.left = 0; + rect.right = getWidth(); + } else { + rect.left = margin; + rect.right = getWidth() - margin; + } + + canvas.drawRect(rect, paint); + } + + } + + private void drawHorizontalPicker(Canvas canvas) { + rect.left = 0; + rect.top = 0; + rect.right = 0; + rect.bottom = getHeight(); + + // 8% + int margin = Math.round(getHeight() * 0.08f); + + for (int i = 0; i < colors.length; i++) { + + paint.setColor(colors[i]); + + rect.left = rect.right; + rect.right += cellSize; + + if (isColorSelected && colors[i] == selectedColor) { + rect.top = 0; + rect.bottom = getHeight(); + } else { + rect.top = margin; + rect.bottom = getHeight() - margin; + } + + canvas.drawRect(rect, paint); + } + } + + private void onColorChanged(int color) { + if (onColorChanged != null) { + onColorChanged.onColorChanged(color); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + + int actionId = event.getAction(); + + int newColor; + + switch (actionId) { + case MotionEvent.ACTION_DOWN: + isClick = true; + break; + case MotionEvent.ACTION_UP: + newColor = getColorAtXY(event.getX(), event.getY()); + + setSelectedColor(newColor); + + if (isClick) { + performClick(); + } + + break; + + case MotionEvent.ACTION_MOVE: + newColor = getColorAtXY(event.getX(), event.getY()); + + setSelectedColor(newColor); + + break; + case MotionEvent.ACTION_CANCEL: + isClick = false; + break; + + case MotionEvent.ACTION_OUTSIDE: + isClick = false; + break; + + default: + break; + } + + return true; + } + + /** + * Return color at x,y coordinate of view. + */ + private int getColorAtXY(float x, float y) { + + // FIXME: colors.length == 0 -> devision by ZERO.s + + if (mOrientation == HORIZONTAL) { + int left = 0; + int right = 0; + + for (int i = 0; i < colors.length; i++) { + left = right; + right += cellSize; + + if (left <= x && right >= x) { + return colors[i]; + } + } + + } else { + int top = 0; + int bottom = 0; + + for (int i = 0; i < colors.length; i++) { + top = bottom; + bottom += cellSize; + + if (y >= top && y <= bottom) { + return colors[i]; + } + } + } + + return selectedColor; + } + + @Override + protected Parcelable onSaveInstanceState() { + // begin boilerplate code that allows parent classes to save state + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + // end + + ss.selectedColor = this.selectedColor; + ss.isColorSelected = this.isColorSelected; + + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + // begin boilerplate code so parent classes can restore state + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + // end + + this.selectedColor = ss.selectedColor; + this.isColorSelected = ss.isColorSelected; + } + + @Override + public boolean performClick() { + return super.performClick(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + + screenW = w; + screenH = h; + + recalcCellSize(); + + super.onSizeChanged(w, h, oldw, oldh); + } + + /** + * Return currently selected color. + */ + public int getColor() { + return selectedColor; + } + + // @Override + // protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // int parentWidth = MeasureSpec.getSize(widthMeasureSpec); + // int parentHeight = MeasureSpec.getSize(heightMeasureSpec); + // this.setMeasuredDimension(parentWidth, parentHeight); + // super.onMeasure(widthMeasureSpec, heightMeasureSpec); + // } + + /** + * Set selected color as color value from palette. + */ + public void setSelectedColor(int color) { + + // not from current palette + if (!containsColor(colors, color)) { + return; + } + + // do we need to re-draw view? + if (!isColorSelected || selectedColor != color) { + this.selectedColor = color; + + isColorSelected = true; + + invalidate(); + + onColorChanged(color); + } + } + + /** + * Set selected color as index from palete + */ + public void setSelectedColorPosition(int position) { + setSelectedColor(colors[position]); + } + + private int recalcCellSize() { + + if (mOrientation == HORIZONTAL) { + cellSize = Math.round(screenW / (colors.length * 1f)); + } else { + cellSize = Math.round(screenH / (colors.length * 1f)); + } + + return cellSize; + } + + /** + * Return current picker palete + */ + public int[] getColors() { + return colors; + } + + /** + * Set picker palette + */ + public void setColors(int[] colors) { + // TODO: selected color can be NOT in set of colors + // FIXME: colors can be null + this.colors = colors; + + if (!containsColor(colors, selectedColor)) { + selectedColor = colors[0]; + } + + recalcCellSize(); + + invalidate(); + } + + /** + * Return true if palette contains this color + */ + private boolean containsColor(int[] colors, int c) { + for (int i = 0; i < colors.length; i++) { + if (colors[i] == c) + return true; + + } + + return false; + } + + /** + * Set onColorChanged listener + * + * @param l + */ + public void setOnColorChangedListener(OnColorChangedListener l) { + this.onColorChanged = l; + } + + static class SavedState extends BaseSavedState { + // required field that makes Parcelables from a Parcel + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + int selectedColor; + boolean isColorSelected; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + this.selectedColor = in.readInt(); + this.isColorSelected = in.readInt() == 1; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(this.selectedColor); + out.writeInt(this.isColorSelected ? 1 : 0); + } + } +} diff --git a/app/src/main/java/uz/shift/colorpicker/OnColorChangedListener.java b/app/src/main/java/uz/shift/colorpicker/OnColorChangedListener.java new file mode 100644 index 00000000..a6207754 --- /dev/null +++ b/app/src/main/java/uz/shift/colorpicker/OnColorChangedListener.java @@ -0,0 +1,5 @@ +package uz.shift.colorpicker; + +public interface OnColorChangedListener { + void onColorChanged(int c); +} diff --git a/app/src/main/java/uz/shift/colorpicker/Palette.java b/app/src/main/java/uz/shift/colorpicker/Palette.java new file mode 100644 index 00000000..9cffad4d --- /dev/null +++ b/app/src/main/java/uz/shift/colorpicker/Palette.java @@ -0,0 +1,24 @@ +package uz.shift.colorpicker; + +import android.graphics.Color; + +public class Palette { + + public static int[] DEFAULT; + + static { + + DEFAULT = new int[]{Color.parseColor("#b8c847"), + Color.parseColor("#67bb43"), Color.parseColor("#41b691"), + Color.parseColor("#4182b6"), Color.parseColor("#4149b6"), + Color.parseColor("#7641b6"), Color.parseColor("#b741a7"), + Color.parseColor("#c54657"), Color.parseColor("#d1694a"), + Color.parseColor("#d1904a"), Color.parseColor("#d1c54a")}; + + } + + private Palette() { + + } + +} diff --git a/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png new file mode 100644 index 00000000..f8f7e7dd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_mode_edit_white_18dp.png b/app/src/main/res/drawable-hdpi/ic_mode_edit_white_18dp.png new file mode 100644 index 00000000..6bd7ffdb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_mode_edit_white_18dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_straighten_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_straighten_white_24dp.png new file mode 100644 index 00000000..8ec61b1d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_straighten_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png new file mode 100644 index 00000000..15d8fc2b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_mode_edit_white_18dp.png b/app/src/main/res/drawable-mdpi/ic_mode_edit_white_18dp.png new file mode 100644 index 00000000..4c907967 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_mode_edit_white_18dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_straighten_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_straighten_white_24dp.png new file mode 100644 index 00000000..17bfb30f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_straighten_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png new file mode 100644 index 00000000..2b725397 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_mode_edit_white_18dp.png b/app/src/main/res/drawable-xhdpi/ic_mode_edit_white_18dp.png new file mode 100644 index 00000000..595ff10a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_mode_edit_white_18dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_straighten_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_straighten_white_24dp.png new file mode 100644 index 00000000..84aa2626 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_straighten_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png new file mode 100644 index 00000000..4d2807e4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_mode_edit_white_18dp.png b/app/src/main/res/drawable-xxhdpi/ic_mode_edit_white_18dp.png new file mode 100644 index 00000000..261cad9b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_mode_edit_white_18dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_straighten_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_straighten_white_24dp.png new file mode 100644 index 00000000..1e6c2721 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_straighten_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png new file mode 100644 index 00000000..2a6d3b04 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_mode_edit_white_18dp.png b/app/src/main/res/drawable-xxxhdpi/ic_mode_edit_white_18dp.png new file mode 100644 index 00000000..02e19d04 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_mode_edit_white_18dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_straighten_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_straighten_white_24dp.png new file mode 100644 index 00000000..02785ff7 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_straighten_white_24dp.png differ diff --git a/app/src/main/res/layout/dialog_functions.xml b/app/src/main/res/layout/dialog_functions.xml new file mode 100644 index 00000000..52a242a8 --- /dev/null +++ b/app/src/main/res/layout/dialog_functions.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_functions_function.xml b/app/src/main/res/layout/dialog_functions_function.xml new file mode 100644 index 00000000..5285b60c --- /dev/null +++ b/app/src/main/res/layout/dialog_functions_function.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_function_edit.xml b/app/src/main/res/layout/fragment_function_edit.xml index dc8303cf..2ecaf214 100644 --- a/app/src/main/res/layout/fragment_function_edit.xml +++ b/app/src/main/res/layout/fragment_function_edit.xml @@ -31,51 +31,8 @@ a:layout_height="wrap_content" a:orientation="vertical"> - - - - - - - - - - - - - - - + diff --git a/app/src/main/res/layout/fragment_function_edit_base_controls.xml b/app/src/main/res/layout/fragment_function_edit_base_controls.xml new file mode 100644 index 00000000..8bf8eb42 --- /dev/null +++ b/app/src/main/res/layout/fragment_function_edit_base_controls.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_plot.xml b/app/src/main/res/layout/fragment_plot.xml new file mode 100644 index 00000000..5f021cb9 --- /dev/null +++ b/app/src/main/res/layout/fragment_plot.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_plot_dimensions.xml b/app/src/main/res/layout/fragment_plot_dimensions.xml new file mode 100644 index 00000000..a874233b --- /dev/null +++ b/app/src/main/res/layout/fragment_plot_dimensions.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_plot_function_edit.xml b/app/src/main/res/layout/fragment_plot_function_edit.xml new file mode 100644 index 00000000..cec2f3c8 --- /dev/null +++ b/app/src/main/res/layout/fragment_plot_function_edit.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/plot.xml b/app/src/main/res/menu/plot.xml index 228840db..c5ad9a8a 100644 --- a/app/src/main/res/menu/plot.xml +++ b/app/src/main/res/menu/plot.xml @@ -37,12 +37,6 @@ a:title="@string/cpp_plot_3d" app:showAsAction="ifRoom" /> - - + + + + + + + + + + + + \ 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 2fdc7706..73b3b010 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -42,4 +42,6 @@ 5dp 400dp 4dp + + 48dp \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 14cbeee7..a28a80c2 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -167,14 +167,6 @@ horizontal - - + + + + + + + + + + diff --git a/app/src/main/res/values/text_non_translatable.xml b/app/src/main/res/values/text_non_translatable.xml index 7cccfbb5..27234069 100644 --- a/app/src/main/res/values/text_non_translatable.xml +++ b/app/src/main/res/values/text_non_translatable.xml @@ -6,4 +6,12 @@ 0.3;0.3;0.3;0.25 + + + + + 0 + + X min + X max + Y min + Y max \ 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 f2621e50..1e084ba3 100644 --- a/app/src/main/res/values/text_strings.xml +++ b/app/src/main/res/values/text_strings.xml @@ -239,4 +239,6 @@ System language Missing permission Please enable \"%1$s\" permission in system settings + Line width + Line color