Solution (source code)

= Solution

Let the committed model be amounts $p,e$ and an exchange rate $r$, with <invariant> $e=pr$. Editing pounds keeps the rate and computes euros; editing euros keeps the rate and computes pounds; editing the rate keeps pounds and computes euros. A usable rate must be strictly positive, and every accepted <floating-point number> and derived result must be finite. Negative amounts are allowed. Rejecting invalid text preserves the last committed model.

Insert the following members inside the supplied `MainFrame`, after the three fields have been initialized. Its existing layout can remain as written:
``
private var p = 10.0
private var e = 11.5
private var r = 1.15
private var refreshing = false

private def finite(x: Double): Boolean = !x.isNaN && !x.isInfinity

private def showModel(): Unit = {
  refreshing = true
  try {
    pounds.text = java.lang.String.format(java.util.Locale.ROOT, "%.2f", Double.box(p))
    euros.text = java.lang.String.format(java.util.Locale.ROOT, "%.2f", Double.box(e))
    rate.text = r.toString
  } finally {
    refreshing = false
  }
}

private def edit(source: TextField): Unit = {
  try {
    val v = source.text.trim.toDouble
    if (finite(v)) {
      var nextP = p
      var nextE = e
      var nextR = r
      if (source eq pounds) {
        nextP = v
        nextE = v * r
      } else if (source eq euros) {
        nextE = v
        nextP = v / r
      } else if (source eq rate) {
        nextR = v
        nextE = p * v
      }
      if (nextR > 0.0 && finite(nextP) && finite(nextE) && finite(nextR)) {
        p = nextP
        e = nextE
        r = nextR
      }
    }
  } catch {
    case _: NumberFormatException => ()
  }
  showModel()
}

listenTo(pounds, euros, rate)
reactions += {
  case EditDone(source) if !refreshing => edit(source)
}
``
The <invariant> is interpreted with the usual <floating-point rounding> in multiplication and division. All candidate values are calculated before committing any of them, so invalid text, zero/negative rates, NaN, infinity and <floating-point overflow> leave the old model intact. The committed `r` remains positive, so euro-to-pound division is defined. Using a fixed formatting locale keeps the decimal separator compatible with `toDouble`. The displayed amounts are rounded to two decimal places, while the model retains their full available precision. The rate is not forcibly rounded to two places, which could otherwise turn a valid small positive rate into displayed zero.

The `refreshing` guard prevents synchronous derived-field notifications from recursively committing another edit. `EditDone` is an edit-completion event, not a request to react to every partial keystroke; pressing the field's completion action is enough to trigger the conversion. If a particular UI additionally needs live updates, it can subscribe to the appropriate text-change events with the same validation and authoritative-source policy. The supplied API and archived <Scala Swing> documentation suffice for the requested completion-event implementation; no unidentified practical library is required.<Scala Swing event-system documentation>[Publisher/Reactor subscription design; archived Swing event API: EditDone carries its source TextField]{ref}