G4P Create new Control-class to use in Sketch

Hi,

Recently I came across this awesome tool G4P GUI Builder to use for my project. I would like to create a new Control to mimic a Encoder Knob. I’ve been fumbling around with the DKnob Class to change it into a new DEncoderKnob class, but sadly my code experience is lacking and need some guidance.

Any help to get me started is greatly appreciated.

Cheers Marcel.

Welcome to the forum.

The easiest way to create a new control compatible with G4P is to start with an existing control that most closely resembles what we want. In this case you are looking at the GKnob class (note that DKnob is part of GUI Builder not G4P). The solution involves creating a new class called GEncoderKnob that inherits from GKnob.

We will need the G4P library so if it is not install do so using the Contributions Manager to do so.

Follow these steps in the Processing IDE

  1. Create and save an empty sketch
  2. create a new tab called GEncoderKnob.java (the name must be exactly as shown and match the name of our new class) then add the following code to it
import g4p_controls.*;
import processing.core.*;

public class GEncoderKnob extends GKnob {
  
  public GEncoderKnob(PApplet theApplet, float p0, float p1, float p2, float p3, float gripAmount) {
    super(theApplet, p0, p1, p2, p3, gripAmount); // call ctor in GKnob
  }

}
  1. In the main sketch tab add the following code
import g4p_controls.*;

GEncoderKnob eknob;

void setup() {
  size(400, 200);
  eknob = new GEncoderKnob(this, 100, 50, 90, 90, 0.8);
  eknob.addEventHandler(this, "eknob_turn");
}

void draw() {
  background(200, 200, 240);
}

public void eknob_turn(GEncoderKnob encoderknob, GEvent event) {
  println("eknob - GEncoderKnob >> GEvent." + event + " @ " + millis());
}

Now run the sketch you will see your GEncoderKnob :grinning:

At the moment it behaves exactly like a GKnob because we haven’t customized the class.

The next step is to override the GKnob methods to make your class do exactly as you want.

These methods you might consider are

public void mouseEvent(MouseEvent event) 
public void draw() 
protected void updateBuffer()

G4P visual controls use an offscreen buffer for its display, the attribute bufferInvalid should be set to true whenever a mouse or other action would change the visual representation.

That should get you started but I recommend that you study the GKnob source code and examine these methods in particular.

3 Likes

Thanks for the reply quark. Made a good step and got the example code working. Since my experience with processing is minimal I’m getting stuck in how and where to add the necessary libraries. On rewriting some methods, several classes are not recognized by the complier, e.g. DBase.
Need some help to get processing setup correctly to be able to really get some coding done.
Please look at the minimized class code.
Thanks in advance,
Cheers

import g4p_controls.*;
import processing.core.*;
import java.awt.Graphics2D;
import java.io.IOException;
import java.io.ObjectInputStream;


public class GEncoderKnob extends GKnob {
  
  public GEncoderKnob(PApplet theApplet, float p0, float p1, float p2, float p3, float gripAmount) {
    super(theApplet, p0, p1, p2, p3, gripAmount); // call ctor in GKnob
  }

  public void draw(Graphics2D g, DBase selected){
    G.pushMatrix(g);
    g.translate(_0820_x, _0821_y);
    
    if(_0600_opaque){
      g.setColor(jpalette[6]);
      g.fillRect(0, 0, _0826_width, _0827_height);
    }
    g.translate(_0826_width/2, _0827_height/2);

    int s = Math.min(_0826_width, _0827_height), hs = s/2;
    // Bezel
    g.setColor(jpalette[5]);
    g.fillOval(-hs, -hs, s, s);
    // grip
    int gs = Math.round(0.7f * s), hgs = gs/2;
    g.setColor(jpalette[4]);
    g.fillOval(-hgs, -hgs, gs, gs);
    g.setColor(jpalette[14]);
    // Needle
    g.setStroke(needleStroke);
    g.drawLine(0, 0, Math.round(0.707f*hs), Math.round(0.707f*hs));

    g.translate(-_0826_width/2, -_0827_height/2);
    displayString(g, globalDisplayFont, name);
    if(this == selected)
      drawSelector(g);
    G.popMatrix(g);;
  } 
 
} 

That is because you are getting confused between GUI Builder and G4P.

  • GUI Builder is a tool used by the Processing IDE to help users design and create the code necessary to use G4P in the users sketch. It cannot be accessed, used by or required by the users sketch at runtime so ignore it.

  • G4P is a library that provides a range of GUI controls that can be included in a users sketch at runtime.

So forget about using any part of GUI Builder as it will not be used by the GEncoderKnob class we are creating.

The source code you need to focus on is G4P and in particular that for the class GKnob (source code)

If you want your GEncoderKnob class to work seamlessly with G4P it must follow the methodology used by the other controls. So some explanation is required

