How can one dynamically update a progress indicator while a calculation started by changing an InputField value is running?

Viewed 104

I have a Mathematica notebook that employs a fairly complicated user interface for controlling a long-running calculation. Among other things, the interface takes liberal advantage of Button, RadioButtonBar, Checkbox, and InputField.

When the effect of clicking a Button is an intermediate calculation that may take more than a couple seconds to complete, I like to provide a visual indication that the code hasn't crashed and is, in fact, doing something useful. A good way to do this is to start up a ProgressIndicator just before the intermediate calculation starts and then turn it off once the calculation is done. I have found this to be straightforward for calculations started by a Button click.

The same method does not work, however, for calculations that are initiated by changes to an InputField value. The simplified code below was written to do this but fails. The last two rows of the Grid are supposed to change automatically when updatingQ changes to True in the inner Dynamic command and then change back when updatingQ reverts to True, but it never happens. It appears that the outer Dynamic code is being blocked while the inner Dynamic code runs so it never even notices the changes to updatingQ.

On the other hand, the last two lines of the Grid respond as expected if one manually sets updatingQ=True on a separate input line.

(BTW, i) Pause[2] is just a stand-in for the intermediate calculation and ii) I multiply the input value by Pi is just to make it more obvious when the stand-in calculation is done.)

Apparently, the action portion of a Button behaves differently. Other pieces of code within the same Dynamic block can see and quickly respond when flags are changed there. It may be notable that I use Method->"Queued" in such cases. I tried the same with InputField (for which it is not a documented option) but to no effect.

I've tried various other things not shown here also without success.

A way to make this work would be much appreciated.

Clear[ProgressIndicatorTest]
updatingQ = False;
ProgressIndicatorTest = {
   TextCell["ProgressIndicatorTest", "Subsubsection", Background -> LightBlue],
   DynamicModule[
    {filterTypes = {"Max energy", "Max length"}, filterValue, workingOn = "", iter = 0},
    Scan[(filterValue[#[[1]]] = #[[2]]) &, Transpose@{filterTypes, {0.1, 100.}}];
    Dynamic[
     Grid[
      Join[
       Map[
        Function[
         filterType,
         {filterType,
          Dynamic@
           InputField[
            Dynamic[
             filterValue[filterType],
             Function[
              value,
              If[value > 0,
               updatingQ = True;
               Pause[2];
               filterValue[filterType] = \[Pi] value;
               updatingQ = False
               ]
              ]
             ], FieldSize -> 5, Alignment -> Right
            ]
          }
         ], filterTypes
        ],
       {{updatingQ, "-------"}},
       {If[updatingQ,
         {"Updating ... ", 
          ProgressIndicator[Appearance -> "Indeterminate"]},
         Nothing
         ]}
       ], Alignment -> Left, 
      Background -> {None, {LightGreen, LightGreen, LightYellow, LightYellow}}
      ]
     ]
    ]
   };
CellGroup[ProgressIndicatorTest]
2 Answers

As Forrest Gump never said, "Stackoverflow/Stackexchange is like a box of chocolates ... you never know what you'll get". And so today I found this answer which solves my problem.

Adapted to my particular case, the resulting code is as follows:

Clear[ProgressIndicatorTest]
calculation[n_] := Module[{a = .3}, Do[a = a (1 - a), {i, n 10^6}]]
updatingQ = False;
ProgressIndicatorTest = {
   TextCell["ProgressIndicatorTest", "Subsubsection", Background -> LightBlue], 
   DynamicModule[{filterTypes = {"Max energy", "Max length"}, filterValue, upToDateQ = True}, 
    Scan[(filterValue[#[[1]]] = #[[2]]) &, Transpose@{filterTypes, {0.1, 100.}}];
    Dynamic[
     Grid[
      Join[
       Map[
        Function[
         filterType, 
         {filterType, 
          DynamicWrapper[
           InputField[
            Dynamic[
             filterValue[filterType], 
             Function[
              value,
              If[value > 0,
               upToDateQ = False;
               filterValue[filterType] = value
               ]
              ]
             ], FieldSize -> 5, Alignment -> Right
            ],
           If[! upToDateQ,
            Refresh[
             updatingQ = True; calculation[2]; updatingQ = False;
             upToDateQ = True,
             None
             ]
            ],
           SynchronousUpdating -> False
           ]
          }
         ], filterTypes
        ],
        {
        If[updatingQ,
         {"Updating ... ", 
          ProgressIndicator[Appearance -> "Indeterminate", ImageSize -> 80]},
         Nothing
         ]
        }
       ], Alignment -> Left, 
      Background -> {None, {LightGreen, LightGreen, LightYellow,}}]
     ]]
   };
CellGroup[ProgressIndicatorTest]

This code does exactly what I want.

The key to success is wrapping DynamicWrapper around InputField and inserting a cleverly constructed second argument that performs the flag reset (upToDate=False in my case) that triggers the ProgressIndicator located elsewhere.

A couple more points.

  • Pause turns out not to be a good stand-in for a calculation. You may observe that the code behaves differently with a real function such as calculation.
  • It is interesting to note that upToDateQ can be a local variable whereas updatingQ cannot.

Kudos to Albert Retey for providing the code back in 2013.

The documentation for InputField says

"An InputField of type Expression [the default] replaces its contents with the fully evaluated form every time the contents are updated".

This seems to mean that InputField privately evaluates its content and all connected dynamics before releasing value changes, probably to prevent circular evaluations.

The following example condenses the problem. The first part works ok ...

changed = processing = False;
Column[{InputField[Dynamic[x, (changed = True; x = 2 #) &], FieldSize -> 5],
  Dynamic[changed],
  Dynamic[processing]}]

... until the dynamic below is also evaluated. Then changed never shows True because it is changed back to False before the update concludes.

Dynamic[If[changed,
  processing = True;
  Pause[2];
  changed = processing = False]]

A alternative strategy would be to use a Button, e.g.

changed = False;
processing = Spacer[0];
Column[{InputField[Dynamic[y, (changed = True; y = #) &], FieldSize -> 5],
  Button["Enter",
   If[changed,
    processing = ProgressIndicator[Appearance -> "Indeterminate", ImageSize -> 120];
    Pause[2];
    y = 2 y;
    changed = False;
    processing = Spacer[0]], Method -> "Queued", Enabled -> Dynamic[changed]],
  Dynamic[changed],
  Dynamic[processing]}]

This shorter version avoids the need to tab out of the input field.

changed = False;
processing = Spacer[0];
Column[{InputField[Dynamic[y], FieldSize -> 5], 
  Button["Enter", 
   processing = ProgressIndicator[Appearance -> "Indeterminate", ImageSize -> 120];
   Pause[2];
   y = 2 y;
   processing = Spacer[0], Method -> "Queued"], Dynamic[processing]}]

Note the use of Method -> "Queued" gives Button the advantage over InputField. Without it Button appears to have the same problem.

Related