Friday, May 26, 2006

Let's look at the class, RadioGroupManager, that provides the functionality necessary to ensure that only one radio button in a group is selected:

#if !defined(__RADIOGROUPMANAGER_H__)
#define __RADIOGROUPMANAGER_H__

#include <map>
#include <list>
#include <string>

#include "cocoaradiobuttonimpl.h"

using namespace std;

class Document;

class RadioGroupManager
{
public:
PRStatus AddToGroup(const string &group, CocoaRadioButtonImpl *impl);
PRStatus RemoveFromGroup(const string &group,
CocoaRadioButtonImpl *impl);
PRStatus SetState(const string &group, CocoaRadioButtonImpl *impl);
static PRStatus AddManager(const Document *doc);
static PRStatus RemoveManager(const Document *doc);
static RadioGroupManager *GetManager(const Document *doc);
private:
map < string, list <CocoaRadioButtonImpl *> *> m_groups;
static map <const Document *, RadioGroupManager *> m_documentMap;
};

#endif

This class is private (and unique) to the Cocoa implementation. I know that Gtk+ does not require it, and very likely, I believe .NET will not require it either.

The basic idea of the above class is to maintain, per document, a list of radiobutton widgets that belong to the same class, for each class specified in the document. When a window is created, the concrete implementation will register the document corresponding to the window by calling the static function AddManager(). AddManager creates an instance of RadioGroupManager, and places it in a static map (m_documentMap in the above listing). Similarly, when the window goes away, RemoveManager() is called by the Cocoa concrete window widget destructor to remove the entry from the map. We need to maintain one RadioButtonManager instance per document/window, since groups are scoped to the window in which they are defined (we might have 2 radiobutton groups with the same name; as long as they are in different window/document instances, this is ok.)

As radiobutton widgets are instantiated, the concrete Cocoa widget implementation will add itself to a second map. This map is maintained privately by an instance of RadioGroupManager; which RadioGroupManager instance is determined by looking in the m_documentMap map using the document as the key.

The key to this second map is the group attribute assigned to the radiobutton. The entry in the map corresponding to this key is an STL list; each item on that list is a pointer to the CocoaRadioButtonWidgetImpl class that implements that widget.

When a Cocoa radio button is clicked, a callback is made into the CocoaRadioButtonImpl instance. When this occurs, the callback simply determines the group assigned to itself (stored as a member variable), then it determines the RadioGroupManager by indexing the m_documentMap map using its document pointer. Then, the list of CocoaRadioButtonImpl widgets found in the list maintained by RadioGroupManager is iterated; if the widget is not the one clicked, then a call is made to the NSButton widget instance to set its state to NSOffState, like this:

PRStatus
RadioGroupManager::SetState(const string &group,
CocoaRadioButtonImpl *impl)
{
if (m_groups.find(group) != m_groups.end()) {
list <CocoaRadioButtonImpl *> *rbList;
rbList = m_groups[group];
if (rbList) {
list <CocoaRadioButtonImpl *>::iterator iter;
for (iter = rbList->begin(); iter != rbList->end(); ++iter) {
if ((*iter) != impl)
(*iter)->SetState(NSOffState);
}
}
}
return PR_SUCCESS;
}

This sort of implementation/logic is probably at the heart of all radio button widget implementations, regardless of the oolkit or platform, including Cocoa. Where Cocoa falls down is in coupling the logic to a widget (NSMatrix) that impacts UI and layout. For Cocoa developers coding directly to Cocoa, or using XCode tools, NSMatrix is great. But, as I mentioned in the previous post, it is critical for some applications (such as mine) to decouple the GUI aspect of radiobutton management from the functional aspect. For me, it was critical to replace the Cocoa scheme with one of my own, since I had to have direct control of layout. Perhaps another advantage at doing things the way I did is now I could have radio buttons scattered throughout the window that belong to the same radio group. Not that this is generally a good thing to do in terms of usability -- usually, you want to have radio buttons that are in the same group physically adjacent in a window -- but it is now possible should a crazed UI designer insist on it.