1. Focus, the details
Please refer to the previous post for the context about Focus. Basically, Curses::Toolkit is a Perl software toolkit that let's you create nice ncurses based interfaces. It extensively uses widgets, that can have the focus.
As I said in the previous post, not every kind of widgets can have the focus : only a widget that can be interacted by the user should be focusable. For instance, a Label doesn't offer by default any interaction to the user. So a Label widget is not focusable. Likewise, a Border is just displaying a graphical border, and doesn't offer interaction to the user, so same, a Border shouldn't be focusable.
Now let's have a look at the Button widget : it should clearly be focusable, as by default a button is clickable, and do be able to "click" it using the keyboard, the user needs to position the focus on the button and trigger the enter key or the space key. So it's obvious that the Button widget should be focusable
Now let's think about the implementation : the Button widget class is just an nearly empty class that inherits from Border, provides a facility constructor that would add a label in the Border. So a Button widget is basically a Border Widget. But we want to add the focusability concept. That is the situation where you would use words like interfaces, mixins, traits and roles. I like the concept of Role, and I think it suits the purpose here, so I decided to implement the focusability with a Role.
2. Roles
A Role is basically some features (code, attributes), that are injected in the class, but unlike a mixin, it is possible to see from which roles a class has been built from, and change that. Moose offers a great implementation of Roles, as does Perl6. Alas, I haven't yet converted Curses::Toolkit to Moose, so I needed a pure Perl5 solution : multiple inheritance !
Right, multiple inheritance is wrong, it's bad, it's pure evil. That's what you've been told at least. I have to say it's true, multiple inheritance is very rarely a good thing, and it can generate a lot of issues (and make it inherently more difficult to fix them).
However in this case, I thought I could use it, provided I take care of limiting the use of multiple inheritance to implement Roles, and be careful not creating the infamous diamond-of-death structure.
3. Implementation
Here is how the Curses::Toolkit::Widget::Button class look like :
package Curses::Toolkit::Widget::Button;
use parent qw(Curses::Toolkit::Widget::Border Curses::Toolkit::Role::Focusable);
By inheriting from Border, we get all its attributes and methods. By inheriting from the Focusable role, we also signify that this widget can be focused. Let's look at the content of Curses::Toolkit::Role::Focusable :
package Curses::Toolkit::Role::Focusable;
[...]
sub new {
my ($class) = shift;
die "role class, has no constructor";
}
[...]
sub is_focusable {
my ($self) = @_;
return 1; # in real life it's less trivial
}
sub is_focused {
my ($self) = @_;
return $self->get_property(basic => 'focused');
}
sub set_focus {
my $self = shift;
my ($focus) = validate_pos( @_, { type => BOOLEAN } );
if ($self->is_focusable()) {
$self->set_property(basic => 'focused', $focus ? 1 : 0);
[...]
As you can see, we protect users from instantiating this class, and then we implement the various role attributes and methods. This implementation is easy to use and flexible : to know if a widget is focusable, just use :
$widget->isa('Curses::Toolkit::Role::Focusable');
Once you know it's focusable, you can use set_focus, is_focused, etc...
One could also imagine needing to remove the focusability at run time on an existing widget. filtering out Curses::Toolkit::Role::Focusable from its @ISA would do the trick, even if it's not very clean.
So, I know all this is not very modern, and it'll disappear when switching to moose (and Perl6 soon ?), but in the mean time, I just wanted to share this trick. Eh, it's not everyday you can argue positively about multiple inheritance !
PS : I am by no mean a good OO designer, and what I just exposed might be just plain wrong. I'd be more than happy to be enlightened :) If there is a better way to do this in old-school Perl5 way, drop me a note.