Methodology used by a G4P control class
A GUI control class uses an off-screen buffer to hold the current visual state (image) to be displayed in the sketch. This buffer is updated in the updateBuffer method like this.

protected void updateBuffer() {
  double a, sina, cosa;
  float tickLength;
  if (bufferInvalid) {
    bufferInvalid = false; // the buffer will be redrawn and so made valid
    buffer.beginDraw();
    buffer.clear();
    // Code to draw the control onto the buffer all Processing drawing
    // methods should be pre-fixed with buffer. e.g.
    // buffer.stroke(0);
    buffer.endDraw();
  }
}

The draw() method is basically the same for all controls and in part simply copies the buffer to the screen. If you don’t provide one it will use the draw method from the parent class, in this case GKnob. Now at the moment the GEncoderKnob class doesn’t need a draw method so do not include one.

Finally you need to override the method handles mouse events

public void mouseEvent(MouseEvent event)

To get started I would copy this method from the GKnob class into your class and try experimenting with it to understand how it works and adapt it to your needs. You might as well copy the updateBuffer method at the same time :smile:

When a mouse event e.g. mouse over changes the state of the control the attribute invalidBuffer is set to true. The update method is only call from the draw method and only if the invalidBuffer flag is true. This technique is called a lazy update because it is not executed for every invalidation event but only when the control has to be displayed.

Note that the methods

updateBuffer()
draw()
mouseEvent(MouseEvent event)

are used internally by G4P and are never called from the users sketch. Note that the first two have no parameters and the third a single parameter. This is important if G4P is to find these methods in your class.

I cannot stress enough that you will not be changing any code in the G4P library simply extending it with the GEncoderKnob control and that the main focus should be on the mouseEvent and updateBuffer methods.

I strongly recommend that you study the code in the GKnob class

2 Likes

Little steps…

I finally created the class and played with the mouse event method. Still don’t understand all functions (and got my work cut out….). At first glance, and after tinkering with it, I want to use the “event.getCount()”. This basically gives me the first good indicator of a encoder simulation (Still need to work on the visuals, but that’s not my biggest concern at this point). After some trail and erroring finally got it working. Please correct my code if I’m making wrong assumptions…
If added a variable in the class : protected int intWheelDirection = 1;
And a method to extract the value outside the class.
public int getWheelDirection(){
return intWheelDirection;
}
Added a line of code within the mouseEvent method under : case Mousewheel.WHEEL to set the value.
See the code snippets below.

Added class code.

  protected int intWheelDirection = 1;
  
  public int getWheelDirection(){
    return intWheelDirection; 
  } 

added variable value settign on event

    case MouseEvent.WHEEL:
      if (currSpot > -1 && z >= focusObjectZ()) {
        intWheelDirection=event.getCount();  <----

Using the wheel state in my main Sketch:

public void eknob_turn(GEncoderKnob encoderknob, GEvent event) {
  println("eknob - GEncoderKnob >> GEvent." + event + " @ " + millis());
  println("Wheel direction : "  + eknob.getWheelDirection());
}

Now getting to take on the challenge of matching the visuals to the encode behavior.

Next…

I’ve altered some visuals and it’s behavior to mimic the look and feel of the EncoderKnob.

First I’ve removed the angular restriction in the mouse.DRAG event by replacing the angleTarget calculations.

Next I’ve removed the Needle by removing the code snippet “// Draw needle” from the updateBuffer method.

In the updateBuffer method I’ve altered the showTrack arc to match to a floating one. Switched the startangle to anglePos.

And for last I’ve set the variables shopwTicks to false and setTurnMode to Angular.

Stiil have to solve the mousewheel restictions… :slight_smile:
Will use the class now and see if I need to change anything…

See code snippets below.

Class pre- settings:

    public GEncoderKnob(PApplet theApplet, float p0, float p1, float p2, float p3, float gripAmount) {
    super(theApplet, p0, p1, p2, p3, gripAmount); // call ctor in GKnob
   
    showTicks = false; // Get rid of the two start and end angle ticks.  // <---
    setTurnMode(CTRL_ANGULAR);  // <---
  }

Needle angle for full rotation:

            deltaMangle -= 360;
          // Calculate and adjust new needle angle
          angleTarget = angleTarget + deltaMangle;  // <-----
          parametricTarget = calcAngletoValue(angleTarget);
          // Update offset for use with angular mouse control
          offset += (angleTarget - lastAngleTarget - deltaMangle);
          // Remember target needle and mouse angles
          lastAngleTarget = angleTarget;
          lastMouseAngle = mouseAngle;

updateBuffer alterations:

        // Draw track?
        if (showTrack) {
         buffer.noStroke();
         buffer.fill(palette[14].getRGB()); 
          // Changed the arc from 0, to line position into a floating small arc at and of line
          buffer.arc(0,0, 2 * (gripRadius + bezelWidth * 0.5f), 2 * (gripRadius + bezelWidth * 0.5f),
          PApplet.radians(anglePos-15),PApplet.radians(anglePos+15));  //<----
         }
      }

      // draw grip (inner) part of knob
      buffer.strokeWeight(1.6f);   //1.6f
      buffer.stroke(palette[2].getRGB()); // was 14
      buffer.fill(palette[2].getRGB());
      if (drawArcOnly)
        buffer.arc(0, 0, 2 * gripRadius, 2 * gripRadius, PApplet.radians(startAng), PApplet.radians(endAng));
      else
        buffer.ellipse(0, 0, 2 * gripRadius, 2 * gripRadius);
      buffer.endDraw();

