LCD Manager – part 4: height and multiline label

We have some control over width so next step should be with height. We will add multiline support for label widget, add height and autoheight support.

Download source

Widgets height

Similar to width, height will always reflect widget height and setting it will change autoheight to false. Autoheight is true as default.

So lets update our correct_init test in test_widget.py:

        assert_equal(self.widget.autoheight, True)
        assert_equal(self.widget.height, 0)

Once again we are in red state.
First add fields to _size dictionary in __init__:

        self._size = {
            'width': 0,
            'autowidth': True,
            'height': 0,
            'autoheight': True
        }

And next add @property for heigh and autoheight:

    @property
    def height(self):
        """get width"""
        return self._size['height']

    @height.setter
    def height(self, height):
        """set width"""
        self._size['height'] = height
        self._size['autoheight'] = False

    @property
    def autoheight(self):
        """get autowidth"""
        return self._size['autoheight']

    @autoheight.setter
    def autoheight(self, enabled):
        """set autowidth"""
        self._size['autoheight'] = enabled

Now when we run tests we see that we are in green state, all tests are passing.

Label height

Height is depending on number of lines we pass to widget. Simple. But for now our label only accept one line. We will change it and allow list to be assigned to label. But if we can send few lines, we need to calculate which one is longest, so we can set width. And if required we need to crop all lines! Ok.
Lets write test:

    def test_assign_multiline(self):
        self.label.label = ['line one', 'second line']
        assert_equal(self.label.render(), ['line one', 'second line'])

Test will fail. We need to check is value assignment to label is list or string and act accordingly. We also need to add width multiline calculation.
Next we need to change render function because for now it has hardcoded first row crop.
New label setter:

    @label.setter
    def label(self, label):
        """set label"""
        if isinstance(label, basestring):
            self._label['base'] = [label]
        else:
            self._label['base'] = label

        if self.autowidth:
            self.width = self._calculate_width()
        else:
            self._label['display'] = self._crop_to_display()

Refactored width setter:

    @width.setter
    def width(self, width):
        """set width"""
        widget.Widget.width.fset(self, width)
        self._label['display'] = self._crop_to_display()

And two private functions:

    def _calculate_width(self):
        """calculate longest row in dictionary"""
        return max(len(label) for label in self._label['base'])

    def _crop_to_display(self):
        """prepare text to display by cropping it"""
        return [label[0:self.width] for label in self._label['base']]

One step done. Lets take a next one.
We need to test if height is changing, add this to previous test.

 
        assert_equal(self.label.height, 2)

Now we know that is not changing. We are not using height while cropping. Another big change is coming. We will first calculate width and height and then crop.

    @label.setter
    def label(self, label):
        """set label"""
        if isinstance(label, basestring):
            self._label['base'] = [label]
        else:
            self._label['base'] = label
        if self.autowidth:
            self.width = self._calculate_width()
        if self.autoheight:
            self.height = len(self._label['base'])
        self._label['display'] = self._crop_to_display()
    def _crop_to_display(self):
        """prepare text to display by cropping it"""
        rows = [label[0:self.width] for label in self._label['base']]
        return rows[0: self.height]

Yey, all tests are green ! So lets add another test, we will check if changing height works:

    def test_change_height(self):
        self.label.label = ['line one', 'second line', '3rd line']
        self.label.height = 2
        assert_equal(self.label.render(), ['line one', 'second line'])
        assert_equal(self.label.height, 2)

Now we are sure that it doesn’t work:) But its simple to fix:

    @property
    def height(self):
        """get width"""
        return widget.Widget.height.fget(self)

    @height.setter
    def height(self, height):
        """set width"""
        widget.Widget.height.fset(self, height)
        self._label['display'] = self._crop_to_display()

And we are green !
Now we know that our label work as it should. But are we sure lcdmanager will display content ? No idea ? So lets write a test in test_manger.py:

    def test_it_should_render_multiline_label(self):
        widget_label = label.Label(1, 1)
        widget_label.label = ["name", "surname"]
        self.lcd_manager.add_widget(widget_label)
        self.lcd_manager.render()
        buffer = self.lcd_buffer
        buffer[1] = " name               "
        buffer[2] = " surname            "
        assert_equal(self.lcd_manager.lcd.buffer, buffer)

Another fail 🙂 Lets do something about it. Open manger.py and look for render function, we have very bad line there:

widget.render()[0]

We are sending only first line. Long time ago it was good but now it is something to refactor. I will show you one trap… But first new render function:

    def render(self):
        """add widget view to lcd buffer"""
        for widget in self.widgets:
            position = widget.position
            y_offset = 0
            for line in widget.render():
                self.lcd.write(
                    line,
                    position['x'],
                    position['y'] + y_offset
                )
                y_offset += 1

Now when you run tests they will pass. But when you run demo:

def demo3():
    """multi line demo"""
    lcd = buffered.CharLCD(16, 2, I2C(0x20, 1))
    lcd.init()
    lcd_manager = manager.Manager(lcd)
    label1 = Label(1, 0)
    label1.label = ['one', 'two', 'three']
    lcd_manager.add_widget(label1)
    lcd_manager.render()
    lcd_manager.flush()

It will crash ! Why ? Because we are trying to display more rows than out lcd can. Three lines and only two available.
This is a trap, we couldn’t foresee this problem. Our tests are passing and we may thing everything is as it should be. So remember think and write 🙂 Tests are not an answer for everything.
The core of this problem is Null driver used in tests. It has only functions without any mechanics.

We may fix this problem quickly by adding:

if position['y'] + y_offset >= self.lcd.get_height():
    break

after

for line in widget.render():

Question is, will this really work later ? Answer is: who knows 🙂 I suspect we will have a problem later because our widgets have no information about size. Maybe we will add dimensions as parameter to render ? But thats for later 🙂

Stop! Jenkins time!

After check it reported code duplication of @property. I looked at google and found solution!
In label.py we have properties with setters, for example height:

    @property
    def height(self):
        """get width"""
        return widget.Widget.height.fget(self)

    @height.setter
    def height(self, height):
        """set width"""
        widget.Widget.height.fset(self, height)
        self._label['display'] = self._crop_to_display()

See that @property is duplicated from widget.py. When we remove @property, setter will fail. But when we change to:

@widget.Widget.height.setter

It will work. So lets refactor all such cases. Now code is much cleaner. This is a nice example that tests are very useful. We done quite a big change in code and with tests passing we are sure we didn’t broke anything.
Only problem left is wrong parameters for inherited functions width, autowidth and height. But this is problem with pyLint.

Label autoheight

When refactoring code I discovered what we forgot! We didn’t implement setter for autoheight! So lets begin with test:

    def test_setting_autoheight(self):
        self.label.label = ['line one', 'second line', '3rd line']
        self.label.height = 2
        self.label.autoheight = True
        assert_equal(self.label.render(), ['line one', 'second line', '3rd line'])
        assert_equal(self.label.height, 3)

And write new setter for autoheight:

    @widget.Widget.autoheight.setter
    def autoheight(self, enabled):
        """set autowidth"""
        widget.Widget.autoheight.fset(self, enabled)
        if enabled:
            self.label = self._label['base']

This time job is done.

Summary

This time we have:

  • add multiline for label
  • add height and autoheight for widgets
  • fixed manager rendering
  • learned that we don’t have to duplicate @property definition

Download source

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