0079: Notebook III – Customized Tabs, Part I

Here’s a screen-shot of what we’re up to:

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

Notebook tabs can be pretty much whatever you want. I read in one book that you could even, in theory, stuff an entire other Notebook inside a Notebook tab. We’re not going that far, but we will do some cool stuff.

But customized tabs can get tricky if you stuff anything into them besides a Label or Image widget. The reason it’s tricky? Because any widget that absorbs mouse clicks will stop the tab from responding to user input.

Figure 1: Active tab areas
Figure 1: Active tab areas

Okay, well… this isn’t 100% true. A label widget (notice, that’s label with a lowercase ‘l’) will not take up the full space allotted to a tab, so if your user clicks to the immediate left or right of the child widget, the result will still be as expected. But that’s not exactly the kind of thing you wanna be writing up in your docs, so we’re going to cook up a workaround. We’ll let the child widget react and harness that signal to trigger whatever would have been triggered by clicking directly on the tab.



Class Preamble for a Notebook with Customized Tabs

Our class starts off like this:

PositionType tabPosition = PositionType.TOP;
string tabLabelPrefix = "Custom Tab";

CustomTab customTab1, customTab2, customTab3;
MyTextView myTextview1, myTextview2, myTextview3;
string myTextView1Content = "Now is the witness of our discontinent.";
string myTextView2Content = "Four stores and seven pounds ago...";
string myTextView3Content = "Help me open yon cantelope.";

We’ve seen all this before. After the tab position variable, we have a prefix for label text (note: this isn’t Label text, just label text), content strings, and declarations for widgets we’ll be stuffing into the tabs.

The Notebook constructor–in fact, the rest of the Notebook class code—is almost the same as we’ve already worked with. The only thing that’s really different is the instantiation of the tab. Here’s what one of those looks like:

myTextview1 = new MyTextView(myTextView1Content);
customTab1 = new CustomTab(tabLabel1, 0, this);
appendPage(myTextview1, customTab1);

Instead of a Label, we’re instantiating a CustomTab object and we’re passing it three arguments:

  • a string that will appear as the prefix in the customized tab label,
  • a number to go with that prefix (so the tab’s name will be CustomTab 0 in this case), and
  • a pointer to the Notebook itself to facilitate communication from the customized tab back to the Notebook. (This is part of the workaround I mentioned earlier.)

Moving right along…

A Signal & Callback for Switching Pages

One more thing that’s different in the Notebook constructor is this line:

addOnSwitchPage(&onSwitchPage);

Here, we’re harnessing a signal that will trigger a callback every time the user clicks on a tab other than the one that’s current. And here’s the callback:

void onSwitchPage(Widget widget, uint pageNumber, Notebook notebook)
{
	_currentPage = pageNumber;
		
	writeln("Current page: ", _currentPage);
			
} // addOnChangeCurrentPage()

It’s straightforward. All it does is report which page is now current. But this callback will play an important part once everything’s hooked up and running because it’s the only way we have of knowing, programmatically, that the page has been switched. More about this later, too.

The Custom Tab Class

class CustomTab : Box
{
	TabDrawingArea tabDrawingArea;
	Notebook _notebook;
	
	this(string tabLabel, Notebook notebook, int tabIndex)
	{
		super(Orientation.HORIZONTAL, 0);
		tabDrawingArea = new TabDrawingArea(tabLabel, notebook, tabIndex);
		packStart(tabDrawingArea, true, true, 0);
		_notebook = notebook;
		showAll();
		
	} // this()
	
} // class CustomTab

This is just a Box. At the moment, we don’t really need this intermediary widget, but down the road, if we want to stuff more than one child widget into a tab, we will, so here it is.

All this does is serve to contain a DrawingArea… which it does quite well.

One thing I’ll point out here… this Box needs to make a call to showAll() so we get to see what’s in the Box. Without it, we’d be looking at a blank DrawingArea.

Conclusion

Over the next two blog posts, we’ll dig further into the customization code. Until next time, take care and may your inspiration always be inspired-ing-ly amazing.

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