With the new tab support in BottomFeeder, I thought I should explain one of the customizations - the tab menus. As it happens, the tab widget in VisualWorks doesn't support menus out of the box. So how did I add them? Well, first off, I noticed that Typeless has tab menus (Typeless also ships as a BottomFeeder plugin, which is how I use it). So, I loaded TL from the Public Store and started having a look around. It turns out that Michael extended the widget in the most natural fashion, by creating a new controller subclass. Normally, the tab bar is managed by a class called TabControlBarController. So, first thing - he created this class:
Smalltalk.UI defineClass: #TabControlBarControllerWithMenu
superclass: #{UI.TabControlBarController}
indexedType: #none
private: false
instanceVariableNames: 'menuHolder performer owner mouseDownIndex '
classInstanceVariableNames: ''
imports: ''
category: 'UIBasics-Controllers'
Then you have to modify the view class, so that it uses this controller (or, at the time you create the view, replace it. This code just replaces the default controller) - here's TabControlBarView>>defaultControllerClass:
defaultControllerClass
^TabControlBarControllerWithMenu
That makes sure that we always get menu support when we create a tab widget - assuming that the new controller class is implemented properly. Here's what's going on there:
- First, add some new instance variables to handle menus and the state required for them: menuHolder performer owner mouseDownIndex
- implement #yellowButtonPressedEvent: so as to know when to pop the menu
- implement all the other supporting methods
There's a small side story in the name of #yellowButtonPressedEvent: - back in the day, at Xerox PARC, the mouse actually had colored buttons - red, yellow, and blue. Those names still exist in the code for VW (and, I think, for Squeak). Anyway - the method:
yellowButtonPressedEvent: event
"we about to bring up a menu, unlock the event
queue so that we can process expose events."
| index |
view numberOfElements = 0 ifTrue: [^self].
index := self findElementFor: (self sensor cursorPointFor: event).
(self owner respondsTo: #adjustTabbarMenuFor:) ifTrue:
[self owner adjustTabbarMenuFor: index].
self sensor windowSensor queueLocked: false.
self processMenuAt: event globalPoint centered: true.
^nil
The reference to #adjustTabbarMenuFor: is something I missed out of the gate. I had to implement that in my class, in order to ensure that the menus were set up correctly - it looks like this:
adjustTabbarMenuFor: anIndex
| tabModel sub tabbed menu |
anIndex = 0 ifTrue: [^self].
tabModel := self browserTabs at: anIndex.
sub := self widgetAt: #feedID.
tabbed := self getComponentFromSubcanvas: sub withID: #TwoflowerHTML1.
menu := self class tabMenu.
(tabbed widget tabBar component controller)
menuHolder: (ValueHolder with: menu);
performer: tabModel;
owner: self.
self checkTabMenuEnablement: menu
All of which does the following - make sure that a new tab has a menu, and that it's attached to that menu. As well, make sure that menu items are in the right enablement state. There's a few other setup methods, but you can see all of that by browsing the package RSSExtensions in the Public Store. The next important thing was setting the tabs up properly in my UI class. In my #postOpenWith: method (which fires after the UI opens, I added the following:
self presetTabs.
tab := self changedTab.
self setupTabMenu: tab
presetTabs
| browserTab |
browserTabs := OrderedCollection new.
browserTab := RSSZoomItem new.
browserTabs add: browserTab.
self tabs list add: browserTab displayString.
self tabs selectionIndex: 1.
self setupEventHandlingForTab: browserTab
changedTab
"changed to a new tab; adjust"
| index browserTab sub tabbed |
index := self tabs selectionIndex max: 1.
browserTab := self browserTabs at: index.
sub := self widgetAt: #feedID.
tabbed := self getComponentFromSubcanvas: sub withID: #TwoflowerHTML1.
tabbed widget client: self htmlModel spec: #windowSpec builder: self builder.
(browserTab feed isNil or: [browserTab feed isFake])
ifFalse: [self focusOnItem: browserTab item].
self tabs selectionIndex = 0
ifTrue: [self tabs selectionIndex: 1].
self setCurrentTabDetails.
self setupHTMLPane.
self setBrowserEvents.
self restoreHistoryFrom: browserTab.
^browserTab
setupTabMenu: aZoomItem
| tabbed sub menu |
sub := self widgetAt: #feedID.
tabbed := self getComponentFromSubcanvas: sub withID: #TwoflowerHTML1.
menu := self class tabMenu.
(tabbed widget tabBar component controller)
menuHolder: (ValueHolder with: menu);
performer: aZoomItem;
owner: self.
self checkTabMenuEnablement: menu
Those three methods are the heart of the support. The first one sets the tabs up - with the proper domain model (in this case, an object that holds the selected feed, the selected item, and the history), and sets the current tab index. The second method, #changedTab, fires whenever a tab is selected. It ensures that the correct stuff gets displayed, and that state stays correct. The last one, #setupTabMenu: makes sure that menus are set up for selected tabs.
There's some infrastructure there - event handling, and some application specific stuff - but that's the gist of it. If you need help doing something like this, just send me an email.