package com.lemenzo.calculator import android.app.Activity import android.app.AlertDialog import android.content.ClipData import android.content.ClipDescription import android.content.ClipboardManager import android.os.Bundle import android.os.PersistableBundle import android.text.Editable import android.text.InputFilter import android.text.TextWatcher import android.view.HapticFeedbackConstants import android.view.View import android.view.WindowInsets import android.widget.* import com.lemenzo.calculator.engine.ConversionEngine import com.lemenzo.calculator.engine.DisplayFormatter import com.lemenzo.calculator.engine.ExpressionEngine import com.lemenzo.calculator.engine.ExpressionCodec import com.lemenzo.calculator.engine.ProgrammerEngine import com.lemenzo.calculator.engine.NumberInputParser import com.lemenzo.calculator.model.HistoryStore import java.math.BigDecimal import java.math.BigInteger import java.text.DecimalFormatSymbols import java.util.Locale class CalculatorActivity : Activity() { private lateinit var expressionView: EditText private lateinit var resultView: TextView private lateinit var keypad: GridLayout private lateinit var converterPanel: LinearLayout private lateinit var calculatorDisplayPanel: LinearLayout private lateinit var modeLabel: TextView private lateinit var history: HistoryStore private lateinit var programmerBases: TextView private lateinit var conversionCategory: Spinner private lateinit var conversionFrom: Spinner private lateinit var conversionTo: Spinner private lateinit var conversionInput: EditText private val engine = ExpressionEngine() private val programmer = ProgrammerEngine() private var expression = "" private var calculatorExpression = "" private var programmerExpression = "" private var calculatorLastValue: BigDecimal? = null private var memory = BigDecimal.ZERO private var lastValue: BigDecimal? = null private var mode = Mode.BASIC private var scientificSecondary = false private var notation = DisplayFormatter.Notation.AUTO private var syncingExpression = false private val localeSymbols by lazy { DecimalFormatSymbols.getInstance(Locale.getDefault()) } private val decimalSeparator: Char get() = localeSymbols.decimalSeparator private val groupingSeparator: Char get() = localeSymbols.groupingSeparator enum class Mode { BASIC, SCIENTIFIC, CONVERTER, PROGRAMMER } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_calculator) findViewById(R.id.root).setOnApplyWindowInsetsListener { v, insets -> val bars = insets.getInsets(WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()) v.setPadding(0, bars.top, 0, bars.bottom) insets } expressionView = findViewById(R.id.expression) expressionView.showSoftInputOnFocus = false resultView = findViewById(R.id.result) keypad = findViewById(R.id.keypad) converterPanel = findViewById(R.id.converterPanel) calculatorDisplayPanel = findViewById(R.id.calculatorDisplayPanel) modeLabel = findViewById(R.id.modeLabel) history = HistoryStore(this) programmerBases = findViewById(R.id.programmerBases) expressionView.filters = arrayOf(InputFilter.LengthFilter(512)) expressionView.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { if (syncingExpression) return expression = canonicalizeDisplayExpression(s?.toString().orEmpty()) if (mode == Mode.PROGRAMMER) calculateProgrammer(false) else calculateLive() } override fun afterTextChanged(s: Editable?) = Unit }) findViewById(R.id.modeBasic).setOnClickListener { setMode(Mode.BASIC) } findViewById(R.id.modeScientific).setOnClickListener { setMode(Mode.SCIENTIFIC) } findViewById(R.id.modeConverter).setOnClickListener { setMode(Mode.CONVERTER) } findViewById(R.id.modeProgrammer).setOnClickListener { setMode(Mode.PROGRAMMER) } findViewById(R.id.historyButton).setOnClickListener { showHistory() } resultView.setOnLongClickListener { val clipboard = getSystemService(ClipboardManager::class.java) val copy = if (mode == Mode.PROGRAMMER) resultView.text.toString() else lastValue?.let(engine::format) ?: resultView.text.toString() val clip = ClipData.newPlainText(getString(R.string.clipboard_result_label), copy) clip.description.extras = PersistableBundle().apply { putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) } clipboard.setPrimaryClip(clip) Toast.makeText(this, R.string.result_copied, Toast.LENGTH_SHORT).show() true } setupConverter() if (savedInstanceState != null) { expression = savedInstanceState.getString("expression", "") calculatorExpression = savedInstanceState.getString("calculatorExpression", expression) programmerExpression = savedInstanceState.getString("programmerExpression", "") memory = savedInstanceState.getString("memory", "0")!!.toBigDecimalOrNull() ?: BigDecimal.ZERO lastValue = savedInstanceState.getString("lastValue")?.toBigDecimalOrNull() mode = runCatching { Mode.valueOf(savedInstanceState.getString("mode", Mode.BASIC.name)!!) }.getOrDefault(Mode.BASIC) engine.angleMode = runCatching { ExpressionEngine.AngleMode.valueOf(savedInstanceState.getString("angle", "DEG")!!) }.getOrDefault(ExpressionEngine.AngleMode.DEG) programmer.base = runCatching { ProgrammerEngine.Base.valueOf(savedInstanceState.getString("base", "DEC")!!) }.getOrDefault(ProgrammerEngine.Base.DEC) programmer.wordSize = runCatching { ProgrammerEngine.WordSize.valueOf(savedInstanceState.getString("word", "B64")!!) }.getOrDefault(ProgrammerEngine.WordSize.B64) scientificSecondary = savedInstanceState.getBoolean("scientificSecondary", false) notation = runCatching { DisplayFormatter.Notation.valueOf(savedInstanceState.getString("notation", "AUTO")!!) }.getOrDefault(DisplayFormatter.Notation.AUTO) } setMode(mode, preserveExpression = true) syncExpressionView() if (mode == Mode.PROGRAMMER) calculateProgrammer(false) else calculateLive() savedInstanceState?.let { restoreConverterState(it) } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString("expression", expression) outState.putString("calculatorExpression", calculatorExpression) outState.putString("programmerExpression", programmerExpression) outState.putString("memory", memory.toPlainString()) outState.putString("lastValue", lastValue?.toPlainString()) outState.putString("mode", mode.name) outState.putString("angle", engine.angleMode.name) outState.putString("base", programmer.base.name) outState.putString("word", programmer.wordSize.name) outState.putBoolean("scientificSecondary", scientificSecondary) outState.putString("notation", notation.name) if (::conversionCategory.isInitialized) { outState.putInt("converterCategory", conversionCategory.selectedItemPosition) outState.putInt("converterFrom", conversionFrom.selectedItemPosition) outState.putInt("converterTo", conversionTo.selectedItemPosition) outState.putString("converterInput", conversionInput.text?.toString().orEmpty()) } } private fun setMode(newMode: Mode, preserveExpression: Boolean = false) { val oldMode = mode if (!preserveExpression && newMode != oldMode) { when (oldMode) { Mode.BASIC, Mode.SCIENTIFIC -> { calculatorExpression = expression; calculatorLastValue = lastValue } Mode.PROGRAMMER -> programmerExpression = expression Mode.CONVERTER -> Unit } } mode = newMode if (!preserveExpression && newMode != oldMode) { when (newMode) { Mode.BASIC, Mode.SCIENTIFIC -> { expression = calculatorExpression; lastValue = calculatorLastValue } Mode.PROGRAMMER -> { expression = programmerExpression; lastValue = null } Mode.CONVERTER -> Unit } } converterPanel.visibility = if (mode == Mode.CONVERTER) View.VISIBLE else View.GONE calculatorDisplayPanel.visibility = if (mode == Mode.CONVERTER) View.GONE else View.VISIBLE keypad.visibility = if (mode == Mode.CONVERTER) View.GONE else View.VISIBLE programmerBases.visibility = if (mode == Mode.PROGRAMMER) View.VISIBLE else View.GONE updateModeButtons(); buildKeys(); updateModeLabel(); syncExpressionView() when (mode) { Mode.BASIC, Mode.SCIENTIFIC -> if (expression.isNotBlank()) calculateLive(); Mode.PROGRAMMER -> if (expression.isNotBlank()) calculateProgrammer(false); Mode.CONVERTER -> Unit } } private fun updateModeButtons() { val mapping = mapOf( R.id.modeBasic to Mode.BASIC, R.id.modeScientific to Mode.SCIENTIFIC, R.id.modeConverter to Mode.CONVERTER, R.id.modeProgrammer to Mode.PROGRAMMER ) mapping.forEach { (id, m) -> val selected = m == mode val button = findViewById