Another day, another custom component in Flex 4. This time it’s a closeable TabBar that I’m calling TerrificTabBar, because SuperTabBar is taken. This post is basically Part 2 of my previous post on SuperTextInput. And much like that post, TerrificTabBar follows the Enhanced Component Pattern by extending TabBar and adding some new functionality.
Extend TabBar and TabBarButton
I just want to build your average web browser tab in Flex 4, no more, no less. Browser tabs have two cool features: each tab has a close button, and the tabs can be reordered by dragging. Actually, I just lied about the no less part, because I’m going to ignore dragging and focus my attention on the closeable tab part. Please see my previous post on drag-and-drop in Flex 4 if you are interested in the dragging part.
A TabBar is just a bag of tabs, where each individual tab is a button, or a TabBarButton to be more precise. To create a closeable tab, we need to add a new button that can be clicked to close the tab, duh. And this is in addition to the default behavior of clicking the tab to select it. Since we are disciples of The Flex 4 Way of building custom components, we know this means we must extend TabBarButton and add a new Button SkinPart to it:
package components { ... public class TerrificTabBarButton extends ButtonBarButton { [SkinPart(required="false")] public var closeButton:Button; private var _closeable:Boolean = true; public function TerrificTabBarButton() { super(); //IMPORTANT: this enables the button's children (aka the close button) to receive mouse events this.mouseChildren = true; } [Bindable] public function get closeable():Boolean { return _closeable; } public function set closeable(val:Boolean):void { if (_closeable != val) { _closeable = val; closeButton.visible = val; labelDisplay.right = (val ? 30 : 14); } } private function closeHandler(e:MouseEvent):void { dispatchEvent(new TerrificTabBarEvent(TerrificTabBarEvent.CLOSE_TAB, itemIndex, true)); } override protected function partAdded(partName:String, instance:Object):void { super.partAdded(partName, instance); if (instance == closeButton) { closeButton.addEventListener(MouseEvent.CLICK, closeHandler); closeButton.visible = closeable; } else if (instance == labelDisplay) { labelDisplay.right = (closeable ? 30 : 14); } } override protected function partRemoved(partName:String, instance:Object):void { super.partRemoved(partName, instance); if (instance == closeButton) { closeButton.removeEventListener(MouseEvent.CLICK, closeHandler); } } } }
First, we have the closeButton SkinPart and it’s closeHandler() event handler. Wiring the handler function to the button is done in the partAdded() override, and un-wiring in the partRemoved() override. The handler just bubbles a closeTab custom event which will be handled by the parent TabBar. Next, we use the closeable property to manage the close button’s visibility. Finally, an additional button visibility check is necessary in partAdded() to set the initial visibility.
There are two other interesting pieces of code. One, we must set mouseChildren = true in the constructor to enable our button within a button to receive and respond to mouse events. Two, in a somewhat hackish fashion, I add or remove padding from the tab’s label when the close button is present or not.
Now that we have a nice close button on the individual tabs, we extend TabBar to make a pretty API for managing tabs and their new functionality. Here is an abbreviated TerrificTabBar component showing the relevant public methods:
package components { ... public class TerrificTabBar extends TabBar { public function TerrificTabBar() { super(); } public function setCloseableTab(index:int, value:Boolean):void { if (index >= 0 && index < dataGroup.numElements) { var btn:TerrificTabBarButton = dataGroup.getElementAt(index) as TerrificTabBarButton; btn.closeable = value; } } public function getCloseableTab(index:int):Boolean { if (index >= 0 && index < dataGroup.numElements) { var btn:TerrificTabBarButton = dataGroup.getElementAt(index) as TerrificTabBarButton; return btn.closeable; } return false; } private function closeHandler(e:TerrificTabBarEvent):void { closeTab(e.index, this.selectedIndex); } public function closeTab(closedTab:int, selectedTab:int):void { ... } protected override function partAdded(partName:String, instance:Object):void { super.partAdded(partName, instance); if (instance == dataGroup) { dataGroup.addEventListener(TerrificTabBarEvent.CLOSE_TAB, closeHandler); } } protected override function partRemoved(partName:String, instance:Object):void { super.partRemoved(partName, instance); if (instance == dataGroup) { dataGroup.removeEventListener(TerrificTabBarEvent.CLOSE_TAB, closeHandler); } } } }
There is a public getter and setter for toggling the close-ability of individual tabs, setCloseableTab() and getCloseableTab(). And also a public method for closing a tab, closeTab(), which can be used to force close an un-closeable tab. Additionally, closeTab() is used internally to close a tab when its close button is clicked. As is standard, the wiring and un-wiring of the closeHandler() handler function is done in the partAdded() and partRemoved() overrides.
Very vanilla, but there is one interesting part worth mentioning. I made the decision to attach the listener to the dataGroup property of TabBar, this is the default container of individual tabs (aka TerrificTabBarButtons). Yes, I could just as easily have attached the handler directly to TabBar in the constructor and used weak references. But after having built enough custom Flex 4 components, it feels funny to me to do any event wiring outside of partAdded() and partRemoved(). Thus, I made the decision to attached to dataGroup. Without doing a bunch of testing (which I haven’t done), I can’t say if this is better or worse from a memory or performance perspective, I just like the way this code looks better.
Conclusion
I put a pretty skin on everything using some of my favorite drawing tricks, but that’s the subject of a future post. Some brief highlights include:
- I used CSS to wire my components to my skins, so I minimize the use of
skinClassattributes for better decoupling - I used a big negative
gapinTerrificTabBarSkinto make the tabs overlap - I set a
maxWidthand aminWidthon the individual tabs inTerrificTabBarButtonSkin, which in combination withmaxDisplayedLines="1"on theLabel, make the tabs truncate with...when they get too big - The curved tab shape is drawn with a
Path, but I wrap it with aGroupand set theGroup‘s scale-9 grid (scaleGridLeft,scaleGridTop, etc.) so it expands and contracts correctly - The close button’s x is drawn as a + around the coordinate origin, also known as (0,0), and then rotated 45 degrees
Here’s the finished product (view source enabled):
Use it and enjoy.
