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