You definitely appear to be on the right track :smile: :+1:

The only code you need to write is in the new class as you can’t change any classes in G4P without affecting other sketches.

When you think you have done it then post the entire GKnobEncoder class and a simple sketch allowing interaction with it for us to test.

1 Like

…Done it?

Please test it a much as you think is necessary. There’s still a small hickup when switching from scroll wheel to again mouse drag movements. The indicator will jump a bit sometimes. Haven’t figured this one out yet.

See below the small sketch and class:

SKETCH

import g4p_controls.*;

GEncoderKnob eknob;

// Visuals
int BackgroundColor = 230;

//---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

public void setup(){
  
  // surface.setResizable(true);
  surface.setAlwaysOnTop(true);

  size(400,500, JAVA2D);
  background(BackgroundColor); // Set background
   
  eknob = new GEncoderKnob(this, 50,25,300, 300, 0.8);
  eknob.addEventHandler(this, "eknob_turn");
  
}

//---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

public void draw(){

  background(BackgroundColor);
   
  rect(15,373,372,120);
  fill(0);
  textSize(20);
  textAlign(CENTER,CENTER);
  text("ENCODER STEPS DECIMAL : "+ nf(eknob.getEncoderTicksF(),0,2),30,375,370,75);
  text("ENCODER STEPS ROUNDED : "+ nf(eknob.getEncoderTicksI()),30,405,370,75);
  // Show direction
  if (eknob.getEncoderTicksI() > 0)
  {
   fill(100,250,100); // Green for forewards
  }  
  else {
   fill(250,100,100); // Red for backwards
  }  
  
}

//---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

public void eknob_turn(GEncoderKnob encoderknob, GEvent event) {
  
  System.out.println(" Encoder steps count : " + nf(eknob.getEncoderTicksF(),0,0));
  
}

CLASS:

package g4p_controls;

import processing.core.PApplet;
import processing.event.MouseEvent;


public class GEncoderKnob extends GKnob {

  public GEncoderKnob(PApplet theApplet, float p0, float p1, float p2, float p3, float gripAmount) {
    super(theApplet, p0, p1, p2, p3, gripAmount); // call ctor in GKnob
  
    showTicks = false; // Hide the two start and end angle ticks.
    setTurnMode(CTRL_ANGULAR); // Always use angular.
   
  }

  protected float parametricTargetF = 0.5f;
  protected float fltEncoderTicks = 0;
  protected float EncoderWheelSpeed = 2.5f;  // For later use if wheel speed is a thing...

  public int getEncoderTicksI(){
      return Math.round(fltEncoderTicks); 
  }  

  public float getEncoderTicksF(){
    return fltEncoderTicks;
  }  

