Basic framework for creating user interface
http://wiki.forum.nokia.com/index.php/Basic_framework_for_creating_user_interface
http://wiki.forum.nokia.com/index.php/Basic_framework_for_creating_user_interface
http://wiki.forum.nokia.com/index.php/Basic_framework_for_creating_user_interface
From Forum Nokia Wiki
ID | Creation date | January 27, 2009 | |
Platform | S60 3rd Edition | Tested on devices | Nokia E71, Nokia N95 |
Category | Python | Subcategory | UI |
Keywords (APIs, classes, methods, functions): appuifw |
Introduction
The API for writing applications using Python for S60 is very straightforward. Once you have decided what kind of body your application will use (canvas, listbox, or text), you need to define your main menu, application title, and default callback handler for exiting. The user interface (UI) is based on events, and there is a special mechanism that relies on a semaphore for indicating if the application is closing. The user must obtain the semaphore and wait for any signal on it. Once signaled, the application may close properly. The UI allows tabs as well, but in this case a body must be defined for each tab. A routine for handling tabs changing is required in such a situation.
However, if your application has more than one window, meaning more than one set of body, menu, and exit handler, you will need to change these elements each time a new window is displayed, giving the user the impression that s/he is using a multiple-window application. Although this solution is possible, some problems arise:
- The strategy for exiting, based on semaphores, must be unique across your application.
- You cannot make any mistakes when switching the UI, that is, the set body+menu+exit handler must be consistently changed.
- There must be a unified strategy for blocking the UI when a time-consuming operation is pending. For instance, when downloading a file, you may want to disable all menu options, otherwise they will be available for the user during the download operation.
For unifying this process, three additional classes are suggested in this article. The first, called Window, is responsible for holding UI contents such as menu, body, and exit handler, and properly exchanging all UI elements for the derived classes. Moreover, it may lock/unlock the UI when necessary. The second class is named Application. It represents the running application itself and is responsible for handling the termination semaphore. Only one Application class must be instantiated per application. The third class is called Dialog. As its name suggests, it is in charge of showing/hiding dialogues when necessary. Many dialogues are allowed, each with its own set of body+menu+exit handler.
Application and Dialog inherit from Window the content handler ability, while each has different ways of finishing itself (finishing the application or just the dialogue).
Code examples
All behaviours described in the previous section are implemented for the script window.py, given below, where Window, Application, and Dialog are depicted.
# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
from appuifw import *
import e32
import key_codes
__all__ = [ "Application", "Dialog" ]
class Window(object):
""" This class is responsible for holding UI contents like menu, body
and exit handler and exchanging properly all UI elements for
the derived classes. Moreover, it may lock/unlock the UI when necessary.
"""
__ui_lock = False
def __init__(self, title, body, global_menu = None, exit_handler = None):
""" Creates a new window given a title, body or a set of bodies (for tabbed
window) and optional global_menu and exit handler. Global menu is
available for all tabs at bottom or it is used as the default
menu for non tabbed window
The list of bodies must have the following format:
[(tab_text, body, menu),(tab_text, body, menu),...]
where:
tab_text: unicode string used in tab
body: a valid body (Listbox, Text or Canvas)
menu: typical menu
"""
self.title = title
if isinstance(body,list):
self.tabbed = True
self.bodies = body
self.body = None
else:
self.tabbed = False
self.bodies = None
self.body = body
if global_menu is None:
global_menu = [(LABELS.loc.wi_info_exit, self.close_app)]
if exit_handler is None:
exit_handler = self.close_app
self.global_menu = global_menu
self.exit_handler = exit_handler
self.last_tab = 0
def set_title(self,title):
" Sets the current application title "
app.title = self.title = title
def get_title(self):
" Returns the current application title "
return self.title
def bind(self, key, cbk):
" Bind a key to the body. A callback must be provided."
self.body.bind(key,cbk)
def refresh(self):
" Update the application itens (menu, body and exit handler) "
if self.tabbed:
app.set_tabs([b[0] for b in self.bodies],self.tab_handler)
app.activate_tab(self.last_tab)
self.tab_handler(self.last_tab)
else:
app.set_tabs([], None)
app.menu = self.global_menu
app.body = self.body
app.title = self.title
app.exit_key_handler = self.exit_handler
def tab_handler(self,idx):
" Update tab and its related contents "
self.last_tab = idx
self.body = self.bodies[idx][1]
self.menu = self.bodies[idx][2] + self.global_menu
app.title = self.title
app.menu = self.menu
app.body = self.body
def run(self):
" Show the dialog/application "
self.refresh()
def lock_ui(self,title = u""):
""" Lock UI (menu, body and exit handler are disabled).
You may set a string to be shown in the title area.
"""
Window.__ui_lock = True
app.menu = []
app.set_tabs([], None)
app.exit_key_handler = lambda: None
if title:
app.title = title
def unlock_ui(self):
"Unlock UI. Call refresh() for restoring menu, body and exit handler."
Window.__ui_lock = False
def ui_is_locked(self):
"Chech if UI is locked or not, return True or False"
return Window.__ui_lock
class Application(Window):
""" This class represents the running application itself
and it is responsible for handling the termination semaphore.
Only one Application class must be instantiated per application.
"""
__highlander = None
__lock = None
def __init__(self, title, body, menu = None, exit_handler = None):
""" Only one application is allowed. It is responsible for starting
and finishing the program.
run() is override for controlling this behavior.
"""
if Application.__highlander:
raise "Only one Application() allowed"
Application.__highlander = self
if not Application.__lock:
Application.__lock = e32.Ao_lock()
Window.__init__(self, title, body, menu, exit_handler)
def close_app(self):
""" Signalize the application lock, allowing run() to terminate the application.
"""
Application.__lock.signal()
def run(self):
""" Show the the application and wait until application lock is
signalized. After that, make all necessary cleanup.
"""
old_title = app.title
self.refresh()
Application.__lock.wait()
# restore everything!
app.set_tabs( [], None )
app.title = old_title
app.menu = []
app.body = None
app.set_exit()
class Dialog(Window):
""" This class is in the charge of showing/hiding dialogs when necessary.
Many dialogs are allowed, each one with their own set of body+menu+exit
handler.
"""
def __init__(self, cbk, title, body, menu = None, exit_handler = None):
""" Create a dialog. cbk is called when dialog is closing.
Dialog contents, like title and body need
to be specified. If menu or exit_handler are not specified,
defautl values for dialog class are used.
"""
self.cbk = cbk
self.cancel = False
Window.__init__(self, title, body, menu, exit_handler)
def close_app(self):
""" When closing the dialog, a call do self.cbk() is done.
If this function returns True the dialog is not refreshed
and the latest dialog/window takes control. If something fails
during callback execution, callback function should return False
and does not call refresh(). Using self.cancel it is possible
to determine when the dialog was canceled or not.
"""
if self.cbk() == False:
self.refresh()
def cancel_app(self):
""" Close the dialog but turn the cancel flag on.
"""
self.cancel = True
self.close_app()
Usage examples
Using this framework, user attention is focused on the application and not on appuifw issues. Consider this first example, with only one window (no dialogues). Since your program inherits from Application, default actions for some specific appuifw are already defined:
- The termination semaphore is controlled by the Application class, being properly initialized inside its constructor (Application.__lock = e32.Ao_lock()). Only one Application object per program is allowed.
- exit_default_handler is set to Application.close_app(), where the termination semaphore is set (Application.__lock.signal()).
- Application.run() initializes the application, calling refresh() for updating the UI and waits for the termination semaphore. When signalized, all necessary cleanup is done and the application is closed.
We can see one simple example in action:
# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
from window import Application
from appuifw import Listbox, note
class MyApp(Application):
def __init__(self):
items = [ u"Option A",
u"Option B",
u"Option C" ]
menu = [ (u"Menu A", self.option_a),
(u"Menu B", self.option_b),
(u"Menu C", self.option_c) ]
body = Listbox(items, self.check_items )
Application.__init__(self,
u"MyApp title",
body,
menu)
def check_items(self):
idx = self.body.current()
( self.option_a, self.option_b, self.option_c )[idx]()
def option_a(self): note(u"A","info")
def option_b(self): note(u"B","info")
def option_c(self): note(u"C","info")
if __name__ == "__main__":
app = MyApp()
app.run()
First example in action:
Of course, if you need special actions, such as a confirmation when exiting, this can be implemented simply by overriding close_app():
# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
from window import Application
from appuifw import Listbox, note, popup_menu
class MyApp(Application):
def __init__(self):
items = [ u"Option A",
u"Option B",
u"Option C" ]
menu = [ (u"Menu A", self.option_a),
(u"Menu B", self.option_b),
(u"Menu C", self.option_c) ]
body = Listbox(items, self.check_items )
Application.__init__(self,
u"MyApp title",
body,
menu)
def check_items(self):
idx = self.body.current()
( self.option_a, self.option_b, self.option_c )[idx]()
def option_a(self): note(u"A","info")
def option_b(self): note(u"B","info")
def option_c(self): note(u"C","info")
def close_app(self):
ny = popup_menu( [u"No", u"Yes"], u"Exit?")
if ny is not None:
if ny == 1:
Application.close_app(self)
if __name__ == "__main__":
app = MyApp()
app.run()
Second example, with customized exit function:
Dialogues
Suppose that the user wants to add a dialogue call. Here is a simple solution that also hides all appuifw issues.
# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
from window import Application, Dialog
from appuifw import Listbox, note, popup_menu, Text
class Notepad(Dialog):
def __init__(self, cbk, txt=u""):
menu = [(u"Save", self.close_app),
(u"Discard", self.cancel_app)]
Dialog.__init__(self, cbk, u"MyDlg title", Text(txt), menu)
class MyApp(Application):
def __init__(self):
self.txt = u""
items = [ u"Text editor",
u"Option B",
u"Option C" ]
menu = [ (u"Text editor", self.text_editor),
(u"Menu B", self.option_b),
(u"Menu C", self.option_c) ]
body = Listbox(items, self.check_items )
Application.__init__(self,
u"MyApp title",
body,
menu)
def check_items(self):
idx = self.body.current()
( self.text_editor, self.option_b, self.option_c )[idx]()
def text_editor(self):
def cbk():
if not self.dlg.cancel:
self.txt = self.dlg.body.get()
note(self.txt, "info")
self.refresh()
return True
self.dlg = Notepad(cbk, self.txt)
self.dlg.run()
def option_b(self): note(u"B","info")
def option_c(self): note(u"C","info")
def close_app(self):
ny = popup_menu( [u"No", u"Yes"], u"Exit?")
if ny is not None:
if ny == 1:
Application.close_app(self)
if __name__ == "__main__":
app = MyApp()
app.run()
Notepad added to menu:
Notepad in action:
Notepad data:
When a dialogue is created, a callback function needs to be defined. This callback is called when the user cancels or closes the dialogue. Inside the callback body, it is possible to check if the dialogue was canceled just by verifying the cancel variable. Dialogue variables may be accessed in a similar way as in any other Python object and this is the way of retrieving dialogue data.
The callback function must return either True or False, before finishing. If it returns True, self.refresh() must be called before, inside callback body. This way, the menu, body and exit handler will be updated using the context of the dialogue caller (MyApp, in this case). If it returns False, self.refresh() is called inside dialogue context and the dialogue is restored. This is an excellent way to check dialogue data and to avoid data loss. A better example with this feature is given below (see the number_sel() method).
# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
from window import Application, Dialog
from appuifw import Listbox, note, popup_menu, Text
class Notepad(Dialog):
def __init__(self, cbk, txt=u""):
menu = [(u"Save", self.close_app),
(u"Discard", self.cancel_app)]
Dialog.__init__(self, cbk, u"MyDlg title", Text(txt), menu)
class NumSel(Dialog):
def __init__(self, cbk):
self.items = [ u"1", u"2", u"a", u"b" ]
Dialog.__init__(self, cbk,
u"Select a number",
Listbox(self.items, self.close_app))
class MyApp(Application):
def __init__(self):
self.txt = u""
items = [ u"Text editor",
u"Number selection",
u"Option C" ]
menu = [ (u"Text editor", self.text_editor),
(u"Number selection", self.number_sel),
(u"Menu C", self.option_c) ]
body = Listbox(items, self.check_items )
Application.__init__(self,
u"MyApp title",
body,
menu)
def check_items(self):
idx = self.body.current()
( self.text_editor, self.number_sel, self.option_c )[idx]()
def text_editor(self):
def cbk():
if not self.dlg.cancel:
self.txt = self.dlg.body.get()
note(self.txt, "info")
self.refresh()
return True
self.dlg = Notepad(cbk, self.txt)
self.dlg.run()
def number_sel(self):
def cbk():
if not self.dlg.cancel:
val = self.dlg.items[self.dlg.body.current()]
try:
n = int(val)
except:
note(u"Invalid number. Try again.", "info")
return False
note(u"Valid number", "info")
self.refresh()
return True
self.dlg = NumSel(cbk)
self.dlg.run()
def option_c(self): note(u"C","info")
def close_app(self):
ny = popup_menu( [u"No", u"Yes"], u"Exit?")
if ny is not None:
if ny == 1:
Application.close_app(self)
if __name__ == "__main__":
app = MyApp()
app.run()
Number selection added to menu:
Number selection in action:
Handling invalid entries (return False and do not call refresh() inside dialogue caller context):
Valid entry accepted (return True and call refresh() inside dialogue caller context):
Finally, what about changing the menu and body dynamically? This can be accomplished simply by overriding refresh(). refresh() is responsible for all UI updates, assigning desired values for menu, body, and exit_handler. If you made changes to a UI element, it is necessary to redraw it. In the next example, the menu and body contents of the NameList() dialogue are changed dynamically.
# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
from window import Application, Dialog
from appuifw import Listbox, note, popup_menu, Text, query, app
class Notepad(Dialog):
def __init__(self, cbk, txt=u""):
menu = [(u"Save", self.close_app),
(u"Discard", self.cancel_app)]
Dialog.__init__(self, cbk, u"MyDlg title", Text(txt), menu)
class NumSel(Dialog):
def __init__(self, cbk):
self.items = [ u"1", u"2", u"a", u"b" ]
Dialog.__init__(self, cbk,
u"Select a number",
Listbox(self.items, self.close_app))
class NameList(Dialog):
def __init__(self, cbk, names=[]):
self.names = names
self.body = Listbox([(u"")],self.options)
# Do not populate Listbox()! Refresh will do this.
Dialog.__init__(self, cbk, u"Name list", self.body)
def options(self):
op = popup_menu( [u"Insert", u"Del"] , u"Names:")
if op is not None:
if op == 0:
name = query(u"New name:", "text", u"" )
if name is not None:
self.names.append(name)
print self.names
elif self.names:
del self.names[self.body.current()]
# Menu and body are changing!
# You need to refresh the interface.
self.refresh()
def refresh(self):
menu = []
if self.names:
menu = map(lambda x: (x, lambda: None), self.names)
items = self.names
else:
items = [u"<empty>"]
self.menu = menu + [(u"Exit", self.close_app)]
self.body.set_list(items,0)
# Since your self.menu and self.body have already defined their
# new values, call base class refresh()
# PSZY:Another way to refresh the body,in this way ,you can even change the body to another type
#appuifw.app.body = None
#del self.body
#self.body = appuifw.Listbox(self.items,self.options)
#Dialog.refresh(self)
Dialog.refresh(self)
class MyApp(Application):
def __init__(self):
self.txt = u""
self.names = []
items = [ u"Text editor",
u"Number selection",
u"Name list" ]
menu = [ (u"Text editor", self.text_editor),
(u"Number selection", self.number_sel),
(u"Name list", self.name_list) ]
body = Listbox(items, self.check_items )
Application.__init__(self,
u"MyApp title",
body,
menu)
def check_items(self):
idx = self.body.current()
( self.text_editor, self.number_sel, self.name_list )[idx]()
def text_editor(self):
def cbk():
if not self.dlg.cancel:
self.txt = self.dlg.body.get()
note(self.txt, "info")
self.refresh()
return True
self.dlg = Notepad(cbk, self.txt)
self.dlg.run()
def number_sel(self):
def cbk():
if not self.dlg.cancel:
val = self.dlg.items[self.dlg.body.current()]
try:
n = int(val)
except:
note(u"Invalid number", "info")
return False
note(u"Valid number", "info")
self.refresh()
return True
self.dlg = NumSel(cbk)
self.dlg.run()
def name_list(self):
def cbk():
if not self.dlg.cancel:
self.names = self.dlg.names
self.refresh()
return True
self.dlg = NameList(cbk, self.names)
self.dlg.run()
def close_app(self):
ny = popup_menu( [u"No", u"Yes"], u"Exit?")
if ny is not None:
if ny == 1:
Application.close_app(self)
if __name__ == "__main__":
app = MyApp()
app.run()
Name list added to menu:
Names added to menu and body:
Download source code: Media:MBA_ui_demo.zip
Go to another Dialog from a Dialog
# -*- coding: utf-8 -*-
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# Edited by Zhang Yu (PSZY)
# cn.psz.zhangy*gmail.com(Replace * by @)
# LICENSE: GPL3
from window import *
import appuifw
class Notepad(Dialog):
def __init__(self,cbk,txt=u''):
menu = [(u'Go to Dlg2',self.GotoList),\
(u'Save',self.close_app),\
(u'Discard',self.cancel_app)]
Dialog.__init__(self,cbk,u'MyDlg title',appuifw.Text(txt),menu)
def GotoList(self):
def cbk():
self.refresh()
self.dlg = List(cbk)
self.dlg.run()
class List(Dialog):
def __init__(self,cbk,title=u'Second Dlg'):
self.ListExample = [u'P',u'S',u'Z',u'Z']
self.body2 = appuifw.Listbox(self.ListExample,self.sayHello)
menu = [(u'Back to Dlg1',self.close_app)]
Dialog.__init__(self,cbk,title,self.body2,menu)
def sayHello(self):
num = self.body.current()
if num == 0:
appuifw.note(u'Peng','conf')
if num == 1:
appuifw.note(u'Su','conf')
if num == 2 or num ==3:
appuifw.note(u'Zhang','conf')
class MyApp(Application):
def __init__(self):
self.txt = u''
items = [ u'Text editor',u'Option B',u'Option C']
menu = [(u'Text editor',self.text_editor),\
(u'Menu B',self.option_b),\
(u'Menu C',self.option_c)]
body = appuifw.Listbox(items,self.check_items)
Application.__init__(self,u'MyApp title',body,menu)
def check_items(self):
idx = self.body.current()
( self.text_editor,self.option_b,self.option_c)[idx]()
def text_editor(self):
def cbk():
if not self.dlg.cancel:
self.txt = self.dlg.body.get()
appuifw.note(self.txt,'info')
self.refresh()
return True
self.dlg = Notepad(cbk,self.txt)
self.dlg.run()
def option_b(self):appuifw.note(u'B','conf')
def option_c(self):appuifw.note(u'C','conf')
def close_app(self):
ny = appuifw.popup_menu([u'No',u'Yes'],u'Exit?')
if ny is not None:
if ny == 1:
Application.close_app(self)
if __name__ == '__main__':
app = MyApp()
app.run()
Tabbed applications
Tabbed applications are supporting as well. In this case, the body must be replaced by a list of bodies with the following format:
[(tab_text, body, menu),(tab_text, body, menu),...]
where:
* tab_text: unicode string used in tab* body: a valid body (Listbox, Text or Canvas)* menu: menu for that body
Each entry in this list will be displayed in a tab. You can specify a global menu to be added to the bottom of each tab menu. This way, it is simple to share common function (like exit) among all tabs. Just specify this menu when calling Dialog() or Application.
# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
from window import Application
from appuifw import *
class MyApp(Application):
def __init__(self):
# defining menus for each tab/body
ma=[(u"menu a",lambda:self.msg(u"menu a"))]
mb=[(u"menu b",lambda:self.msg(u"menu b"))]
mc=[(u"menu c",lambda:self.msg(u"menu c"))]
md=[(u"menu d",lambda:self.msg(u"menu d"))]
# common menu for all tabs
common_menu=[(u"common menu",lambda:self.msg(u"common menu"))]
# bodies
ba=Listbox([u"a",u"b",u"c"])
bb=Canvas()
bc=Text(u"Text")
bd=Listbox([u"1",u"2",u"3"])
Application.__init__(self,
u"MyApp title",
[(u"Tab a",ba,ma),(u"Tab b",bb,mb),
(u"Tab c",bc,mc),(u"Tab d",bd,md)],
common_menu)
def msg(self,m):
note(m,"info")
if __name__ == "__main__":
app = MyApp()
app.run()
Tabbed application in action:
Locking UI
For time-consuming operations, such as network connections, one interesting option is to lock the user interface to avoid undesired user actions. Two methods are used in such a situation: lock_ui() and unlock_ui(). Simply lock the UI, do whatever you want to do, and unlock the UI. If you wish, change the application title during this locking to indicate some operation status, and do not forget to call refresh() just after unlocking the UI.
self.lock_ui(u"Connecting...")
#
# your stuff here
#
self.unlock_ui()
self.set_title(u"My app")
Conclusion
The framework presented here, although simple, is powerful and easy to use, allowing rapid prototyping of applications with multiple dialogues. It is used in the project Wordmobi, where more use cases can be found.
Related links
'기본 카테고리' 카테고리의 다른 글
정보시스템감리사 수검전략 (0) | 2010.02.28 |
---|---|
IT관련 직종의 평균연봉.... (0) | 2010.02.28 |
착용형 컴퓨터를 위한 햅택 기술 동향 (0) | 2009.12.13 |
웨어러블 컴퓨터의 연구동향 (0) | 2009.12.13 |
Android 기본정리 (0) | 2009.12.13 |