/*
* 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;
import android.app.Application;
import android.content.SharedPreferences;
import android.text.TextUtils;
import com.squareup.otto.Bus;
import com.squareup.otto.Subscribe;
import org.solovyev.android.Check;
import org.solovyev.android.calculator.history.HistoryState;
import org.solovyev.android.calculator.history.RecentHistory;
import org.solovyev.android.calculator.math.MathType;
import org.solovyev.android.calculator.memory.Memory;
import org.solovyev.android.calculator.text.TextProcessorEditorResult;
import org.solovyev.android.calculator.view.EditorTextProcessor;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import static java.lang.Math.min;
@Singleton
public class Editor {
@Nullable
private final EditorTextProcessor textProcessor;
@Nullable
private EditorView view;
@Nonnull
private EditorState state = EditorState.empty();
@Inject
Bus bus;
@Inject
Engine engine;
@Inject
public Editor(@Nonnull Application application, @Nonnull SharedPreferences preferences, @Nonnull Engine engine) {
textProcessor = new EditorTextProcessor(application, preferences, engine);
}
public void init() {
bus.register(this);
}
public static int clamp(int selection, @Nonnull CharSequence text) {
return clamp(selection, text.length());
}
public static int clamp(int selection, int max) {
return min(Math.max(selection, 0), max);
}
public void setView(@Nonnull EditorView view) {
Check.isMainThread();
this.view = view;
this.view.setState(state);
this.view.setEditor(this);
}
public void clearView(@Nonnull EditorView view) {
Check.isMainThread();
if (this.view == view) {
this.view.setEditor(null);
this.view = null;
}
}
@Nonnull
public EditorState getState() {
return state;
}
@Nonnull
public EditorState onTextChanged(@Nonnull EditorState newState) {
return onTextChanged(newState, false);
}
@Nonnull
public EditorState onTextChanged(@Nonnull EditorState newState, boolean force) {
Check.isMainThread();
if (textProcessor != null) {
final TextProcessorEditorResult result = textProcessor.process(newState.getTextString());
newState = EditorState.create(result.getCharSequence(), newState.selection + result.getOffset());
}
final EditorState oldState = state;
state = newState;
if (view != null) {
view.setState(newState);
}
bus.post(new ChangedEvent(oldState, newState, force));
return state;
}
@Nonnull
private EditorState onSelectionChanged(@Nonnull EditorState newState) {
Check.isMainThread();
state = newState;
if (view != null) {
view.setState(newState);
}
bus.post(new CursorMovedEvent(newState));
return state;
}
@Nonnull
public EditorState setState(@Nonnull EditorState state) {
Check.isMainThread();
return onTextChanged(state);
}
@Nonnull
private EditorState newSelectionViewState(int newSelection) {
Check.isMainThread();
if (state.selection == newSelection) {
return state;
}
return onSelectionChanged(EditorState.forNewSelection(state, newSelection));
}
@Nonnull
public EditorState setCursorOnStart() {
Check.isMainThread();
return newSelectionViewState(0);
}
@Nonnull
public EditorState setCursorOnEnd() {
Check.isMainThread();
return newSelectionViewState(state.text.length());
}
@Nonnull
public EditorState moveCursorLeft() {
Check.isMainThread();
if (state.selection <= 0) {
return state;
}
return newSelectionViewState(state.selection - 1);
}
@Nonnull
public EditorState moveCursorRight() {
Check.isMainThread();
if (state.selection >= state.text.length()) {
return state;
}
return newSelectionViewState(state.selection + 1);
}
@Nonnull
public EditorState erase() {
Check.isMainThread();
final int selection = state.selection;
final String text = state.getTextString();
if (selection <= 0 || text.length() <= 0 || selection > text.length()) {
return state;
}
int removeStart = selection - 1;
if (MathType.getType(text, selection - 1, false, engine).type == MathType.grouping_separator) {
// we shouldn't remove just separator as it will be re-added after the evaluation is done. Remove the digit
// before
removeStart -= 1;
}
final String newText = text.substring(0, removeStart) + text.substring(selection, text.length());
return onTextChanged(EditorState.create(newText, removeStart));
}
@Nonnull
public EditorState clear() {
Check.isMainThread();
return setText("");
}
@Nonnull
public EditorState setText(@Nonnull String text) {
Check.isMainThread();
return onTextChanged(EditorState.create(text, text.length()));
}
@Nonnull
public EditorState setText(@Nonnull String text, int selection) {
Check.isMainThread();
return onTextChanged(EditorState.create(text, clamp(selection, text)));
}
@Nonnull
public EditorState insert(@Nonnull String text) {
Check.isMainThread();
return insert(text, 0);
}
@Nonnull
public EditorState insert(@Nonnull String text, int selectionOffset) {
Check.isMainThread();
if (TextUtils.isEmpty(text) && selectionOffset == 0) {
return state;
}
final String oldText = state.getTextString();
final int selection = clamp(state.selection, oldText);
final int newTextLength = text.length() + oldText.length();
final int newSelection = clamp(text.length() + selection + selectionOffset, newTextLength);
final String newText = oldText.substring(0, selection) + text + oldText.substring(selection);
return onTextChanged(EditorState.create(newText, newSelection));
}
@Nonnull
public EditorState moveSelection(int offset) {
Check.isMainThread();
return setSelection(state.selection + offset);
}
@Nonnull
public EditorState setSelection(int selection) {
Check.isMainThread();
if (state.selection == selection) {
return state;
}
return onSelectionChanged(EditorState.forNewSelection(state, clamp(selection, state.text)));
}
@Subscribe
public void onEngineChanged(@Nonnull Engine.ChangedEvent e) {
// this will effectively apply new formatting (if f.e. grouping separator has changed) and
// will start new evaluation
onTextChanged(getState(), true);
}
@Subscribe
public void onMemoryValueReady(@Nonnull Memory.ValueReadyEvent e) {
insert(e.value);
}
public void onHistoryLoaded(@Nonnull RecentHistory history) {
if (!state.isEmpty()) {
return;
}
final HistoryState state = history.getCurrent();
if (state == null) {
return;
}
setState(state.editor);
}
public static class ChangedEvent {
@Nonnull
public final EditorState oldState;
@Nonnull
public final EditorState newState;
public final boolean force;
private ChangedEvent(@Nonnull EditorState oldState, @Nonnull EditorState newState, boolean force) {
this.oldState = oldState;
this.newState = newState;
this.force = force;
}
boolean shouldEvaluate() {
return force || !TextUtils.equals(newState.text, oldState.text);
}
}
public static class CursorMovedEvent {
@Nonnull
public final EditorState state;
public CursorMovedEvent(@Nonnull EditorState state) {
this.state = state;
}
}
}