Author Topic: PJSR: Handling async widget events  (Read 5849 times)

Offline georg.viehoever

  • PTeam Member
  • PixInsight Jedi Master
  • ******
  • Posts: 2132
PJSR: Handling async widget events
« on: 2009 July 22 13:04:46 »
Juan,

the attached program is an excerpt from an improved version of my banding script (yes, with PI 1.56 it works much more stable than with any previous PI version  :) ). The problem appears to be with the event handling. When I move the slider rapidly from 1.0 to 4.0, this triggers a large number of "onValueUpdated()" events, but they are not processed in the order in which they appear. Also, generating new events continues for quite a while after I stop moving the slider. This can clearly be seen in attached log from the console

So I wonder how I can get a more controlled slider handling. I like to see the slider move smoothly, but paint events that dont relate to the latest value should be ignored/canceled. Any idea how this can be done?

Kind regards,
Georg


Log exerpt:
Code: [Select]
processing in slider, amount=1.02
processing in slider, amount=1.424
processing in slider, amount=1.924
processing in slider, amount=1.992
processing in slider, amount=2.076
processing in slider, amount=2.328
processing in slider, amount=2.628
processing in slider, amount=2.78
processing in slider, amount=2.912
processing in slider, amount=3.064
processing in slider, amount=3.18
processing in slider, amount=3.296
processing in slider, amount=3.516
processing in slider, amount=3.616
processing in slider, amount=3.832
processing in slider, amount=3.9
processing in slider, amount=3.916
processing in slider, amount=3.948
processing in slider, amount=4
processing in slider, iteration=0, amount=4
processing in slider, iteration=1, amount=4
processing in slider, iteration=2, amount=4
processing in slider, iteration=3, amount=4
processing in slider, iteration=4, amount=4
processing in slider, iteration=5, amount=4
processing in slider, iteration=6, amount=4
processing in slider, iteration=7, amount=4
processing in slider, iteration=8, amount=4
processing in slider, iteration=9, amount=4
processing in slider, triggering update, amount=4
processing in slider done, amount=4
processing in slider, iteration=0, amount=3.948
onPaint() entry
onPaint(), iteration=0
onPaint(), iteration=1
onPaint(), iteration=2
onPaint(), iteration=3
onPaint(), iteration=4
onPaint(), iteration=5
onPaint(), iteration=6
onPaint(), iteration=7
onPaint(), iteration=8
onPaint(), iteration=9
onPaint() exit
processing in slider, iteration=1, amount=3.948
processing in slider, iteration=2, amount=3.948
processing in slider, iteration=3, amount=3.948
processing in slider, iteration=4, amount=3.948
processing in slider, iteration=5, amount=3.948
processing in slider, iteration=6, amount=3.948
processing in slider, iteration=7, amount=3.948
processing in slider, iteration=8, amount=3.948
processing in slider, iteration=9, amount=3.948
processing in slider, triggering update, amount=3.948
processing in slider done, amount=3.948
processing in slider, iteration=0, amount=3.916
onPaint() entry
onPaint(), iteration=0
onPaint(), iteration=1
onPaint(), iteration=2
onPaint(), iteration=3
onPaint(), iteration=4
onPaint(), iteration=5
onPaint(), iteration=6
onPaint(), iteration=7
onPaint(), iteration=8
onPaint(), iteration=9
onPaint() exit
processing in slider, iteration=1, amount=3.916
processing in slider, iteration=2, amount=3.916
processing in slider, iteration=3, amount=3.916
processing in slider, iteration=4, amount=3.916
processing in slider, iteration=5, amount=3.916
processing in slider, iteration=6, amount=3.916
processing in slider, iteration=7, amount=3.916
processing in slider, iteration=8, amount=3.916
processing in slider, iteration=9, amount=3.916
processing in slider, triggering update, amount=3.916
processing in slider done, amount=3.916
processing in slider, iteration=0, amount=3.9
onPaint() entry
onPaint(), iteration=0
onPaint(), iteration=1
onPaint(), iteration=2
onPaint(), iteration=3
onPaint(), iteration=4
onPaint(), iteration=5
onPaint(), iteration=6
onPaint(), iteration=7
onPaint(), iteration=8
onPaint(), iteration=9
onPaint() exit
processing in slider, iteration=1, amount=3.9
processing in slider, iteration=2, amount=3.9
processing in slider, iteration=3, amount=3.9
processing in slider, iteration=4, amount=3.9
processing in slider, iteration=5, amount=3.9
processing in slider, iteration=6, amount=3.9
processing in slider, iteration=7, amount=3.9
processing in slider, iteration=8, amount=3.9
processing in slider, iteration=9, amount=3.9
processing in slider, triggering update, amount=3.9
processing in slider done, amount=3.9
processing in slider, iteration=0, amount=3.832