  public void mouseEvent(MouseEvent event) {
    if (!visible || !enabled || !available)
      return;

    calcTransformedOrigin(winApp.mouseX, winApp.mouseY);
    currSpot = whichHotSpot(ox, oy);

    manageToolTip();

    // Normalise ox and oy to the centre of the knob
    ox -= width / 2;
    oy -= height / 2;

    // currSpot == 1 for text display area
    if (currSpot >= 0 || focusIsWith == this)
      cursorIsOver = this;
    else if (cursorIsOver == this)
      cursorIsOver = null;

    switch (event.getAction()) {
    case MouseEvent.PRESS:
      if (focusIsWith != this && currSpot > -1 && z > focusObjectZ()) {
        startMouseX = ox;
        startMouseY = oy;
        lastMouseAngle = mouseAngle = getAngleFromUser(ox, oy);
        offset = scaleValueToAngle(parametricTarget) - mouseAngle;
        takeFocus();
      }
      break;
    case MouseEvent.WHEEL:
      if (currSpot > -1 && z >= focusObjectZ()) {
        fltEncoderTicks=-1*event.getCount()*EncoderWheelSpeed; // Make ticks avail for outside world
        // Replacing the calculation of the position to pure wheeldelta spinnings
        if (getEncoderTicksF()<0) 
          parametricTargetF = parametricTargetF - (wheelDelta*EncoderWheelSpeed);
        if (getEncoderTicksF()>0) 
          parametricTargetF= parametricTargetF + (wheelDelta*EncoderWheelSpeed);
        parametricTarget = parametricTargetF;   
      }
      break;
    case MouseEvent.RELEASE:
      if (focusIsWith == this) {
        loseFocus(parent);
      }
      // Correct for sticky ticks if needed
      if (stickToTicks)
        parametricTarget = findNearestTickValueTo(parametricTarget);        
      parametricTargetF = parametricTarget; // Make sure that last wheel turn by mouse movement is stored to ensure correct starting position when switching to scroll wheel;
      dragging = false;
      break;
    case MouseEvent.DRAG:
      if (focusIsWith == this) {
        mouseAngle = getAngleFromUser(ox, oy);
        if (mouseAngle != lastMouseAngle) {
          float deltaMangle= mouseAngle - lastMouseAngle;
          //float deltaMangle = mouseAngle - lastMouseAngle;
          // correct when we go over zero degree position
          if (deltaMangle < -180)
            deltaMangle += 360;
          else if (deltaMangle > 180)
            deltaMangle -= 360;
          // Calculate and adjust new encoder position, enabling full rotation
          angleTarget = angleTarget + deltaMangle; 
          parametricTarget = calcAngletoValue(angleTarget);           
          // Update offset for use with angular mouse control
          offset += (angleTarget - lastAngleTarget - deltaMangle);
          // Remember target needle and mouse angles
          lastAngleTarget = angleTarget;
          lastMouseAngle = mouseAngle;
          fltEncoderTicks = deltaMangle; // Make tikcs avail for outside world
        }
      }
      break;
    }
  }
 
  protected void updateBuffer() {
    double a, sina, cosa;
    float tickLength;
    if (bufferInvalid) {
      bufferInvalid = false;
      buffer.beginDraw();
      buffer.ellipseMode(PApplet.CENTER);
      // Background colour
      if (opaque == true)
        buffer.background(palette[6].getRGB());
      else
        buffer.background(buffer.color(255, 0));
      buffer.translate(width / 2, height / 2);
      buffer.noStroke();
      float anglePos = scaleValueToAngle(parametricPos);
      if (bezelWidth > 0) {
        // Draw bezel
        buffer.noStroke();
        buffer.fill(palette[5].getRGB());
        if (drawArcOnly)
          buffer.arc(0, 0, 2 * bezelRadius, 2 * bezelRadius, PApplet.radians(startAng),
              PApplet.radians(endAng));
        else
          buffer.ellipse(0, 0, 2 * bezelRadius, 2 * bezelRadius);
        // Draw ticks?
        if (showTicks) {
          buffer.noFill();
          buffer.strokeWeight(1.6f);
          buffer.stroke(palette[3].getRGB());
          float deltaA = (endAng - startAng) / (nbrTicks - 1);
          for (int t = 0; t < nbrTicks; t++) {
            tickLength = gripRadius + ((t == 0 || t == nbrTicks - 1) ? bezelWidth : bezelWidth * 0.8f);
            a = Math.toRadians(startAng + t * deltaA);
            sina = Math.sin(a);
            cosa = Math.cos(a);
            buffer.line((float) (gripRadius * cosa), (float) (gripRadius * sina),
                (float) (tickLength * cosa), (float) (tickLength * sina));
          }
        }
        // Draw track?
        if (showTrack) {
         buffer.noStroke();
         buffer.fill(palette[14].getRGB()); 
          // Changed the arc from 0, to line position into a floating small arc at and of line
         buffer.arc(0,0, 2 * (gripRadius + bezelWidth * 0.5f), 2 * (gripRadius + bezelWidth * 0.5f),
           PApplet.radians(anglePos-15), PApplet.radians(anglePos+15));
         }
      }
      // draw grip (inner) part of knob
      buffer.strokeWeight(1.6f);   
      buffer.stroke(palette[2].getRGB()); 
      buffer.fill(palette[2].getRGB());
      if (drawArcOnly)
        buffer.arc(0, 0, 2 * gripRadius, 2 * gripRadius, PApplet.radians(startAng), PApplet.radians(endAng));
      else
        buffer.ellipse(0, 0, 2 * gripRadius, 2 * gripRadius);
      buffer.endDraw();
    }
  } 

 
}  
1 Like

Actually I meant ‘for us to play with’ after all testing is your job since you know what you expect it to do. I am sure you will resolve any small problems.

The user can use this approach of using existing controls as the base for new ones. Don’t forget just because it inherits from an existing control you don’t have to keep unwanted functionality.

What else can I say but well done :smile: