LCD Manager – part 5: widget pane

Next widget we will add to our manager is pane (wrongly named panel before). It will allow us to create group of other widgets (label for now:)).

In this part we will focus on simple and quick implementation. In next part we will improve it.

After this we will rewrite Piader game to use manager and widgets. Somewhere between we will add event queue and probably refactor whole structure.

Download source

Pane widget

Basics

Lets quickly review what we want from pane. First we need to be able to add and remove widgets to/from pane. Next while rendering it should create combined view. Finally it should return view according to its position and visibility.

Pane has a nice functionality. Imagine stacking few panes on each other and switching them via visibility attribute. We simulate tabs!

This functionality will come in handy while rewriting Piader. One tab for welcome screen, second for options, third for game, fourth pause and fifth statistics. Its just a base concept, as always we will see what comes out 🙂

But return to now and start with bare pane widget.

So we need standard functions plus dict for objects (we will use same mechanic like in manager). Create pane.py in widget folder and start with bare minimum:


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

"""Widget - pane"""

import lcdmanager.abstract.widget as widget

class Pane(widget.Widget):
    """Label widget"""
    def __init__(self, pos_x, pos_y, name=None, visibility=True):
        widget.Widget.__init__(self, pos_x, pos_y, name, visibility)

    def render(self):
        """return view array"""
        return []

Now create file test_pane.py in test directory and add first test:

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

"""Tests for pane widget"""

from nose.tools import assert_equal
import lcdmanager.widget.pane as pane

class TestWidget(object):
    def setUp(self):
        self.pane = pane.Pane(1, 1, 'Tab1')

    def test_correct_init(self):
        assert_equal(self.pane.visibility, True)
        assert_equal(self.pane.position, {'x': 1, 'y': 1})
        assert_equal(self.pane.autowidth, True)
        assert_equal(self.pane.width, 0)

Basic initialization works. Next we will create add_widget. Test:

def test_add_widget(self):
    widget_label = label.Label(1, 1)
    widget_label.label = "name"
    self.pane.add_widget(widget_label)
    assert_equal(self.pane.widgets, [widget_label])

Of course tests will fail. Two quick additions and we will be back to green. First add:

self.widgets = []

to pane __init__ and next add:

def add_widget(self, widget):
    """add widget to pane"""
    self.widgets.append(widget)

That was quick. If we can add, we should be able to get widget. Test:

def test_should_get_widget_by_name(self):
    widget_label = label.Label(1, 1, 'text1')
    widget_label.label = "name"
    self.pane.add_widget(widget_label)
    assert_equal(self.pane.get_widget('text1'), widget_label)

And proper code:

def get_widget(self, name):
    """get widget from pane or None"""
    for widget in self.widgets:
        if widget.name == name:
            return widget

    return None

Rendering

Rendering needs to do something similar to manager, get all widgets from dictionary and render them. For now we are ignoring width, height and transparency. But don’t worry, their time will come.

First problem, imagine we have two labels and their Y is 1 and 2. This require that row 0 is empty. Remember that we are returning view array. So we must detect empty rows and extend out view accordingly.

Next we may start with X bigger then 0, so we need to add empty spaces before string. But doing this will destroy transparent background.
Why?
Imagine we have Label on (1, 0)  with label name. So out view will return ‘ name’. First space will overwrite any char in this place. Later we will do something about it.

But lets start with simple test:

def test_it_should_render_widget_from_top(self):
    widget_label = label.Label(1, 0)
    widget_label.label = "name"
    self.pane.add_widget(widget_label)
    output = [
        " name"
    ]
    assert_equal(self.pane.render(), output)

And first implementation of render function:

def render(self):
    """return view array"""
    output = []
    for widget in self.widgets:
        view = widget.render()
        offset_y = 0
        for line in view:
            if offset_y >= len(output):
                for _ in range(0, offset_y - len(output) + 1):
                    output.append([])

            output[offset_y] = line.rjust(widget.position['x'] + len(line), " ")
            offset_y += 1

    return output

Another test to check if render works with two lines :

def test_it_should_render_widget_two_labels(self):
    widget_label = label.Label(1, 0)
    widget_label_1 = label.Label(0, 2)
    widget_label.label = "name"
    widget_label_1.label = "eman"
    self.pane.add_widget(widget_label)
    self.pane.add_widget(widget_label_1)
    output = [
        " name",
        "",
        "eman"
    ]
    assert_equal(self.pane.render(), output)

And we are red. But with new render we are back to green:

def render(self):
    """return view array"""
    output = []
    for widget in self.widgets:
        view = widget.render()
        offset_y = widget.position['y']
        for line in view:
            if offset_y >= len(output):
                for _ in range(0, offset_y - len(output) + 1):
                output.append('')

            output[offset_y] = line.rjust(widget.position['x'] + len(line), " ")
            offset_y += 1

    return output

We properly set offset_y and when skipping row we use ” and not [].

Demo

We know that something works, now lets see how it works. Create file pane.py in demos folder.

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

"""LCD Manager - pane demo"""

import sys
sys.path.append("../../")
import RPi.GPIO as GPIO #pylint: disable=I0011,F0401
from charlcd import buffered as buffered
from charlcd.drivers.i2c import I2C
import charlcd.drivers.gpio as gp
from lcdmanager import manager
from lcdmanager.widget.label import Label
from lcdmanager.widget.pane import Pane
import time

GPIO.setmode(GPIO.BCM)


def demo1():
    """basic demo"""
    lcd = buffered.CharLCD(20, 4, gp.Gpio())
    lcd.init()
    lcd_manager = manager.Manager(lcd)
    label1 = Label(0, 0)
    label1.label = "1 - Hello !"
    label2 = Label(1, 2)
    label2.label = "3 - Meow !"
    label3 = Label(1, 1)
    label3.label = "2 - Woof !"

    pane1 = Pane(1, 0)
    pane1.add_widget(label1)
    pane1.add_widget(label2)
    pane1.add_widget(label3)
    lcd_manager.add_widget(pane1)
    lcd_manager.render()
    lcd_manager.flush()

demo1()

Run and… it works as it should 🙂 Nice.

Stop! Jenkins time!

Time for middle stop, checking code and fixing code. I used widget as variable and this was detected as redefining class widget. So I had to change name.

Summary

This time we have:

  • add pane widget in its base form
  • add tests 🙂
  • add simple demo

This ends part one with pane widget.

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