Script code:
Code: [Select]
#include <pjsr/NumericControl.jsh>

function BandingScrollControl( parent )
{
   this.__base__ = ScrollBox;
   this.__base__( parent );

   this.autoScroll = true;
   this.tracking = true;

   /// called to notify that a redraw of image is necessary
   this.doUpdateImage=function(){
      this.initScrollBars();
      this.viewport.update();
   }

   this.initScrollBars = function()
   {
         this.pageWidth=100;
         this.pageHeight=200;
         this.setHorizontalScrollRange( 0, Math.max( 0, 100 - this.viewport.width ) );
         this.setVerticalScrollRange( 0, Math.max( 0, 200 - this.viewport.height ) );
         this.viewport.update();
   };

   this.viewport.onResize = function()
   {
      this.parent.initScrollBars();
   };

   this.onHorizontalScrollPosUpdated = function( x )
   {
      this.viewport.update();
   };

   this.onVerticalScrollPosUpdated = function( y )
   {
      this.viewport.update();
   };

   this.viewport.onPaint = function( x0, y0, x1, y1 )
   {
      // do something incredibly complex here...
         console.writeln("onPaint() entry");
         for(var i=0;i<10;++i){
            for (var j=0;j<1000000;++j){
               //just do nothing and delay it...
            }
            console.writeln("onPaint(), iteration=",i);
            processEvents(); // some complex painting here,,,
           }
         console.writeln("onPaint() exit");
   }; //onPaint()

   this.initScrollBars();
}

BandingScrollControl.prototype = new ScrollBox;

function MyDialog() {
   this.__base__ = Dialog;
   this.__base__();

   this.amountControl=new NumericControl(this);
    with (this.amountControl) {
      label.text = "Amount:";
      setRange( 0, 4.0 );
      slider.setRange( 0, 1000 );
      slider.minWidth = 250;
      setPrecision( 2 );
      setValue( 1.0);
      toolTip = "Define amount of correction";
      onValueUpdated = function( value ) {
         console.writeln("processing in slider, amount=",value);
      // do something incredibly complex here...
         for(var i=0;i<10;++i){
            for (var j=0;j<1000000;++j){
               //just do nothing and delay it...
            }
            processEvents();
            console.writeln("processing in slider, iteration=",i,", amount=",value);
         }
         console.writeln("processing in slider, triggering update, amount=",value);
         this.dialog.previewControl.doUpdateImage();
         console.writeln("processing in slider done, amount=",value);
         return;
      }; //function onValueUpdated();

      this.previewControl=new BandingScrollControl(this);

  this.sizer = new VerticalSizer;
   this.sizer.margin = 6;
   this.sizer.spacing = 6;
   this.sizer.addSpacing (4);
   this.sizer.add (this.amountControl);

   }  //with amountControl

   this.windowTitle = "TestWindow";
   this.adjustToContents();
   this.sizer.addSpacing (4);
   this.sizer.add (this.previewControl);
}  //class MyDialog
MyDialog.prototype = new Dialog;


