Today we take our knowledge of Menus and apply it to the MenuButton
Design
We'll make a MenuButton, fill it with a menu of Look names, and then, when a look is selected, we'll change to that look. We'll cache the current look on startup, and make sure to reset it when we close our window.
As always, we start with a subclass of UserInterface:
Smalltalk defineClass: #MenuButtonWork
superclass: #{Widgetry.UserInterface}
indexedType: #none
private: false
instanceVariableNames: 'originalLook menuButton '
classInstanceVariableNames: ''
imports: 'private Widgetry.*'
category: '(none)'
Capture The Flag
To make sure we capture the look that is in place when we open our window, we override the #initialize method:
initialize
originalLook := Screen default defaultWidgetPolicy lookPolicyClass.
super initialize.
Don't forget that call to super initialize!
Next we create a #hookupWindow method. In it we subscribe to the Closing Announcement of the window, and tell it to send a message to us when the window is being closed:
hookupWindow
self mainWindow when: Closing send: #resetLookIfNeeded to: self.
Finally we write the #resetLookIfNeeded method to reset the look back to the original if it has changed:
resetLookIfNeeded
originalLook == Screen default defaultWidgetPolicy lookPolicyClass
ifFalse: [originalLook installLookPreferences]
This also shows how one changes the current look programmatically. All one has to do is send UILookPolicy. Wrapper has 7 looks: Default, Mac, MacOSX, Motif, Win95, Win98 and WinXP. Default is a very old look, which some have called the Smalltalk look. The only meaningful difference between the Win95 and Win98 look is how toolbar buttons look and act.
Wrapper supports only 4 looks: MacOSX, Win9x, Motif and WinXP. The Win9x look is in effect a Windows 98 look. Widgetry does not have a "classic" Mac look, and it finally puts to rest the Default look.
Each look is associated with an existing Wrapper look policy: MacOSX with MacOSXLookPolicy, Win9x with Win95LookPolicy, Motif with MotifLookPolicy and WinXP with WinXPLookPolicy. Whereas each of these has both Widgetry and Wrapper code in them, there is no sharing of that code. Thus, changing the look will affect both Wrapper and Widgetry, but it allows the removal of Wrapper methods in the future.
When one changes the look, Widgetry automatically will update all widgets, whereas Wrapper required you to write code to tear down the old widgets and rebuild them. This has been a complaint about our tools for some time. If someone changed the look in the settings, not all of the tools would change to match the new selected look. Over the years, the Tools team has updated the Launcher, Workspace and RefactoringBrowser to do the changes required, but still, other tools do not. With Widgetry, no one has to worry about it. It just happens.
The Work
A MenuButton is used when you have a fixed set of selections, and no requirement to dynamically enter a selection (as is possible with a DropDownList).
Here is our #createInterface method:
createInterface
menuButton := MenuButton new.
menuButton frame: (FractionalFrame
fractionLeft: 0.1
right: 0.9
top: 0.1
bottom: 0.3).
self addComponent: menuButton
If we execute MenuButtonWork open, here is what we see:
Fill It Up
Next we want to fill our MenuButton with a Menu. We use a regular Menu, just like we do for our work the last two times. To tell a MenuButton what menu to use when activated, we send the method #dropDownMenu: to the MenuButton, and pass our menu as the parameter.
As always, we put our configuring methods in the #hookupInterface method:
hookupInterface
| menu |
menu := Menu new.
menu addMenuItem: ((MenuItem label: 'WinXP Look') id: WinXPLookPolicy).
menu addMenuItem: ((MenuItem label: 'Win9x Look') id: Win95LookPolicy).
menu addMenuItem: ((MenuItem label: 'Motif Look') id: MotifLookPolicy).
menu addMenuItem: ((MenuItem label: 'MacOSX Look') id: MacOSXLookPolicy).
menuButton dropDownMenu: menu
Here is what it looks like, once we re-open our window, and press the button:
And here is what it looks like after we select one of the items in the menu::
There are a couple of things you'll notice here. When the MenuButton has focus, you can hit the down arrow key, and the menu will open. From there, using the arrow keys will highlight successive menu items. Pressing the Escape key or the Space key, closes the menu without choosing a menu item. Pressing Enter chooses a menu item.
The size of the drop down menu is always the full width of the MenuButton, or the minimum width to show the menu, which ever is larger. So if you make the window wider (and we used a FractonalFrame, so the MenuButton will get wider also), then the resulting menu will get bigger, to match the width of the MenuButton.
If you resize the window to it's narrowest, then the menu will make sure it can be fully shown, and actually be wider than the MenuButton:
Dance The Dance
You may have noticed that for the ID for each menu item, I gave it the actual Look policy class. Again, like we did with RadioButtons, we can use anything which can be uniquely identified.
We did this because now we want to watch for MenuItemSelected Announcement in our MenuButton. Once that is announced, the actual MenuItem will be in the Announcement. With that, we can simply send #installLookPreferences to the ID in the MenuItem:
hookupInterface
| menu |
menu := Menu new.
menu addMenuItem: ((MenuItem label: 'WinXP Look') id: WinXPLookPolicy).
menu addMenuItem: ((MenuItem label: 'Win9x Look') id: Win95LookPolicy).
menu addMenuItem: ((MenuItem label: 'Motif Look') id: MotifLookPolicy).
menu addMenuItem: ((MenuItem label: 'MacOSX Look') id: MacOSXLookPolicy).
menuButton dropDownMenu: menu.
menuButton when: MenuItemSelected do:
[:announcement | announcement item id installLookPreferences]
Yes, its just that simple!
Notice that the announcement is optionally sent as a parameter to the #when:do: block. All we have to do is send the right message
And here is what it looks like after we choose each item:
Now, even if we change the look, and close the window, the original look will re-install itself
Extra Credit
For fun, and to show that automatic changes really do take place, let's add a few more widgets and watch them change:
createInterface
| radioButton checkBox spinButton |
menuButton := MenuButton new.
menuButton frame: (FractionalFrame
fractionLeft: 0.1
right: 0.9
top: 0.1
bottom: 0.3).
self addComponent: menuButton.
spinButton := SpinButton new.
spinButton frame: (FractionalFrame
fractionLeft: 0.05
right: 0.95
top: 0.5
bottom: 0.65).
self addComponent: spinButton.
checkBox := CheckBox withLabelString: 'Check Box'.
checkBox frame: (FractionalFrame
fractionLeft: 0.05
right: 0.35
top: 0.75
bottom: 0.9).
self addComponent: checkBox.
radioButton := RadioButton withLabelString: 'Radio Button'.
radioButton frame: (FractionalFrame
fractionLeft: 0.45
right: 0.95
top: 0.75
bottom: 0.9).
self addComponent: radioButton.
And here are the results:
Isn't that pretty?
And So It GoesSames