In this article we will work on views and create our first view for home tab. We could put all code into one file but it would be a mess. So lets take a different approach, we will create a separate file for each view. Next we will work on button widget and create quit game action. Don’t forget that Piader is a tool to improve LCDManager.
One problem with keeping each view in separate file is lack of communication between them. We could create and use the view manager but it is overkill. Lets keep it simple. Control over views will be in game class. Why do we need control? We need to switch between views, send some basic data (like options to option view) and coordinate all this.
Our views have two states. One when it is active and visible and second when it is inactive and invisible. When it is active loop function will be called.
Each view require initialization – create all widgets, show event – used when view is entering visible state, hide event – used when hiding view and loop – called in each frame, here we store event dispatcher. Show function should also restore view state and set all required information.
We know that we need some functions, lets create an interface.
Create new package abstract. And new file view.py. Content of this file:
#!/usr/bin/python # -*- coding: utf-8 -*- #pylint: disable=I0011,R0913,R0902,R0921 """View interface""" class View(object): """Class for views""" def hide(self): """on hide event""" raise NotImplementedError("hide not implemented") def show(self): """on show event - used when switching to this view. Should reinitialize view""" raise NotImplementedError("show not implemented") def loop(self, action): """main loop, called from view manager""" raise NotImplementedError("loop not implemented")
Create new package views. In this create home.py. In __init__ we will create all widgets. Home screen consist of one tab with 1 label and 3 buttons. Content of home.py:
# -*- coding: utf-8 -*- """ home view """ import abstract.view as view import lcdmanager.widget.pane as pane import lcdmanager.widget.button as button import lcdmanager.widget.label as label class Home(view.View): """Home view""" def __init__(self, lcdmanager): """create all widgets""" self.manager = lcdmanager self.pane = pane.Pane(0, 0, 'home') self.pane.width = lcdmanager.width self.pane.height = lcdmanager.height self.active_button = 0 title = label.Label(self.pane.width / 2 - 5, 0, 'title') title.label = "Piader v2.0" self.pane.add_widget(title) button_start = button.Button(self.pane.width / 2 - 4, 1, 'btn_start') button_start.label = " Start " self.pane.add_widget(button_start) button_options = button.Button(self.pane.width / 2 - 5, 2, 'btn_options') button_options.label = " Options " self.pane.add_widget(button_options) button_quit = button.Button(self.pane.width / 2 - 3, 3, 'btn_quit') button_quit.label = " Quit " self.pane.add_widget(button_quit) self.buttons = [ button_start, button_options, button_quit ] self.manager.add_widget(self.pane) def hide(self): """hide home tab""" self.pane.visibility = False def show(self): """show home tab""" self.pane.visibility = True self.pane.get_widget('btn_start').event_focus() def loop(self, action): """tick""" pass
Lets quickly explain the code. In init we create label with game title and three buttons. We try to center them on display.
In function show we set start state, set tab as visible and first button as active. Each button has it unique name and additionally we keep all buttons in dictionary for quick access.
Go back to game.py into __init__ function and add just before the last line: (don’t forget about import)
self.views['home'] = home_view.Home(self.game_manager)
Next go to main_loop function and add:
"""main loop""" self.event_server.start() self.views['home'].show()
Another step is to add:
between ifs and end calculation.
Finally new tick function:
def tick(self): """render view""" self.game_manager.render() self.game_manager.flush()
We added home view into game class and added display refreshing. When you run main.py you will see something like this:
Not bad 🙂
Lets add event support – moving up and down and switching between buttons. Because we have all buttons in dictionary, switching between them is easy. Changing state goes like this: set current as inactive, increase/decrease pointer and set active button.
New loop function and two helpers:
def loop(self, action): """tick""" if action == 'move.up': self.buttons[self.active_button].event_blur() self._prev_button() self.buttons[self.active_button].event_focus() if action == 'move.down': self.buttons[self.active_button].event_blur() self._next_button() self.buttons[self.active_button].event_focus() def _prev_button(self): """select previous button""" self.active_button -= 1 if self.active_button < 0: self.active_button = len(self.buttons) - 1 def _next_button(self): """select next button""" self.active_button += 1 if self.active_button > len(self.buttons) - 1: self.active_button = 0
We can control active button! (with W and S). Good 🙂
But when we press action (space) nothing happens… lets do something about it.
We need to go back to LCD Manager because we need to add an action callback. But before this we need to create an action interface.
Create new file action.py in abstract/event package:
#!/usr/bin/python # -*- coding: utf-8 -*- """Action event - abstract class""" class Action(object): """event action""" def __init__(self): self._callback = None def event_action(self, widget): """called on action""" raise NotImplementedError("event_action not implemented") @property def callback(self): """return callback""" return self._callback @callback.setter def callback(self, func): """set callback""" self._callback = func
Now we need to make sure Button class implement this, write test:
def test_should_implement_event_action_interface(self): assert_equal(isinstance(self.button, action.Action), True)
We are in nice red state. But when we add another inheritance to Button class and another parent initialization we are back to green. Currently Button inherits three parents:
class Button(widget.Widget, focus.Focus, action.Action):
Lets check if we can attach and call some callback. Write test:
def test_should_attach_and_call_callback(self): self.button.label = " Start game" def _log(widget): return widget.label self.button.callback = _log assert_equal(self.button.callback, _log) assert_equal(self.button.event_action(), self.button.label)
def event_action(self): """call action attached to widget""" return self.callback(self)
See that we are passing calling widget to callback. We may check its label or state in it.
Go back to home.py. Add another if to loop function:
if action == 'action': self.buttons[self.active_button].event_action()
def _button_quit(self, widget): """quit option""" print 'quit' def _button_options(self, widget): """options""" print "options" def _button_start(self, widget): """start game""" print "start game"
And attach them to proper buttons in __init__:
button_start.callback = self._button_start button_options.callback = self._button_options button_quit.callback = self._button_quit
That’s it! When you run a code you may move up/down on menu and call actions.
We have three dummy actions. Quit function is the simplest so we will implement it as first one.
Quit routine should be in Game class, so we need to pass it to home view:
self.views['home'] = home_view.Home(self.game_manager, self)
Modify home view constructor and store it in self.game attribute.
Next change _button_quit to:
def _button_quit(self, widget): """quit option""" self.game.quit_game()
And finally add new function to game.py:
def quit_game(self): """quit game""" self.option['game_on'] = False
Yey! We can now quit the game 🙂
Stop! Jenkins time!
This time we have two projects to check. First LCDManager. Problems only with too long lines. Easy to fix.
Next Piader, this time we had many violations. Mostly too long lines and import of libraries that don’t exist on server. Quick fixes and what is left is not used widget parameter in action function.
We added home view and home actions to Piader. We also added callback support to button widget. And the most important we can quit the game 🙂