diff --git a/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorDisplayImpl.java b/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorDisplayImpl.java index 0bb47562..a5318509 100644 --- a/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorDisplayImpl.java +++ b/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorDisplayImpl.java @@ -1,170 +1,183 @@ -package org.solovyev.android.calculator; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import static org.solovyev.android.calculator.CalculatorEventType.*; - -/** - * User: serso - * Date: 9/20/12 - * Time: 8:24 PM - */ -public class CalculatorDisplayImpl implements CalculatorDisplay { - - @NotNull - private CalculatorEventData lastCalculatorEventData; - - @Nullable - private CalculatorDisplayView view; - - @NotNull - private final Object viewLock = new Object(); - - @NotNull - private CalculatorDisplayViewState viewState = CalculatorDisplayViewStateImpl.newDefaultInstance(); - - @NotNull - private final Calculator calculator; - - public CalculatorDisplayImpl(@NotNull Calculator calculator) { - this.calculator = calculator; - this.lastCalculatorEventData = CalculatorUtils.createFirstEventDataId(); - this.calculator.addCalculatorEventListener(this); - } - - @Override - public void setView(@Nullable CalculatorDisplayView view) { - synchronized (viewLock) { - this.view = view; - - if (view != null) { - this.view.setState(viewState); - } - } - } - - @Nullable - @Override - public CalculatorDisplayView getView() { - return this.view; - } - - @NotNull - @Override - public CalculatorDisplayViewState getViewState() { - return this.viewState; - } - - @Override - public void setViewState(@NotNull CalculatorDisplayViewState newViewState) { - synchronized (viewLock) { - final CalculatorDisplayViewState oldViewState = setViewState0(newViewState); - - this.calculator.fireCalculatorEvent(display_state_changed, new CalculatorDisplayChangeEventDataImpl(oldViewState, newViewState)); - } - } - - private void setViewStateForSequence(@NotNull CalculatorDisplayViewState newViewState, @NotNull Long sequenceId) { - synchronized (viewLock) { - final CalculatorDisplayViewState oldViewState = setViewState0(newViewState); - - this.calculator.fireCalculatorEvent(display_state_changed, new CalculatorDisplayChangeEventDataImpl(oldViewState, newViewState), sequenceId); - } - } - - // must be synchronized with viewLock - @NotNull - private CalculatorDisplayViewState setViewState0(@NotNull CalculatorDisplayViewState newViewState) { - final CalculatorDisplayViewState oldViewState = this.viewState; - - this.viewState = newViewState; - if (this.view != null) { - this.view.setState(newViewState); - } - return oldViewState; - } - - @Override - @NotNull - public CalculatorEventData getLastEventData() { - return lastCalculatorEventData; - } - - @Override - public void onCalculatorEvent(@NotNull CalculatorEventData calculatorEventData, - @NotNull CalculatorEventType calculatorEventType, - @Nullable Object data) { - if (calculatorEventType.isOfType(calculation_result, calculation_failed, calculation_cancelled, conversion_result, conversion_failed)) { - - if (calculatorEventData.isAfter(lastCalculatorEventData)) { - lastCalculatorEventData = calculatorEventData; - } - - switch (calculatorEventType) { - case conversion_failed: - processConversationFailed((CalculatorConversionEventData) calculatorEventData, (ConversionFailure) data); - break; - case conversion_result: - processConversationResult((CalculatorConversionEventData)calculatorEventData, (String)data); - break; - case calculation_result: - processCalculationResult((CalculatorEvaluationEventData) calculatorEventData, (CalculatorOutput) data); - break; - case calculation_cancelled: - processCalculationCancelled((CalculatorEvaluationEventData)calculatorEventData); - break; - case calculation_failed: - processCalculationFailed((CalculatorEvaluationEventData)calculatorEventData, (CalculatorFailure) data); - break; - } - - } - } - - private void processConversationFailed(@NotNull CalculatorConversionEventData calculatorEventData, - @NotNull ConversionFailure data) { - this.setViewStateForSequence(CalculatorDisplayViewStateImpl.newErrorState(calculatorEventData.getDisplayState().getOperation(), CalculatorMessages.getBundle().getString(CalculatorMessages.syntax_error)), calculatorEventData.getSequenceId()); - - } - - private void processCalculationFailed(@NotNull CalculatorEvaluationEventData calculatorEventData, @NotNull CalculatorFailure data) { - - final CalculatorEvalException calculatorEvalException = data.getCalculationEvalException(); - - final String errorMessage; - if (calculatorEvalException != null) { - errorMessage = CalculatorMessages.getBundle().getString(CalculatorMessages.syntax_error); - } else { - final CalculatorParseException calculationParseException = data.getCalculationParseException(); - if (calculationParseException != null) { - errorMessage = calculationParseException.getLocalizedMessage(); - } else { - errorMessage = CalculatorMessages.getBundle().getString(CalculatorMessages.syntax_error); - } - } - - this.setViewStateForSequence(CalculatorDisplayViewStateImpl.newErrorState(calculatorEventData.getOperation(), errorMessage), calculatorEventData.getSequenceId()); - } - - private void processCalculationCancelled(@NotNull CalculatorEvaluationEventData calculatorEventData) { - final String errorMessage = CalculatorMessages.getBundle().getString(CalculatorMessages.syntax_error); - - this.setViewStateForSequence(CalculatorDisplayViewStateImpl.newErrorState(calculatorEventData.getOperation(), errorMessage), calculatorEventData.getSequenceId()); - } - - private void processCalculationResult(@NotNull CalculatorEvaluationEventData calculatorEventData, @NotNull CalculatorOutput data) { - final String stringResult = data.getStringResult(); - this.setViewStateForSequence(CalculatorDisplayViewStateImpl.newValidState(calculatorEventData.getOperation(), data.getResult(), stringResult, 0), calculatorEventData.getSequenceId()); - } - - private void processConversationResult(@NotNull CalculatorConversionEventData calculatorEventData, @NotNull String result) { - // add prefix - if (calculatorEventData.getFromNumeralBase() != calculatorEventData.getToNumeralBase()) { - result = calculatorEventData.getToNumeralBase().getJsclPrefix() + result; - } - - final CalculatorDisplayViewState displayState = calculatorEventData.getDisplayState(); - this.setViewStateForSequence(CalculatorDisplayViewStateImpl.newValidState(displayState.getOperation(), displayState.getResult(), result, 0), calculatorEventData.getSequenceId()); - } -} +package org.solovyev.android.calculator; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.solovyev.android.calculator.CalculatorEventType.*; + +/** + * User: serso + * Date: 9/20/12 + * Time: 8:24 PM + */ +public class CalculatorDisplayImpl implements CalculatorDisplay { + + @NotNull + private volatile CalculatorEventData lastCalculatorEventData; + + @NotNull + private final Object lastCalculatorEventDataLock = new Object(); + + @Nullable + private CalculatorDisplayView view; + + @NotNull + private final Object viewLock = new Object(); + + @NotNull + private CalculatorDisplayViewState viewState = CalculatorDisplayViewStateImpl.newDefaultInstance(); + + @NotNull + private final Calculator calculator; + + public CalculatorDisplayImpl(@NotNull Calculator calculator) { + this.calculator = calculator; + this.lastCalculatorEventData = CalculatorUtils.createFirstEventDataId(); + this.calculator.addCalculatorEventListener(this); + } + + @Override + public void setView(@Nullable CalculatorDisplayView view) { + synchronized (viewLock) { + this.view = view; + + if (view != null) { + this.view.setState(viewState); + } + } + } + + @Nullable + @Override + public CalculatorDisplayView getView() { + return this.view; + } + + @NotNull + @Override + public CalculatorDisplayViewState getViewState() { + return this.viewState; + } + + @Override + public void setViewState(@NotNull CalculatorDisplayViewState newViewState) { + synchronized (viewLock) { + final CalculatorDisplayViewState oldViewState = setViewState0(newViewState); + + this.calculator.fireCalculatorEvent(display_state_changed, new CalculatorDisplayChangeEventDataImpl(oldViewState, newViewState)); + } + } + + private void setViewStateForSequence(@NotNull CalculatorDisplayViewState newViewState, @NotNull Long sequenceId) { + synchronized (viewLock) { + final CalculatorDisplayViewState oldViewState = setViewState0(newViewState); + + this.calculator.fireCalculatorEvent(display_state_changed, new CalculatorDisplayChangeEventDataImpl(oldViewState, newViewState), sequenceId); + } + } + + // must be synchronized with viewLock + @NotNull + private CalculatorDisplayViewState setViewState0(@NotNull CalculatorDisplayViewState newViewState) { + final CalculatorDisplayViewState oldViewState = this.viewState; + + this.viewState = newViewState; + if (this.view != null) { + this.view.setState(newViewState); + } + return oldViewState; + } + + @Override + @NotNull + public CalculatorEventData getLastEventData() { + synchronized (lastCalculatorEventDataLock) { + return lastCalculatorEventData; + } + } + + @Override + public void onCalculatorEvent(@NotNull CalculatorEventData calculatorEventData, + @NotNull CalculatorEventType calculatorEventType, + @Nullable Object data) { + if (calculatorEventType.isOfType(calculation_result, calculation_failed, calculation_cancelled, conversion_result, conversion_failed)) { + + boolean processEvent = false; + boolean sameSequence = false; + + synchronized (lastCalculatorEventDataLock) { + if (calculatorEventData.isAfter(lastCalculatorEventData)) { + sameSequence = calculatorEventData.isSameSequence(lastCalculatorEventData); + lastCalculatorEventData = calculatorEventData; + processEvent = true; + } + } + + if (processEvent) { + switch (calculatorEventType) { + case conversion_failed: + processConversationFailed((CalculatorConversionEventData) calculatorEventData, (ConversionFailure) data); + break; + case conversion_result: + processConversationResult((CalculatorConversionEventData)calculatorEventData, (String)data); + break; + case calculation_result: + processCalculationResult((CalculatorEvaluationEventData) calculatorEventData, (CalculatorOutput) data); + break; + case calculation_cancelled: + processCalculationCancelled((CalculatorEvaluationEventData)calculatorEventData); + break; + case calculation_failed: + processCalculationFailed((CalculatorEvaluationEventData)calculatorEventData, (CalculatorFailure) data); + break; + } + } + } + } + + private void processConversationFailed(@NotNull CalculatorConversionEventData calculatorEventData, + @NotNull ConversionFailure data) { + this.setViewStateForSequence(CalculatorDisplayViewStateImpl.newErrorState(calculatorEventData.getDisplayState().getOperation(), CalculatorMessages.getBundle().getString(CalculatorMessages.syntax_error)), calculatorEventData.getSequenceId()); + + } + + private void processCalculationFailed(@NotNull CalculatorEvaluationEventData calculatorEventData, @NotNull CalculatorFailure data) { + + final CalculatorEvalException calculatorEvalException = data.getCalculationEvalException(); + + final String errorMessage; + if (calculatorEvalException != null) { + errorMessage = CalculatorMessages.getBundle().getString(CalculatorMessages.syntax_error); + } else { + final CalculatorParseException calculationParseException = data.getCalculationParseException(); + if (calculationParseException != null) { + errorMessage = calculationParseException.getLocalizedMessage(); + } else { + errorMessage = CalculatorMessages.getBundle().getString(CalculatorMessages.syntax_error); + } + } + + this.setViewStateForSequence(CalculatorDisplayViewStateImpl.newErrorState(calculatorEventData.getOperation(), errorMessage), calculatorEventData.getSequenceId()); + } + + private void processCalculationCancelled(@NotNull CalculatorEvaluationEventData calculatorEventData) { + final String errorMessage = CalculatorMessages.getBundle().getString(CalculatorMessages.syntax_error); + + this.setViewStateForSequence(CalculatorDisplayViewStateImpl.newErrorState(calculatorEventData.getOperation(), errorMessage), calculatorEventData.getSequenceId()); + } + + private void processCalculationResult(@NotNull CalculatorEvaluationEventData calculatorEventData, @NotNull CalculatorOutput data) { + final String stringResult = data.getStringResult(); + this.setViewStateForSequence(CalculatorDisplayViewStateImpl.newValidState(calculatorEventData.getOperation(), data.getResult(), stringResult, 0), calculatorEventData.getSequenceId()); + } + + private void processConversationResult(@NotNull CalculatorConversionEventData calculatorEventData, @NotNull String result) { + // add prefix + if (calculatorEventData.getFromNumeralBase() != calculatorEventData.getToNumeralBase()) { + result = calculatorEventData.getToNumeralBase().getJsclPrefix() + result; + } + + final CalculatorDisplayViewState displayState = calculatorEventData.getDisplayState(); + this.setViewStateForSequence(CalculatorDisplayViewStateImpl.newValidState(displayState.getOperation(), displayState.getResult(), result, 0), calculatorEventData.getSequenceId()); + } +} diff --git a/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorEditor.java b/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorEditor.java index 9a40aa9d..c80b2489 100644 --- a/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorEditor.java +++ b/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorEditor.java @@ -16,6 +16,9 @@ public interface CalculatorEditor extends CalculatorEventListener { @NotNull CalculatorEditorViewState getViewState(); + // updates state of view (view.setState()) + void updateViewState(); + void setViewState(@NotNull CalculatorEditorViewState viewState); /* diff --git a/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorEditorImpl.java b/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorEditorImpl.java index c918c0a7..40445dbe 100644 --- a/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorEditorImpl.java +++ b/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorEditorImpl.java @@ -48,8 +48,17 @@ public class CalculatorEditorImpl implements CalculatorEditor { return lastViewState; } + @Override + public void updateViewState() { + setViewState(this.lastViewState, false); + } + @Override public void setViewState(@NotNull CalculatorEditorViewState newViewState) { + setViewState(newViewState, true); + } + + private void setViewState(CalculatorEditorViewState newViewState, boolean fireEvent) { synchronized (viewLock) { final CalculatorEditorViewState oldViewState = this.lastViewState; @@ -58,7 +67,9 @@ public class CalculatorEditorImpl implements CalculatorEditor { this.view.setState(newViewState); } - calculator.fireCalculatorEvent(CalculatorEventType.editor_state_changed, new CalculatorEditorChangeEventDataImpl(oldViewState, newViewState)); + if (fireEvent) { + calculator.fireCalculatorEvent(CalculatorEventType.editor_state_changed, new CalculatorEditorChangeEventDataImpl(oldViewState, newViewState)); + } } } diff --git a/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorImpl.java b/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorImpl.java index fadd3a33..40798619 100644 --- a/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorImpl.java +++ b/calculatorpp-core/src/main/java/org/solovyev/android/calculator/CalculatorImpl.java @@ -1,433 +1,436 @@ -package org.solovyev.android.calculator; - -import jscl.AbstractJsclArithmeticException; -import jscl.NumeralBase; -import jscl.NumeralBaseException; -import jscl.math.Generic; -import jscl.text.ParseInterruptedException; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.solovyev.android.calculator.history.CalculatorHistory; -import org.solovyev.android.calculator.history.CalculatorHistoryState; -import org.solovyev.android.calculator.jscl.JsclOperation; -import org.solovyev.android.calculator.text.TextProcessor; -import org.solovyev.android.calculator.units.CalculatorNumeralBase; -import org.solovyev.common.history.HistoryAction; -import org.solovyev.common.msg.MessageRegistry; -import org.solovyev.common.msg.MessageType; -import org.solovyev.common.text.StringUtils; -import org.solovyev.math.units.*; - -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicLong; - -/** - * User: Solovyev_S - * Date: 20.09.12 - * Time: 16:42 - */ -public class CalculatorImpl implements Calculator, CalculatorEventListener { - - @NotNull - private final CalculatorEventContainer calculatorEventContainer = new ListCalculatorEventContainer(); - - @NotNull - private final AtomicLong counter = new AtomicLong(CalculatorUtils.FIRST_ID); - - @NotNull - private final TextProcessor preprocessor = ToJsclTextProcessor.getInstance(); - - @NotNull - private final Executor threadPoolExecutor = Executors.newFixedThreadPool(10); - - public CalculatorImpl() { - this.addCalculatorEventListener(this); - } - - @NotNull - private CalculatorEventData nextEventData() { - long eventId = counter.incrementAndGet(); - return CalculatorEventDataImpl.newInstance(eventId, eventId); - } - - @NotNull - private CalculatorEventData nextEventData(@NotNull Long sequenceId) { - long eventId = counter.incrementAndGet(); - return CalculatorEventDataImpl.newInstance(eventId, sequenceId); - } - - /* - ********************************************************************** - * - * CALCULATION - * - ********************************************************************** - */ - - @Override - public void evaluate() { - final CalculatorEditorViewState viewState = getEditor().getViewState(); - fireCalculatorEvent(CalculatorEventType.manual_calculation_requested, viewState); - this.evaluate(JsclOperation.numeric, viewState.getText()); - } - - @Override - public void evaluate(@NotNull Long sequenceId) { - final CalculatorEditorViewState viewState = getEditor().getViewState(); - fireCalculatorEvent(CalculatorEventType.manual_calculation_requested, viewState, sequenceId); - this.evaluate(JsclOperation.numeric, viewState.getText(), sequenceId); - } - - @Override - public void simplify() { - final CalculatorEditorViewState viewState = getEditor().getViewState(); - fireCalculatorEvent(CalculatorEventType.manual_calculation_requested, viewState); - this.evaluate(JsclOperation.simplify, viewState.getText()); - } - - @NotNull - @Override - public CalculatorEventData evaluate(@NotNull final JsclOperation operation, - @NotNull final String expression) { - - final CalculatorEventData eventDataId = nextEventData(); - - threadPoolExecutor.execute(new Runnable() { - @Override - public void run() { - CalculatorImpl.this.evaluate(eventDataId.getSequenceId(), operation, expression, null); - } - }); - - return eventDataId; - } - - @NotNull - @Override - public CalculatorEventData evaluate(@NotNull final JsclOperation operation, @NotNull final String expression, @NotNull Long sequenceId) { - final CalculatorEventData eventDataId = nextEventData(sequenceId); - - threadPoolExecutor.execute(new Runnable() { - @Override - public void run() { - CalculatorImpl.this.evaluate(eventDataId.getSequenceId(), operation, expression, null); - } - }); - - return eventDataId; - } - - @Override - public void init() { - CalculatorLocatorImpl.getInstance().getEngine().init(); - CalculatorLocatorImpl.getInstance().getHistory().load(); - } - - @NotNull - private CalculatorConversionEventData newConversionEventData(@NotNull Long sequenceId, - @NotNull Generic value, - @NotNull NumeralBase from, - @NotNull NumeralBase to, - @NotNull CalculatorDisplayViewState displayViewState) { - return CalculatorConversionEventDataImpl.newInstance(nextEventData(sequenceId), value, from, to, displayViewState); - } - - private void evaluate(@NotNull Long sequenceId, - @NotNull JsclOperation operation, - @NotNull String expression, - @Nullable MessageRegistry mr) { - - PreparedExpression preparedExpression = null; - - fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_started, new CalculatorInputImpl(expression, operation)); - - try { - - expression = expression.trim(); - - if (StringUtils.isEmpty(expression)) { - fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_result, CalculatorOutputImpl.newEmptyOutput(operation)); - } else { - preparedExpression = preprocessor.process(expression); - - final String jsclExpression = preparedExpression.toString(); - - try { - - final Generic result = operation.evaluateGeneric(jsclExpression, CalculatorLocatorImpl.getInstance().getEngine().getMathEngine()); - - // NOTE: toString() method must be called here as ArithmeticOperationException may occur in it (just to avoid later check!) - result.toString(); - - final CalculatorOutput data = CalculatorOutputImpl.newOutput(operation.getFromProcessor().process(result), operation, result); - fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_result, data); - - } catch (AbstractJsclArithmeticException e) { - handleException(sequenceId, operation, expression, mr, new CalculatorEvalException(e, e, jsclExpression)); - } - } - - } catch (ArithmeticException e) { - handleException(sequenceId, operation, expression, mr, preparedExpression, new CalculatorParseException(expression, new CalculatorMessage(CalculatorMessages.msg_001, MessageType.error, e.getMessage()))); - } catch (StackOverflowError e) { - handleException(sequenceId, operation, expression, mr, preparedExpression, new CalculatorParseException(expression, new CalculatorMessage(CalculatorMessages.msg_002, MessageType.error))); - } catch (jscl.text.ParseException e) { - handleException(sequenceId, operation, expression, mr, preparedExpression, new CalculatorParseException(e)); - } catch (ParseInterruptedException e) { - - // do nothing - we ourselves interrupt the calculations - fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_cancelled, null); - - } catch (CalculatorParseException e) { - handleException(sequenceId, operation, expression, mr, preparedExpression, e); - } finally { - fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_finished, null); - } - } - - @NotNull - private CalculatorEventData newCalculationEventData(@NotNull JsclOperation operation, - @NotNull String expression, - @NotNull Long calculationId) { - return new CalculatorEvaluationEventDataImpl(nextEventData(calculationId), operation, expression); - } - - private void handleException(@NotNull Long sequenceId, - @NotNull JsclOperation operation, - @NotNull String expression, - @Nullable MessageRegistry mr, - @Nullable PreparedExpression preparedExpression, - @NotNull CalculatorParseException parseException) { - - if (operation == JsclOperation.numeric - && preparedExpression != null - && preparedExpression.isExistsUndefinedVar()) { - - evaluate(sequenceId, JsclOperation.simplify, expression, mr); - } else { - - fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_failed, new CalculatorFailureImpl(parseException)); - } - } - - private void handleException(@NotNull Long calculationId, - @NotNull JsclOperation operation, - @NotNull String expression, - @Nullable MessageRegistry mr, - @NotNull CalculatorEvalException evalException) { - - if (operation == JsclOperation.numeric && evalException.getCause() instanceof NumeralBaseException) { - evaluate(calculationId, JsclOperation.simplify, expression, mr); - } - - fireCalculatorEvent(newCalculationEventData(operation, expression, calculationId), CalculatorEventType.calculation_failed, new CalculatorFailureImpl(evalException)); - } - - /* - ********************************************************************** - * - * CONVERSION - * - ********************************************************************** - */ - - @NotNull - @Override - public CalculatorEventData convert(@NotNull final Generic value, - @NotNull final NumeralBase to) { - final CalculatorEventData eventDataId = nextEventData(); - - final CalculatorDisplayViewState displayViewState = CalculatorLocatorImpl.getInstance().getDisplay().getViewState(); - final NumeralBase from = CalculatorLocatorImpl.getInstance().getEngine().getNumeralBase(); - - threadPoolExecutor.execute(new Runnable() { - @Override - public void run() { - final Long sequenceId = eventDataId.getSequenceId(); - - fireCalculatorEvent(newConversionEventData(sequenceId, value, from, to, displayViewState), CalculatorEventType.conversion_started, null); - try { - - final String result = doConversion(value, from, to); - - fireCalculatorEvent(newConversionEventData(sequenceId, value, from, to, displayViewState), CalculatorEventType.conversion_result, result); - - } catch (ConversionException e) { - fireCalculatorEvent(newConversionEventData(sequenceId, value, from, to, displayViewState), CalculatorEventType.conversion_failed, new ConversionFailureImpl(e)); - } - } - }); - - return eventDataId; - } - - @NotNull - private static String doConversion(@NotNull Generic generic, - @NotNull NumeralBase from, - @NotNull NumeralBase to) throws ConversionException { - final String result; - - if (from != to) { - String fromString = generic.toString(); - if (!StringUtils.isEmpty(fromString)) { - try { - fromString = ToJsclTextProcessor.getInstance().process(fromString).getExpression(); - } catch (CalculatorParseException e) { - // ok, problems while processing occurred - } - } - - result = ConversionUtils.doConversion(CalculatorNumeralBase.getConverter(), fromString, CalculatorNumeralBase.valueOf(from), CalculatorNumeralBase.valueOf(to)); - } else { - result = generic.toString(); - } - - return result; - } - - @Override - public boolean isConversionPossible(@NotNull Generic generic, NumeralBase from, @NotNull NumeralBase to) { - try { - doConversion(generic, from, to); - return true; - } catch (ConversionException e) { - return false; - } - } - - /* - ********************************************************************** - * - * EVENTS - * - ********************************************************************** - */ - - @Override - public void addCalculatorEventListener(@NotNull CalculatorEventListener calculatorEventListener) { - calculatorEventContainer.addCalculatorEventListener(calculatorEventListener); - } - - @Override - public void removeCalculatorEventListener(@NotNull CalculatorEventListener calculatorEventListener) { - calculatorEventContainer.removeCalculatorEventListener(calculatorEventListener); - } - - @Override - public void fireCalculatorEvent(@NotNull CalculatorEventData calculatorEventData, @NotNull CalculatorEventType calculatorEventType, @Nullable Object data) { - calculatorEventContainer.fireCalculatorEvent(calculatorEventData, calculatorEventType, data); - } - - @Override - public void fireCalculatorEvents(@NotNull List calculatorEvents) { - calculatorEventContainer.fireCalculatorEvents(calculatorEvents); - } - - @NotNull - @Override - public CalculatorEventData fireCalculatorEvent(@NotNull final CalculatorEventType calculatorEventType, @Nullable final Object data) { - final CalculatorEventData eventData = nextEventData(); - - threadPoolExecutor.execute(new Runnable() { - @Override - public void run() { - fireCalculatorEvent(eventData, calculatorEventType, data); - } - }); - - return eventData; - } - - @NotNull - @Override - public CalculatorEventData fireCalculatorEvent(@NotNull final CalculatorEventType calculatorEventType, @Nullable final Object data, @NotNull Long sequenceId) { - final CalculatorEventData eventData = nextEventData(sequenceId); - - threadPoolExecutor.execute(new Runnable() { - @Override - public void run() { - fireCalculatorEvent(eventData, calculatorEventType, data); - } - }); - - return eventData; - } - - /* - ********************************************************************** - * - * EVENTS HANDLER - * - ********************************************************************** - */ - - @Override - public void onCalculatorEvent(@NotNull CalculatorEventData calculatorEventData, @NotNull CalculatorEventType calculatorEventType, @Nullable Object data) { - - switch (calculatorEventType) { - case editor_state_changed: - final CalculatorEditorChangeEventData changeEventData = (CalculatorEditorChangeEventData) data; - - final String newText = changeEventData.getNewState().getText(); - final String oldText = changeEventData.getOldState().getText(); - - if (!newText.equals(oldText)) { - evaluate(JsclOperation.numeric, changeEventData.getNewState().getText(), calculatorEventData.getSequenceId()); - } - break; - case engine_preferences_changed: - evaluate(calculatorEventData.getSequenceId()); - break; - } - } - - /* - ********************************************************************** - * - * HISTORY - * - ********************************************************************** - */ - - @Override - public void doHistoryAction(@NotNull HistoryAction historyAction) { - final CalculatorHistory history = CalculatorLocatorImpl.getInstance().getHistory(); - if (history.isActionAvailable(historyAction)) { - final CalculatorHistoryState newState = history.doAction(historyAction, getCurrentHistoryState()); - if (newState != null) { - setCurrentHistoryState(newState); - } - } - } - - @Override - public void setCurrentHistoryState(@NotNull CalculatorHistoryState editorHistoryState) { - editorHistoryState.setValuesFromHistory(getEditor(), getDisplay()); - } - - @NotNull - @Override - public CalculatorHistoryState getCurrentHistoryState() { - return CalculatorHistoryState.newInstance(getEditor(), getDisplay()); - } - - /* - ********************************************************************** - * - * OTHER - * - ********************************************************************** - */ - - @NotNull - private CalculatorEditor getEditor() { - return CalculatorLocatorImpl.getInstance().getEditor(); - } - - @NotNull - private CalculatorDisplay getDisplay() { - return CalculatorLocatorImpl.getInstance().getDisplay(); - } -} +package org.solovyev.android.calculator; + +import jscl.AbstractJsclArithmeticException; +import jscl.NumeralBase; +import jscl.NumeralBaseException; +import jscl.math.Generic; +import jscl.text.ParseInterruptedException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.solovyev.android.calculator.history.CalculatorHistory; +import org.solovyev.android.calculator.history.CalculatorHistoryState; +import org.solovyev.android.calculator.jscl.JsclOperation; +import org.solovyev.android.calculator.text.TextProcessor; +import org.solovyev.android.calculator.units.CalculatorNumeralBase; +import org.solovyev.common.history.HistoryAction; +import org.solovyev.common.msg.MessageRegistry; +import org.solovyev.common.msg.MessageType; +import org.solovyev.common.text.StringUtils; +import org.solovyev.math.units.*; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; + +/** + * User: Solovyev_S + * Date: 20.09.12 + * Time: 16:42 + */ +public class CalculatorImpl implements Calculator, CalculatorEventListener { + + @NotNull + private final CalculatorEventContainer calculatorEventContainer = new ListCalculatorEventContainer(); + + @NotNull + private final AtomicLong counter = new AtomicLong(CalculatorUtils.FIRST_ID); + + @NotNull + private final TextProcessor preprocessor = ToJsclTextProcessor.getInstance(); + + @NotNull + private final Executor calculationsExecutor = Executors.newFixedThreadPool(10); + + @NotNull + private final Executor eventExecutor = Executors.newFixedThreadPool(1); + + public CalculatorImpl() { + this.addCalculatorEventListener(this); + } + + @NotNull + private CalculatorEventData nextEventData() { + long eventId = counter.incrementAndGet(); + return CalculatorEventDataImpl.newInstance(eventId, eventId); + } + + @NotNull + private CalculatorEventData nextEventData(@NotNull Long sequenceId) { + long eventId = counter.incrementAndGet(); + return CalculatorEventDataImpl.newInstance(eventId, sequenceId); + } + + /* + ********************************************************************** + * + * CALCULATION + * + ********************************************************************** + */ + + @Override + public void evaluate() { + final CalculatorEditorViewState viewState = getEditor().getViewState(); + fireCalculatorEvent(CalculatorEventType.manual_calculation_requested, viewState); + this.evaluate(JsclOperation.numeric, viewState.getText()); + } + + @Override + public void evaluate(@NotNull Long sequenceId) { + final CalculatorEditorViewState viewState = getEditor().getViewState(); + fireCalculatorEvent(CalculatorEventType.manual_calculation_requested, viewState, sequenceId); + this.evaluate(JsclOperation.numeric, viewState.getText(), sequenceId); + } + + @Override + public void simplify() { + final CalculatorEditorViewState viewState = getEditor().getViewState(); + fireCalculatorEvent(CalculatorEventType.manual_calculation_requested, viewState); + this.evaluate(JsclOperation.simplify, viewState.getText()); + } + + @NotNull + @Override + public CalculatorEventData evaluate(@NotNull final JsclOperation operation, + @NotNull final String expression) { + + final CalculatorEventData eventDataId = nextEventData(); + + calculationsExecutor.execute(new Runnable() { + @Override + public void run() { + CalculatorImpl.this.evaluate(eventDataId.getSequenceId(), operation, expression, null); + } + }); + + return eventDataId; + } + + @NotNull + @Override + public CalculatorEventData evaluate(@NotNull final JsclOperation operation, @NotNull final String expression, @NotNull Long sequenceId) { + final CalculatorEventData eventDataId = nextEventData(sequenceId); + + calculationsExecutor.execute(new Runnable() { + @Override + public void run() { + CalculatorImpl.this.evaluate(eventDataId.getSequenceId(), operation, expression, null); + } + }); + + return eventDataId; + } + + @Override + public void init() { + CalculatorLocatorImpl.getInstance().getEngine().init(); + CalculatorLocatorImpl.getInstance().getHistory().load(); + } + + @NotNull + private CalculatorConversionEventData newConversionEventData(@NotNull Long sequenceId, + @NotNull Generic value, + @NotNull NumeralBase from, + @NotNull NumeralBase to, + @NotNull CalculatorDisplayViewState displayViewState) { + return CalculatorConversionEventDataImpl.newInstance(nextEventData(sequenceId), value, from, to, displayViewState); + } + + private void evaluate(@NotNull Long sequenceId, + @NotNull JsclOperation operation, + @NotNull String expression, + @Nullable MessageRegistry mr) { + + PreparedExpression preparedExpression = null; + + fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_started, new CalculatorInputImpl(expression, operation)); + + try { + + expression = expression.trim(); + + if (StringUtils.isEmpty(expression)) { + fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_result, CalculatorOutputImpl.newEmptyOutput(operation)); + } else { + preparedExpression = preprocessor.process(expression); + + final String jsclExpression = preparedExpression.toString(); + + try { + + final Generic result = operation.evaluateGeneric(jsclExpression, CalculatorLocatorImpl.getInstance().getEngine().getMathEngine()); + + // NOTE: toString() method must be called here as ArithmeticOperationException may occur in it (just to avoid later check!) + result.toString(); + + final CalculatorOutput data = CalculatorOutputImpl.newOutput(operation.getFromProcessor().process(result), operation, result); + fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_result, data); + + } catch (AbstractJsclArithmeticException e) { + handleException(sequenceId, operation, expression, mr, new CalculatorEvalException(e, e, jsclExpression)); + } + } + + } catch (ArithmeticException e) { + handleException(sequenceId, operation, expression, mr, preparedExpression, new CalculatorParseException(expression, new CalculatorMessage(CalculatorMessages.msg_001, MessageType.error, e.getMessage()))); + } catch (StackOverflowError e) { + handleException(sequenceId, operation, expression, mr, preparedExpression, new CalculatorParseException(expression, new CalculatorMessage(CalculatorMessages.msg_002, MessageType.error))); + } catch (jscl.text.ParseException e) { + handleException(sequenceId, operation, expression, mr, preparedExpression, new CalculatorParseException(e)); + } catch (ParseInterruptedException e) { + + // do nothing - we ourselves interrupt the calculations + fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_cancelled, null); + + } catch (CalculatorParseException e) { + handleException(sequenceId, operation, expression, mr, preparedExpression, e); + } finally { + fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_finished, null); + } + } + + @NotNull + private CalculatorEventData newCalculationEventData(@NotNull JsclOperation operation, + @NotNull String expression, + @NotNull Long calculationId) { + return new CalculatorEvaluationEventDataImpl(nextEventData(calculationId), operation, expression); + } + + private void handleException(@NotNull Long sequenceId, + @NotNull JsclOperation operation, + @NotNull String expression, + @Nullable MessageRegistry mr, + @Nullable PreparedExpression preparedExpression, + @NotNull CalculatorParseException parseException) { + + if (operation == JsclOperation.numeric + && preparedExpression != null + && preparedExpression.isExistsUndefinedVar()) { + + evaluate(sequenceId, JsclOperation.simplify, expression, mr); + } else { + + fireCalculatorEvent(newCalculationEventData(operation, expression, sequenceId), CalculatorEventType.calculation_failed, new CalculatorFailureImpl(parseException)); + } + } + + private void handleException(@NotNull Long calculationId, + @NotNull JsclOperation operation, + @NotNull String expression, + @Nullable MessageRegistry mr, + @NotNull CalculatorEvalException evalException) { + + if (operation == JsclOperation.numeric && evalException.getCause() instanceof NumeralBaseException) { + evaluate(calculationId, JsclOperation.simplify, expression, mr); + } + + fireCalculatorEvent(newCalculationEventData(operation, expression, calculationId), CalculatorEventType.calculation_failed, new CalculatorFailureImpl(evalException)); + } + + /* + ********************************************************************** + * + * CONVERSION + * + ********************************************************************** + */ + + @NotNull + @Override + public CalculatorEventData convert(@NotNull final Generic value, + @NotNull final NumeralBase to) { + final CalculatorEventData eventDataId = nextEventData(); + + final CalculatorDisplayViewState displayViewState = CalculatorLocatorImpl.getInstance().getDisplay().getViewState(); + final NumeralBase from = CalculatorLocatorImpl.getInstance().getEngine().getNumeralBase(); + + calculationsExecutor.execute(new Runnable() { + @Override + public void run() { + final Long sequenceId = eventDataId.getSequenceId(); + + fireCalculatorEvent(newConversionEventData(sequenceId, value, from, to, displayViewState), CalculatorEventType.conversion_started, null); + try { + + final String result = doConversion(value, from, to); + + fireCalculatorEvent(newConversionEventData(sequenceId, value, from, to, displayViewState), CalculatorEventType.conversion_result, result); + + } catch (ConversionException e) { + fireCalculatorEvent(newConversionEventData(sequenceId, value, from, to, displayViewState), CalculatorEventType.conversion_failed, new ConversionFailureImpl(e)); + } + } + }); + + return eventDataId; + } + + @NotNull + private static String doConversion(@NotNull Generic generic, + @NotNull NumeralBase from, + @NotNull NumeralBase to) throws ConversionException { + final String result; + + if (from != to) { + String fromString = generic.toString(); + if (!StringUtils.isEmpty(fromString)) { + try { + fromString = ToJsclTextProcessor.getInstance().process(fromString).getExpression(); + } catch (CalculatorParseException e) { + // ok, problems while processing occurred + } + } + + result = ConversionUtils.doConversion(CalculatorNumeralBase.getConverter(), fromString, CalculatorNumeralBase.valueOf(from), CalculatorNumeralBase.valueOf(to)); + } else { + result = generic.toString(); + } + + return result; + } + + @Override + public boolean isConversionPossible(@NotNull Generic generic, NumeralBase from, @NotNull NumeralBase to) { + try { + doConversion(generic, from, to); + return true; + } catch (ConversionException e) { + return false; + } + } + + /* + ********************************************************************** + * + * EVENTS + * + ********************************************************************** + */ + + @Override + public void addCalculatorEventListener(@NotNull CalculatorEventListener calculatorEventListener) { + calculatorEventContainer.addCalculatorEventListener(calculatorEventListener); + } + + @Override + public void removeCalculatorEventListener(@NotNull CalculatorEventListener calculatorEventListener) { + calculatorEventContainer.removeCalculatorEventListener(calculatorEventListener); + } + + @Override + public void fireCalculatorEvent(@NotNull final CalculatorEventData calculatorEventData, @NotNull final CalculatorEventType calculatorEventType, @Nullable final Object data) { + eventExecutor.execute(new Runnable() { + @Override + public void run() { + calculatorEventContainer.fireCalculatorEvent(calculatorEventData, calculatorEventType, data); + } + }); + } + + @Override + public void fireCalculatorEvents(@NotNull final List calculatorEvents) { + eventExecutor.execute(new Runnable() { + @Override + public void run() { + calculatorEventContainer.fireCalculatorEvents(calculatorEvents); + } + }); + } + + @NotNull + @Override + public CalculatorEventData fireCalculatorEvent(@NotNull final CalculatorEventType calculatorEventType, @Nullable final Object data) { + final CalculatorEventData eventData = nextEventData(); + + fireCalculatorEvent(eventData, calculatorEventType, data); + + return eventData; + } + + @NotNull + @Override + public CalculatorEventData fireCalculatorEvent(@NotNull final CalculatorEventType calculatorEventType, @Nullable final Object data, @NotNull Long sequenceId) { + final CalculatorEventData eventData = nextEventData(sequenceId); + + fireCalculatorEvent(eventData, calculatorEventType, data); + + return eventData; + } + + /* + ********************************************************************** + * + * EVENTS HANDLER + * + ********************************************************************** + */ + + @Override + public void onCalculatorEvent(@NotNull CalculatorEventData calculatorEventData, @NotNull CalculatorEventType calculatorEventType, @Nullable Object data) { + + switch (calculatorEventType) { + case editor_state_changed: + final CalculatorEditorChangeEventData changeEventData = (CalculatorEditorChangeEventData) data; + + final String newText = changeEventData.getNewState().getText(); + final String oldText = changeEventData.getOldState().getText(); + + if (!newText.equals(oldText)) { + evaluate(JsclOperation.numeric, changeEventData.getNewState().getText(), calculatorEventData.getSequenceId()); + } + break; + case engine_preferences_changed: + evaluate(calculatorEventData.getSequenceId()); + break; + } + } + + /* + ********************************************************************** + * + * HISTORY + * + ********************************************************************** + */ + + @Override + public void doHistoryAction(@NotNull HistoryAction historyAction) { + final CalculatorHistory history = CalculatorLocatorImpl.getInstance().getHistory(); + if (history.isActionAvailable(historyAction)) { + final CalculatorHistoryState newState = history.doAction(historyAction, getCurrentHistoryState()); + if (newState != null) { + setCurrentHistoryState(newState); + } + } + } + + @Override + public void setCurrentHistoryState(@NotNull CalculatorHistoryState editorHistoryState) { + editorHistoryState.setValuesFromHistory(getEditor(), getDisplay()); + } + + @NotNull + @Override + public CalculatorHistoryState getCurrentHistoryState() { + return CalculatorHistoryState.newInstance(getEditor(), getDisplay()); + } + + /* + ********************************************************************** + * + * OTHER + * + ********************************************************************** + */ + + @NotNull + private CalculatorEditor getEditor() { + return CalculatorLocatorImpl.getInstance().getEditor(); + } + + @NotNull + private CalculatorDisplay getDisplay() { + return CalculatorLocatorImpl.getInstance().getDisplay(); + } +} diff --git a/calculatorpp/src/main/java/org/solovyev/android/calculator/AndroidCalculatorDisplayView.java b/calculatorpp/src/main/java/org/solovyev/android/calculator/AndroidCalculatorDisplayView.java index 87b6b714..4b8a7948 100644 --- a/calculatorpp/src/main/java/org/solovyev/android/calculator/AndroidCalculatorDisplayView.java +++ b/calculatorpp/src/main/java/org/solovyev/android/calculator/AndroidCalculatorDisplayView.java @@ -8,10 +8,12 @@ package org.solovyev.android.calculator; import android.content.Context; import android.graphics.Color; import android.os.Handler; +import android.text.Editable; import android.text.Html; +import android.text.TextWatcher; import android.util.AttributeSet; -import android.util.Log; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.solovyev.android.calculator.text.TextProcessor; import org.solovyev.android.calculator.view.TextHighlighter; import org.solovyev.android.view.AutoResizeTextView; @@ -43,7 +45,12 @@ public class AndroidCalculatorDisplayView extends AutoResizeTextView implements */ @NotNull - private CalculatorDisplayViewState state = CalculatorDisplayViewStateImpl.newDefaultInstance(); + private volatile CalculatorDisplayViewState state = CalculatorDisplayViewStateImpl.newDefaultInstance(); + + private volatile boolean viewStateChange = false; + + @NotNull + private final Object lock = new Object(); @NotNull private final Handler handler = new Handler(); @@ -58,14 +65,18 @@ public class AndroidCalculatorDisplayView extends AutoResizeTextView implements public AndroidCalculatorDisplayView(Context context) { super(context); + this.addTextChangedListener(new TextWatcherImpl()); } public AndroidCalculatorDisplayView(Context context, AttributeSet attrs) { super(context, attrs); + this.addTextChangedListener(new TextWatcherImpl()); + } public AndroidCalculatorDisplayView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + this.addTextChangedListener(new TextWatcherImpl()); } /* @@ -76,63 +87,103 @@ public class AndroidCalculatorDisplayView extends AutoResizeTextView implements ********************************************************************** */ - public boolean isValid() { - synchronized (this) { - return this.state.isValid(); - } - } @Override public void setState(@NotNull final CalculatorDisplayViewState state) { - handler.postDelayed(new Runnable() { + final CharSequence text = prepareText(state.getStringResult(), state.isValid()); + + handler.post(new Runnable() { @Override public void run() { - synchronized (AndroidCalculatorDisplayView.this) { - AndroidCalculatorDisplayView.this.state = state; - if ( state.isValid() ) { - setTextColor(getResources().getColor(R.color.default_text_color)); - setText(state.getStringResult()); - redraw(); - } else { - setTextColor(getResources().getColor(R.color.display_error_text_color)); + synchronized (lock) { + try { + viewStateChange = true; - // error messages are never shown -> just greyed out text (error message will be shown on click) - //setText(state.getErrorMessage()); - //redraw(); + AndroidCalculatorDisplayView.this.state = state; + if (state.isValid()) { + setTextColor(getResources().getColor(R.color.default_text_color)); + setText(text); + + adjustTextSize(); + + } else { + // update text in order to get rid of HTML tags + setText(getText().toString()); + setTextColor(getResources().getColor(R.color.display_error_text_color)); + + // error messages are never shown -> just greyed out text (error message will be shown on click) + //setText(state.getErrorMessage()); + //redraw(); + } + } finally { + viewStateChange = false; } } } - }, 1); + }); } @NotNull @Override public CalculatorDisplayViewState getState() { - synchronized (this) { + synchronized (lock) { return this.state; } } - private synchronized void redraw() { - if (isValid()) { - String text = getText().toString(); + @Nullable + private static CharSequence prepareText(@Nullable String text, boolean valid) { + CharSequence result; - Log.d(this.getClass().getName(), text); + if (valid && text != null) { + + //Log.d(this.getClass().getName(), text); try { - TextHighlighter.Result result = textHighlighter.process(text); - text = result.toString(); + final TextHighlighter.Result processedText = textHighlighter.process(text); + text = processedText.toString(); + result = Html.fromHtml(text); } catch (CalculatorParseException e) { - Log.e(this.getClass().getName(), e.getMessage(), e); + result = text; } - - Log.d(this.getClass().getName(), text); - super.setText(Html.fromHtml(text), BufferType.EDITABLE); + } else { + result = text; } + return result; + } + + private void adjustTextSize() { // todo serso: think where to move it (keep in mind org.solovyev.android.view.AutoResizeTextView.resetTextSize()) setAddEllipsis(false); setMinTextSize(10); resizeText(); } + + + public void handleTextChange(Editable s) { + synchronized (lock) { + if (!viewStateChange) { + // external text change => need to notify display + // todo serso: implement + } + } + } + + private final class TextWatcherImpl implements TextWatcher { + + @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) { + handleTextChange(s); + } + } } diff --git a/calculatorpp/src/main/java/org/solovyev/android/calculator/AndroidCalculatorEditorView.java b/calculatorpp/src/main/java/org/solovyev/android/calculator/AndroidCalculatorEditorView.java index 8dbf32b2..2b8ee8ed 100644 --- a/calculatorpp/src/main/java/org/solovyev/android/calculator/AndroidCalculatorEditorView.java +++ b/calculatorpp/src/main/java/org/solovyev/android/calculator/AndroidCalculatorEditorView.java @@ -10,10 +10,15 @@ import android.content.SharedPreferences; import android.graphics.Color; import android.os.Build; import android.os.Handler; +import android.text.Editable; +import android.text.Html; +import android.text.TextWatcher; import android.util.AttributeSet; +import android.util.Log; import android.view.ContextMenu; import android.widget.EditText; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.solovyev.android.calculator.text.TextProcessor; import org.solovyev.android.calculator.view.TextHighlighter; import org.solovyev.common.collections.CollectionsUtils; @@ -25,32 +30,39 @@ import org.solovyev.common.collections.CollectionsUtils; */ public class AndroidCalculatorEditorView extends EditText implements SharedPreferences.OnSharedPreferenceChangeListener, CalculatorEditorView { - private static final String CALC_COLOR_DISPLAY_KEY = "org.solovyev.android.calculator.CalculatorModel_color_display"; - private static final boolean CALC_COLOR_DISPLAY_DEFAULT = true; + private static final String CALC_COLOR_DISPLAY_KEY = "org.solovyev.android.calculator.CalculatorModel_color_display"; + private static final boolean CALC_COLOR_DISPLAY_DEFAULT = true; - private boolean highlightText = true; + private boolean highlightText = true; - @NotNull - private final static TextProcessor textHighlighter = new TextHighlighter(Color.WHITE, true); + @NotNull + private final static TextProcessor textHighlighter = new TextHighlighter(Color.WHITE, false); @NotNull private volatile CalculatorEditorViewState viewState = CalculatorEditorViewStateImpl.newDefaultInstance(); private volatile boolean viewStateChange = false; + // NOTE: static because super constructor calls some overridden methods (like onSelectionChanged and current lock is not yet created) + @NotNull + private static final Object lock = new Object(); + @NotNull private final Handler handler = new Handler(); public AndroidCalculatorEditorView(Context context) { - super(context); - } + super(context); + this.addTextChangedListener(new TextWatcherImpl()); + } - public AndroidCalculatorEditorView(Context context, AttributeSet attrs) { - super(context, attrs); + public AndroidCalculatorEditorView(Context context, AttributeSet attrs) { + super(context, attrs); + this.addTextChangedListener(new TextWatcherImpl()); } public AndroidCalculatorEditorView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + this.addTextChangedListener(new TextWatcherImpl()); } @@ -59,12 +71,12 @@ public class AndroidCalculatorEditorView extends EditText implements SharedPrefe // NOTE: code below can be used carefully and should not be copied without special intention // The main purpose of code is to disable soft input (virtual keyboard) but leave all the TextEdit functionality, like cursor, scrolling, copy/paste menu etc - if ( Build.VERSION.SDK_INT >= 11 ) { + if (Build.VERSION.SDK_INT >= 11) { // fix for missing cursor in android 3 and higher try { // IDEA: return false always except if method was called from TextView.isCursorVisible() method for (StackTraceElement stackTraceElement : CollectionsUtils.asList(Thread.currentThread().getStackTrace())) { - if ( "isCursorVisible".equals(stackTraceElement.getMethodName()) ) { + if ("isCursorVisible".equals(stackTraceElement.getMethodName())) { return true; } } @@ -78,94 +90,115 @@ public class AndroidCalculatorEditorView extends EditText implements SharedPrefe } } - @Override - protected void onCreateContextMenu(ContextMenu menu) { - super.onCreateContextMenu(menu); + @Override + protected void onCreateContextMenu(ContextMenu menu) { + super.onCreateContextMenu(menu); - menu.removeItem(android.R.id.selectAll); - } + menu.removeItem(android.R.id.selectAll); + } - // todo serso: fix redraw - // Now problem is that calculator editor cursor position might be different than position of cursor in view (as some extra spaces can be inserted fur to number formatting) - /*private synchronized void redraw() { - String text = getText().toString(); + @Nullable + private CharSequence prepareText(@NotNull String text, boolean highlightText) { + CharSequence result; - int selectionStart = getSelectionStart(); - int selectionEnd = getSelectionEnd(); + if (highlightText) { - if (highlightText) { + try { + final TextHighlighter.Result processesText = textHighlighter.process(text); - Log.d(this.getClass().getName(), text); + assert processesText.getOffset() == 0; - try { - final TextHighlighter.Result result = textHighlighter.process(text); - selectionStart += result.getOffset(); - selectionEnd += result.getOffset(); - text = result.toString(); - } catch (CalculatorParseException e) { - Log.e(this.getClass().getName(), e.getMessage(), e); - } + result = Html.fromHtml(processesText.toString()); + } catch (CalculatorParseException e) { + // set raw text + result = text; - Log.d(this.getClass().getName(), text); - super.setText(Html.fromHtml(text), BufferType.EDITABLE); - } else { - super.setText(text, BufferType.EDITABLE); - } + Log.e(this.getClass().getName(), e.getMessage(), e); + } + } else { + result = text; + } - Log.d(this.getClass().getName(), getText().toString()); + return result; + } - int length = getText().length(); - setSelection(Math.max(Math.min(length, selectionStart), 0), Math.max(Math.min(length, selectionEnd), 0)); - }*/ + public boolean isHighlightText() { + return highlightText; + } - public boolean isHighlightText() { - return highlightText; - } + public void setHighlightText(boolean highlightText) { + this.highlightText = highlightText; + CalculatorLocatorImpl.getInstance().getEditor().updateViewState(); + } - public void setHighlightText(boolean highlightText) { - this.highlightText = highlightText; - //redraw(); - } + @Override + public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { + if (CALC_COLOR_DISPLAY_KEY.equals(key)) { + this.setHighlightText(preferences.getBoolean(CALC_COLOR_DISPLAY_KEY, CALC_COLOR_DISPLAY_DEFAULT)); + } + } - @Override - public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { - if (CALC_COLOR_DISPLAY_KEY.equals(key)) { - this.setHighlightText(preferences.getBoolean(CALC_COLOR_DISPLAY_KEY, CALC_COLOR_DISPLAY_DEFAULT)); - } - } - - public void init(@NotNull SharedPreferences preferences) { - onSharedPreferenceChanged(preferences, CALC_COLOR_DISPLAY_KEY); - } + public void init(@NotNull SharedPreferences preferences) { + onSharedPreferenceChanged(preferences, CALC_COLOR_DISPLAY_KEY); + } @Override public void setState(@NotNull final CalculatorEditorViewState viewState) { - handler.postDelayed(new Runnable() { + + final CharSequence text = prepareText(viewState.getText(), highlightText); + + handler.post(new Runnable() { @Override public void run() { final AndroidCalculatorEditorView editorView = AndroidCalculatorEditorView.this; - synchronized (editorView) { + synchronized (lock) { try { editorView.viewStateChange = true; editorView.viewState = viewState; - editorView.setText(viewState.getText()); + editorView.setText(text, BufferType.EDITABLE); editorView.setSelection(viewState.getSelection()); - //redraw(); } finally { editorView.viewStateChange = false; } } } - }, 1); + }); } @Override protected void onSelectionChanged(int selStart, int selEnd) { - synchronized (this) { + synchronized (lock) { if (!viewStateChange) { + // external text change => need to notify editor super.onSelectionChanged(selStart, selEnd); CalculatorLocatorImpl.getInstance().getEditor().setSelection(selStart); } } } + + public void handleTextChange(Editable s) { + synchronized (lock) { + if (!viewStateChange) { + // external text change => need to notify editor + CalculatorLocatorImpl.getInstance().getEditor().setText(String.valueOf(s)); + } + } + } + + private final class TextWatcherImpl implements TextWatcher { + + @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) { + handleTextChange(s); + } + } }