0087: Nodes-n-noodles VI – Adding Hot Spots

Here’s a look at what we’re dealing with today. It may look the same as the previous couple of screen shots, but clicking on one of the node connectors (yellow or orange circles) or the light blue drag-bar area dumps a line to the terminal.

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

The objective now is to move toward a node that’s actually moveable… like the class name says. Here’s a breakdown of how we’re going about this:

  • when the pointer is within certain areas on the node, and
  • the user clicks and holds mouse button #1, and
  • moves the pointer again,
  • a function triggers that moves the node.

The first step toward this is to define those areas as…

Hotspots

Figure 1: The Node
Figure 1: The Node

This just means we’re going to define parts of the node that will react to mouse clicks. Those would be the orange and yellow circles and the light blue rectangle at the top.

Why do we need multiple areas? Because eventually, we’ll also be connecting nodes one to another and this means we’ll want hotspots that react to:

  • an incoming connection,
  • an outgoing connection, and
  • mouse click-n-drag.

Everything we need to do in order to get these hotspots set up will take place in the MoveableNode class. Let’s look at these changes by section…

The Preamble

Here’s where we declare the hot spots:

double[string] dragArea;
double[string] inHotspot;
double[string] outHotspot;

These three arrays will hold x/y coordinates for the upper-left and lower-right corners of the hot areas.

The Constructor

We leave the definition of the hotspot arrays until the constructor because we’re using associative arrays and, by nature, associative arrays aren’t constant and therefore can’t be defined in the preamble. But that’s okay, we’re just as happy to fill in the details here in the constructor:

dragArea = ["left" : 13, "top" : 9, "right" : 99, "bottom" : 30];
inHotspot = ["left" : 0, "top" : 27, "right" : 10, "bottom" : 38];
outHotspot = ["left" : 100, "top" : 60, "right" : 110, "bottom" : 70];

And why associate arrays? Because when it comes time to put these values into play in our code, it’ll be easier to keep track of what we’re doing if we refer to them as dragArea[“left”] or outHotspot[“top”] as opposed to using a numerical index.

And we add one more line to the constructor to hook up the all-important onButtonPress() callback:

addOnButtonPress(&onButtonPress);

For this stage, that’s all we need to do here so, moving on…

The Callback

Here’s the onButtonPress() function:

	bool onButtonPress(Event event, Widget widget)
	{
		GdkEventButton* buttonEvent = event.button;
		int button1 = 1;
		double xMouse, yMouse;
		xMouse = buttonEvent.x;
		yMouse = buttonEvent.y;

		// restrict active areas to terminal connections and the dragbar
		if(xMouse > dragArea["left"] && xMouse < dragArea["right"] && yMouse > dragArea["top"] && yMouse < dragArea["bottom"])
		{
			if(buttonEvent.button is button1) // ModifierType.BUTTON1_MASK
			{
				// dragArea
				dragAreaActive(xMouse, yMouse);
			}
		}
		else if(xMouse > inHotspot["left"] && xMouse < inHotspot["right"] && yMouse > inHotspot["top"] && yMouse < inHotspot["bottom"])
		{
			if(buttonEvent.button is button1) // ModifierType.BUTTON1_MASK
			{
				// inHotspot
				terminalInActive(xMouse, yMouse);
			}
		}
		else if(xMouse > outHotspot["left"] && xMouse < outHotspot["right"] && yMouse > outHotspot["top"] && yMouse < outHotspot["bottom"])
		{
			if(buttonEvent.button is button1) // ModifierType.BUTTON1_MASK
			{
				// inHotspot
				terminalOutActive(xMouse, yMouse);
			}
		}
	
		return(true);
		
	} // onButtonPress()

This is where we separate the clicks we want to deal with from those we don’t, but we have a bit of prep to do before we can distinguish between them. And it starts with this line:

GdkEventButton* buttonEvent = event.button;

What’s happening here is similar to a cast. The event variable passed into the function is generic, but we need to know the type of Event it represents.

If you look at the GtkD wrapper source (generated/gtkd/gdk/c/types.d) starting on line #2495, you’ll see that this Event construct has some depth. It’s a struct wrapped around a union. And within the union, there are multiple structs, each defining a different type of Event. There’s a motion Event, a button Event, and a touch Event—just to name a few. And each of these Event types has properties meaningful to the type of Event it is. What we need to do is dig down into the Event’s union to pull out the type of Event we’re dealing with and from that, get the information we need… and what we need is three things:

  • the x coordinate of the pointer when the mouse button was pressed,
  • the y coordinate, too, and
  • the number of the mouse button… so we aren’t reacting to them all.

The Event we’re after in this situation is the GdkButtonEvent, a struct which looks like this:

struct GdkEventButton
{
	GdkEventType type;
	GdkWindow* window;
	byte sendEvent;
	uint time;
	double x;
	double y;
	double* axes;
	ModifierType state;
	uint button;
	GdkDevice* device;
	double xRoot;
	double yRoot;
}

Of all the values in this struct, what we want are the x and y variables which, as you might expect, reflect the position of the pointer within theDrawingArea.

Figure 1: button variables at two levels in the hierarchy
Figure 1: button variables at two levels in the hierarchy

Note: We don’t use xRoot and yRoot because they report the pointer position in relation to the upper-left corner of the Screen, the Screen being the area of your entire display—the monitor(s) connected to your computer. We’ll get into this Screen stuff in a later post.

Another note: Keep in mind, too, that a button variable also appears at two different levels of the hierarchy. The first (highest, outermost) is a GdkEventButton struct and the second (lowest, innermost) is a uint. Confusing the two could get dicey.

Getting back to the discussion at hand… of all the data contained within the event, all we need are:

  • the x and y coordinates, and
  • the two button values:
    • the GdkEventButton struct from which we’ll glean those coordinates, as well as
    • the uint representing the mouse button that was pressed.

So, we grab the button from the top-level of the Event and dig into it to get the mouse button number. We could also have addressed it directly as: event.button.button.

The x/y coordinates are pulled from the GdkEventButton struct and assigned to xMouse and yMouse. As mentioned above, these keep track of where the pointer was when the physical mouse button was clicked.

Then we use an if/else construct to differentiate between the various hotspot areas, calling a different handler function for each, those handlers being dragAreaActive(), terminalInActive(), and terminalOutActive() as mentioned above.

We could have done this the other way around but it really doesn’t matter in the end whether the inner if/else deals with which mouse button was pressed or which hotspot was under the pointer at the time.

And finally, we have…

The Active() Functions

There are three of them (terminalInActive(), terminalOutActive(), and dragAreaActive()) We’ll just look at one because they’re pretty much the same:

void dragAreaActive(double xMouse, double yMouse)
{
	// see if the mouse is in the drag area 
	writeln("dragArea: xMouse = ", xMouse, " yMouse = ", yMouse);
		
} // dragAreaActive()

Right now, all these functions are stubs. Next time, we’ll start looking at their content.

Conclusion

So, our hotspots are set up and all we have to do now is work out how to make this sucker move.

Note that at this time, we have no idea where the mouse pointer is in relation to our NodeLayout—that is, the surface we’ll be moving the Node around on—but that’s okay. We’ll look at that next time, too.

See you then.

Comments? Questions? Observations?

Did we miss a tidbit of information that would make this post even more informative? Let's talk about it in the comments.

You can also subscribe via RSS so you won't miss anything. Thank you very much for dropping by.

© Copyright 2023 Ron Tarrant