function main() {
   console.show();
   console.writeln("Script started");
   var dialog = new MyDialog();
   dialog.execute();
   console.writeln("Dialog done");
   console.hide();
}

main();

Georg (6 inch Newton, unmodified Canon EOS40D+80D, unguided EQ5 mount)

Offline georg.viehoever

  • PTeam Member
  • PixInsight Jedi Master
  • ******
  • Posts: 2132
Re: PJSR: Handling async widget events
« Reply #1 on: 2009 July 22 15:01:41 »
As an additional note:
- I do not call processEvents() explicitly in the real program. Somewhere, this appears to happen implicitly in the scrollBox processing.
- Is it possible to get/output a stack trace from PJSR during runtime? This would help to identify the call chain that causes this behaviour in the real program.
Georg
Georg (6 inch Newton, unmodified Canon EOS40D+80D, unguided EQ5 mount)

Offline Juan Conejero

  • PTeam Member
  • PixInsight Jedi Grand Master
  • ********
  • Posts: 7111
    • http://pixinsight.com/
Re: PJSR: Handling async widget events
« Reply #2 on: 2009 July 24 11:05:14 »
Hi Georg,

Sorry for taking long to respond. Here is a little script that does what you want. I've tried to write a script as easy to follow and understand as possible.

Code: [Select]
#include <pjsr/NumericControl.jsh>
#include <pjsr/FrameStyle.jsh>

function MyDialog()
{
   this.__base__ = Dialog;
   this.__base__();

   this.startingAngle = 0;

   this.image = new Image;

   this.abort = false;
   this.busy = false;
   this.terminate = false;

   //
   // Transformation of an image from Cartesian to polar coordinates.
   // Code adapted from the PolarCoordinates example script.
   //
   this.cartesianToPolar = function( image )
   {
      // Gather working parameters
      var n = image.numberOfChannels;
      var w = image.width;
      var h = image.height;
      var w2 = 0.5*w;
      var h2 = 0.5*h;
      var r0 = Math.sqrt( w2*w2 + h2*h2 ); // semi-diagonal

      // Total work
      var N = n*h;

      // Create a working image to store one channel of the target image
      var tmp = new Image( w, h, 1 );

      // Reset the rectangular selection to the whole image boundaries
      image.resetRectSelection();

      // For each channel
      for ( var c = 0, status = 0; c < n; ++c )
      {
         tmp.fill( 0 ); // initialize working data with zeros

         // For each row
         for ( var i = 0; i < h; ++i, ++status )
         {
            // Polar angle for the current row
            var theta =  this.startingAngle + 360*i/h;
            if ( theta >= 360 )
               theta -= 360;
            theta = Math.rad( theta );

            // Sine and cosine of current polar angle
            var stheta = Math.sin( theta );
            var ctheta = Math.cos( theta );

            // For each column
            for ( var j = 0; j < w; ++j )
            {
               // Radial distance for the current column
               var r = r0*j/w;

               // Horizontal coordinate on the source image
               var x = w2 + r*ctheta;

               if ( x >= 0 && x < w )
               {
                  // Vertical coordinate on the source image
                  var y = h2 - r*stheta;

                  // Copy the source pixel to the polar-transformed location on
                  // the working image.
                  if ( y >= 0 && y < h )
                     tmp.setSample( image.interpolate( x, y, c ), j, i );
               }
            }

            // Process pending GUI events
            processEvents();

            // Check for abort condition
            if ( this.abort )
               break;

            this.infoLabel.text = format( "Computing: %d%%", Math.round( 100.0*status/N ) );
         }

         // Check for abort condition
         if ( this.abort )
            break;

         // Copy our working data to the channel c of image
         image.selectedChannel = c;
         image.apply( tmp );
      }

      image.resetChannelSelection();

      // Destroy our working image
      delete tmp;
   };

   //
   // Asynchronous generation routine.
   //
   this.generate = function()
   {
      // If we are already generating data, request job abortion and return.
      if ( this.busy )
      {
         this.abort = true;
         return;
      }

      // Start of job
      this.busy = true;

      do
      {
         this.abort = false;
         this.image.assign( ImageWindow.activeWindow.currentView.image );
         this.cartesianToPolar( this.image );
      }
      while ( this.abort && !this.terminate );

      // End of job
      this.busy = false;

      if ( !this.terminate )
      {
         this.infoLabel.text = "Ready.";
         this.renderControl.update();
      }
   };

   this.renderControl = new Control( this );
   with ( this.renderControl )
   {
      setMinSize( 400, 400 );

      onPaint = function()
      {
         var G = new Graphics( this );

         if ( parent.busy )
            G.eraseRect( this.boundsRect );
         else
            G.drawScaledBitmap( this.boundsRect, parent.image.render() );

         G.end();
      };
   }

   this.angleControl = new NumericControl( this );
   with ( this.angleControl )
   {
      real = false;
      setRange( 0, 360 );
      label.text = "Starting angle:";
      slider.setRange( 0, 360 );
      slider.minWidth = 400;
      setValue( this.startingAngle );
      toolTip = "<p>Starting angle of the Cartesian to Polar transform.</p>";

      onValueUpdated = function( value )
      {
         parent.startingAngle = value;
         parent.generate();
      };
   }

   this.infoLabel = new Label( this );
   with ( this.infoLabel )
   {
      frameStyle = FrameStyle_Box;
      margin = 4;
      text = "";
   }

   this.sizer = new VerticalSizer;
   with ( this.sizer )
   {
      margin = 6;
      spacing = 6;
      add( this.renderControl, 100 );
      add( this.angleControl );
      add( this.infoLabel );
   }

   // Ensure first-time generation
   this.onGetFocus = function()
   {
      if ( this.image.isEmpty ) // but only for the first time
         this.generate();
   }

   // Ensure unconditional job abortion upon dialog termination
   this.onHide = function()
   {
      this.terminate = this.abort = true;
   }

   this.windowTitle = "Asynchronous PolarTransform";
   this.adjustToContents();
}

MyDialog.prototype = new Dialog;

function main()
{
   // Uncomment this to enable automatic garbage collection.
   //jsAutoGC = true;

   console.show();

   var dialog = new MyDialog;
   dialog.execute();

   console.hide();
}

main();

The code above implements an asynchronous, fully interruptible general-purpose generation routine --just replace the call to cartesianToPolar with a compatible worker function that suits to your needs. Of course, the drawing and rendering code can be highly improved (I refer to the onPaint event handler). I've focused on the generation part, which is what you really need.

Be careful with these routines as it is easy to lead to infinite recursion if you break the internal logic. Always think in terms of the underlying time line as defined by the sequence of GUI events. In this case, it is relatively simple because it depends on a single component (the slider control). If you add more dialog controls that change generation parameters, don't forget the corresponding calls to parent.generate().

Let me know if this helps you.
« Last Edit: 2009 July 24 11:27:28 by Juan Conejero »
Juan Conejero
PixInsight Development Team
http://pixinsight.com/

Offline Juan Conejero

  • PTeam Member
  • PixInsight Jedi Grand Master
  • ********
  • Posts: 7111
    • http://pixinsight.com/
Re: PJSR: Handling async widget events
« Reply #3 on: 2009 July 24 11:28:06 »
Georg,

I have modified the code because it had a small error (06:28 UTC).
Juan Conejero
PixInsight Development Team
http://pixinsight.com/

Offline georg.viehoever

  • PTeam Member
  • PixInsight Jedi Master
  • ******
  • Posts: 2132
Re: PJSR: Handling async widget events
« Reply #4 on: 2009 July 24 12:51:57 »
Juan

I think I got the idea now! Thanks for giving me this valuable hint!

Georg
Georg (6 inch Newton, unmodified Canon EOS40D+80D, unguided EQ5 mount)