LCD Manager – part 7: button widget and events

Time for something new, imagine that we rewrite Piader to use LCD Manager. We want simple start screen, game screen and maybe options. We can use pane widget to simulate tabs. But how to change between them ? On main screen how to create actions like start game or quit ?

This will require buttons 🙂 But using buttons imply using events and this goes well with event server. And this is something interesting.
Imagine out start screen. With 3 buttons, first to start game, second with options and third to quit. First problem how to change between them and call action? And before that, what to use as input ?

We need to write an event server, a thread that will wait for events and propagate them to manager and widgets.
But easy things first. We will create widget button and attach action to it.

Button – planing

Now lets skip event server and all this stuff and focus on widget. Button require some label and few events: focus, blur and action.
We need focus and blur to show current active button. Active button we will mark with some char. Calling action should call any function attached to it.
We need to have two interfaces, Focus with two functions and Action with one function. Why interfaces ? Because they will help us implement required function in our classes.

Lets get to work.

Because button is extension of label, create button.py in widget folder and copy content from label.py. Change class name. Repeat this process for test file. Don’t forget to check if tests are passing.

Events interface

Before we move further we need to create an interface with required function. Every object which implements it will be able to gain and lose focus. So it will become selectable.

So lets start from tests, add line to the end of test_should_initialize_correctly test:

assert_equal(self.button.has_focus, False)

Add this test (and don’t forget about import):

    def test_should_implement_event_focus_interface(self):
        assert_equal(isinstance(self.button, focus.Focus), True)

Tests ready. Create event package in abstract one. Then create focus.py and copy:

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""Focus event - abstract class"""


class Focus(object):
    """event focus/blur"""
    def __init__(self):
        self.has_focus = False

    def event_focus(self):
        """called on focus gain"""
        raise NotImplementedError("event_focus not implemented")

    def event_blur(self):
        """called on focus lost"""
        raise NotImplementedError("event_blur not implemented")

Now we need to add this to button widget, change class button definition to (don’t forget to add import):

class Button(widget.Widget, focus.Focus):

Button events

We can use focus event but why we need it? It’s simple when button has focus we will display additional char at first and/or last position. So unfocused state is like that: ‘click me’ and focused: ‘>click me’.
Question is how to append those chars ? Overwrite label chars? Or append and extend width ?
We will control this so we need another variable.

We need to make sure events are working, tests:

    def test_focus_should_change_variable(self):
        self.button.event_focus()
        assert_equal(self.button.has_focus, True)

And return to green by adding those functions (to Button class):

    def event_focus(self):
        """gain focus"""
        self.has_focus = True

    def event_blur(self):
        """lost focus"""
        self.has_focus = False

Declare consts:

APPEND_MODE_OVERWRITE = 1
APPEND_MODE_APPEND = 2

Overwrite declaration in __init__ with:

        self._label = {
            'base': [''],
            'display': [''],
            'pointer': {
                'before': '>',
                'after': '<',
                'append_mode': APPEND_MODE_OVERWRITE
            }
        }

What we are missing are setters and getters for pointer parameters. Lets add them, tests:

    def test_pointer_setters_getters_should_work(self):
        self.button.pointer_before = "+"
        self.button.pointer_after = "-"
        self.button.pointer_mode = button.APPEND_MODE_APPEND
        assert_equal(self.button.pointer_before, "+")
        assert_equal(self.button.pointer_after, "-")
        assert_equal(self.button.pointer_mode, button.APPEND_MODE_APPEND)

and setters:

    @property
    def pointer_before(self):
        """return char for pointer before label"""
        return self._label['pointer']['before']

    @pointer_before.setter
    def pointer_before(self, char):
        """set char for pointer before label"""
        self._label['pointer']['before'] = char

    @property
    def pointer_after(self):
        """return char for pointer after label"""
        return self._label['pointer']['after']

    @pointer_after.setter
    def pointer_after(self, char):
        """set char for pointer after label"""
        self._label['pointer']['after'] = char

    @property
    def pointer_mode(self):
        """return char for pointer after label"""
        return self._label['pointer']['append_mode']

    @pointer_mode.setter
    def pointer_mode(self, mode):
        """set char for pointer after label"""
        self._label['pointer']['append_mode'] = mode

Rendering require some refactoring. Time for tests.
Default rendering, with two pointer chars and overwriting:

    def test_render_focused_default_options(self):
        self.button.label = " Start game "
        self.button.event_focus()
        assert_equal(self.button.render(), ['>Start game<'])

Fixing red status require refactoring in _crop_to_display function. New one looks like this:

    def _crop_to_display(self):
        """prepare text to display by cropping it and append pointers"""
        rows = [label[0:self.width] for label in self._label['base']]
        if self.has_focus and self._label['pointer']['append_mode'] == APPEND_MODE_OVERWRITE:
           rows = [self._label['pointer']['before'] +
                label[
                    len(self._label['pointer']['before']): ((len(self._label['pointer']['after']) * -1) or len(label))
                ] +
                self._label['pointer']['after']
                for label in rows]
        return rows[0: self.height]

Lets add another test:

    def test_render_focused_left_marker_options(self):
        self.button.pointer_after = ''
        self.button.label = " Start game"
        self.button.event_focus()
        assert_equal(self.button.render(), ['>Start game'])

And we are still green. Hurrah! Overwrite mode works. But what about append mode? After some thoughts I will drop this idea:) Maybe later, who knows.
What we need now? Yes that’s right! We need to call some action 🙂 We need to add some function to be called from button. For now lets skip how button widget receive events and focus on calling function.
This new function is called action. Because for know we have no idea how it will work, just do an empty function:

    def action(self):
        """call action attached to widget"""
        pass

Summary

That’s right, summary without Jenkins time, lately my live changed drastically. This article is finished after long break. It was ready almost two months ago. I’m still struggling with few things but hopefully I will restart my little project.

This time we have:

  • added new widget, button
  • added focus interface
  • prepared code for some action 🙂

Download

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s