LCD Manager – part 3: width and autowidth

Welcome after long break, in this part we will add fixed and automatic width to our label widget.

Download code

Widgets width

We need to know two things, current width and state of autowidth. Our width variable will always return current width. But by setting autowidth to false and setting width we will make it persistent. By default we will go with autowidth set to true.

For now lets use it as attribute of all widgets. So open test_widget.py and extend test correct_init with checking width and autowidth states:


        assert_equal(self.widget.size['autowidth'], True)
        assert_equal(self.widget.size['width'], 0)

Of course now test will fail. We are in red. Open widget.py and update __init__ with:


        self.size = {
            'autowidth': True,
            'width': 0
        }

With this we are back to green. And we will do refactoring because I don’t like idea of accessing by array. We should just call width or autowidth. First change tests to

        assert_equal(self.widget.autowidth, True)
        assert_equal(self.widget.width, 0)

Next add new functions to widget.py:

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

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

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

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

and finally change __init__:

        self._size = {}
        self.width = 0
        self.autowidth = True

All done. Lets move to the next step.

Labels width

Lets think what we want. If we specify autowidth true, then if we set label, width will be set accordingly. If we set autowidth to false, we must set width (default is 0) but to make it more user friendly, when we set width, autowidth will be set to false. Now when we set label, it must be cut to required width.

Question is, will we keep original text ?

Imagine we have width set to 3. We set label as ‘its my label’. It should display ‘its’. Good. But now we extend width to 6, without original input we can’t display ‘its my’.  So many problems with so simple widget 🙂

Modify __init__ change it to:

        self._label = {
            'base': [''],
            'display': ['']
        }

Why we will keep label as array ? Because we will add multiline support 🙂
We need to change one test, because we will be returning array as label, change test_label to:

    def test_label(self):
        self.label.label = "It's label"
        assert_equal(self.label.label, ["It's label"])

To set label correctly we need to change render, setter and getter:

    @property
    def label(self):
        """return label text"""
        return self._label['base']

    @label.setter
    def label(self, label):
        """set label"""
        self._label['base'] = [label]
        self._label['display'] = [label]

    def render(self):
        """return view array"""
        return self._label['display']

Run tests and we are green 🙂 Nice.

We have refactored our code and prepared it to width. Now lets move forward and add fixed width.

Label – fixed width

Lets write a test. When we set width and add shorter text, it should be same. But when we add longer text it should be cropped. Width should also change when setting label,

    def test_set_label_should_change_width(self):
        self.label.label = "It's label"
        assert_equal(self.label.width, 10)

    def test_set_width_set_autowidth_false(self):
        self.label.width = 14
        assert_equal(self.label.autowidth, False)

    def test_label_fixed_width_nocrop(self):
        self.label.width = 14
        self.label.label = "It's a label"
        assert_equal(self.label.render(), ["It's a label"])

    def test_label_fixed_width_crop(self):
        self.label.width = 4
        self.label.label = "It's a label"
        assert_equal(self.label.render(), ["It's"])

Run tests, and they will fail. We are red. Good.

Open widget.py and add in width setter:

        self._size['autowidth'] = False

One test fixed. Go back to label.py and change label setter to:

    @label.setter
    def label(self, label):
        """set label"""
        self._label['base'] = [label]
        if self.autowidth:
            self._label['display'] = [label]
            self.width = len(label)
        else:
            self._label['display'] = [label[0:self.width]]

We are setting width when required and we are cropping text when required. Simple and after this change tests are green.

Now we need one more functionality. When we set long label, set width and change width it should update itself correctly. So first test:

    def test_change_width(self):
        self.label.width = 4
        self.label.label = "It's a label"
        self.label.width = 12
        assert_equal(self.label.render(), ["It's a label"])

Red state and new code for width setter:

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

and still red 🙂 Change __init__ move parent initialization to the bottom of the function.
And now we are green.

But what will happen when we have a label with width set to 4 and we set autowidth ?
No idea ? Write test:

    def test_change_width(self):
        self.label.width = 4
        self.label.label = "It's a label"
        self.label.autowidth = True
        assert_equal(self.label.render(), ["It's a label"])
        assert_equal(self.label.width, 12)

Now we know. It won’t work 🙂 But two changes and it will. First new autowidth function:

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

And second, change __init__ in widget.py, initialize dictionary by setting it and not by property:

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

Demo

Lets add another demo function to label demo file:

def demo2():
    """width demo"""
    lcd = buffered.CharLCD(16, 2, I2C(0x20, 1))
    lcd.init()
    lcd_manager = manager.Manager(lcd)
    label1 = Label(0, 0)
    label1.width = 3
    label1.label = "Hello !!!111"
    lcd_manager.add_widget(label1)
    lcd_manager.render()
    lcd_manager.flush()

    time.sleep(2)
    label1.width = 6
    lcd_manager.render()
    lcd_manager.flush()

    time.sleep(2)
    label1.autowidth = True
    lcd_manager.render()
    lcd_manager.flush()

Its showing us that width really works.

Stop! Jenkins time !

Ahh is this time already. This time proof that pylint has problems with @property. Its reporting violations because it looks at def width and see four functions with different number of arguments.

It didn’t change after upgrading to newest pyLint. So lets ignore it.
But something else changed. Seems warning for not abstracting class was removed (yey). It appeared when abstract class was not extended in same file.

Summary

We added widget width and autowidth property.

Download code

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