Fix ListView to make sense.
[matthijs/upstream/mobilegtd.git] / src / gui / gui.py
1 from config.config import *
2 from model.projects import Projects
3
4 import appuifw
5 import thread
6 from log.logging import logger
7 from e32 import Ao_lock, in_emulator
8 from key_codes import *
9 import key_codes
10
11
12 def show_config(cfg):        
13     fields = []
14     for k, v in cfg.items():
15         v = cfg.format_value(v)
16         if isinstance(v, int) or isinstance(v, long):
17             tname = 'number'
18             v = int(v)
19         elif isinstance(v, list) or isinstance(v, tuple):
20             for item in v[0]:
21                 if not isinstance(item, unicode):
22                     raise Exception("list can contain only unicode objects, "\
23                                     "object %r is not supported" % item)
24             
25             tname = 'combo'
26         elif isinstance(v, unicode):
27             tname = 'text'
28         else:
29             raise Exception("%s has non-supported value" % k)
30
31         fields.append((unicode(k), tname, v))
32
33
34     form = appuifw.Form(fields=fields, flags=appuifw.FFormEditModeOnly | \
35                         appuifw.FFormDoubleSpaced)
36
37     saved = [False]
38     def save_hook(param):
39         saved[0] = True
40     form.save_hook = save_hook
41     
42     form.execute()
43
44     # return true if user saved, false otherwise
45     if not saved[0]:
46         return False
47     
48     for label, tname, value in form:
49         if tname == 'combo':
50             value = (value[0], int(value[1]))
51
52         cfg[str(label)] = cfg.parse_value(value)
53
54     return True
55
56
57 def no_action():
58     pass
59
60 def applicable_functions(obj,allowed_function_names):
61     function_names = [function_name for function_name in dir(obj) if function_name in allowed_function_names]
62     return [eval('obj.%s'%function_name) for function_name in function_names]
63
64 def get_key(key_name):
65     if key_name == '':
66         key = None
67     else:
68         key=eval('EKey%s'%key_name)
69     return key
70
71 def all_key_names():
72     return filter(lambda entry:entry[0:4]=='EKey',dir(key_codes))
73 def all_key_values():
74     key_values=[
75                 EKey0,
76                 EKey1,
77                 EKey2,
78                 EKey3,
79                 EKey4,
80                 EKey5,
81                 EKey6,
82                 EKey7,
83                 EKey8,
84                 EKey9,
85                 EKeyStar,
86                 EKeyHash,
87                 ]
88     return key_values
89
90
91 def save_gui(object):
92     object.old_gui = appuifw.app.body
93     object.old_menu = appuifw.app.menu
94     object.old_exit_key_handler = appuifw.app.exit_key_handler
95     object.old_title=appuifw.app.title
96
97 def restore_gui(object):
98     appuifw.app.body = object.old_gui
99     appuifw.app.menu = object.old_menu
100     appuifw.app.exit_key_handler = object.old_exit_key_handler
101     appuifw.app.title = object.old_title
102
103 class View(object):
104     def __init__(self):
105         self.title = None
106         self.view = None
107         self.lock = Ao_lock()
108         self.exit_flag = False
109         super(View, self).__init__()
110
111     def set_title(self, title):
112         self.title = title
113    
114     def set_view(self, view):
115         """
116         Sets the main view to be displayed (e.g., an appuifw.Listbox
117         instance).
118         """
119         self.view = view
120
121     def run(self):
122         self.adjustment = None
123         save_gui(self)
124         appuifw.app.screen=COMMON_CONFIG['screen'].encode('utf-8')
125         appuifw.app.title=self.title
126         appuifw.app.body=self.view
127         appuifw.app.exit_key_handler=self.exit
128         try:
129             self.lock.wait()
130             while not self.exit_flag:
131                 self.refresh()
132                 self.lock.wait()
133         except:
134             pass
135         restore_gui(self)
136
137     def exit(self):
138         self.exit_flag = True
139         self.lock.signal()
140
141     def update(self,subject=None):
142         """
143         Update the current view (e.g., make sure refresh is called). We
144         can't call it directly, since we're in another thread.
145         """
146         if self.lock:
147             self.lock.signal()
148
149     def refresh(self):
150         """
151         Called when the current view must be updated. Never call
152         directly. Subclasses should extend this method, not update.
153         """
154         appuifw.app.menu=self.get_menu_entries()
155
156     def get_menu_entries(self):
157         """ Returns a list of menu entries to display. Will be
158         automatically updated on each refresh.
159
160         Each menu entry is a tuple of a title for the entry and a
161         function to call when the entry is selected.
162         """
163         return []
164
165 class ListView(View):
166     def __init__(self):
167         super(ListView, self).__init__()
168         self.set_view(appuifw.Listbox(self.items(),self.entry_selected))
169         self.view.bind(EKeyUpArrow,lambda: self.arrow_key_pressed(-1))
170         self.view.bind(EKeyDownArrow,lambda: self.arrow_key_pressed(1))
171
172     def arrow_key_pressed(self, dir):
173         """
174         This function is called when an arrow key is pressed. Since we
175         don't get any "current list index has changed" events, we'll
176         have to create these ourselves this way.
177
178         Since the current index is only updated after the key event,
179         we'll have to adjust the index with the direction of the
180         keypress (-1 for up, +1 for down).
181         """
182         new_index = (self.selected_index() + dir) % len(self.items())
183         self.index_changed(new_index)
184
185     def entry_selected(self):
186         """
187         This function is called when the user selects an an entry (e.g.,
188         navigates to it and push the ok button).
189         """
190         pass
191
192     def index_changed(self,new_index):
193         """
194         This function is called when the index changes. The given index
195         is the new index (don't use self.selected_index() here, since it
196         won't be correct yet!).
197         """
198         pass
199
200     def items(self):
201         """ This function should return the list of items to display.
202         See appuifw.ListBox for valid elements for this list. """
203         return []
204
205     def set_index(self,index):
206         """ Changes the currently selected item to index. """
207         self.view.set_list(self.items(),index % len(self.items()))
208
209     def selected_index(self):
210         """ Returns the currently selected index. """
211         return self.view.current()
212
213 class WidgetBasedListView(ListView):
214     def __init__(self):
215         self.widgets = self.generate_widgets()
216         super(WidgetBasedListView,self).__init__()
217
218     def run(self):
219         self.refresh()
220         self.set_bindings_for_selection(0)
221         ListView.run(self)
222
223     def index_changed(self,new_index):
224         self.set_bindings_for_selection(new_index)
225         super(WidgeteBasedListView, self).index_changed(new_index)
226
227     def notify(self,object,attribute,new=None,old=None):
228         self.refresh()
229     def refresh(self):
230         self.widgets = self.generate_widgets()
231         self.redisplay_widgets()
232         super(WidgetBasedListView,self).refresh()
233     def redisplay_widgets(self):
234         self.set_index(self.selected_index())
235     def items(self):
236         return self.all_widget_texts()
237     def all_widget_texts(self):
238         return [entry.list_repr() for entry in self.widgets]
239
240     
241
242     def current_widget(self):
243         return self.widgets[self.selected_index()]
244         
245
246 class KeyBindingView(View):
247     
248     def __init__(self):
249         self.binding_map = {}
250         super(KeyBindingView,self).__init__()
251
252     def set_keybindings(self, binding_map):
253         """
254         Set a new map of key bindings. This map maps method names to a
255         tuple of keyname and description.
256
257         The method name refers to a method on the selected item, or the
258         current view.
259
260         Example: { 'search_item' : ('0', 'Search item') }
261
262         """
263         self.binding_map = binding_map
264
265     def get_menu_entries(self):
266         menu_entries=[]
267         for key,key_name,description,function in self.key_and_menu_bindings(self.selected_index()):
268             if description != '':
269                 if key:
270                     if key_name == 'Backspace': key_name='C'
271                     description='[%s] '%key_name +description
272                 else:
273                     description='    '+description
274                 menu_entries.append((description,function)) 
275         menu_entries.append((u'Exit', self.exit))
276         return menu_entries + super(KeyBindingView, self).get_menu_entries()
277
278     def set_bindings_for_selection(self,selected_index):
279         self.remove_all_key_bindings()
280         
281         for key,key_name,description,function in self.key_and_menu_bindings(selected_index):
282             if key:
283                 self.view.bind(key,function)
284         
285     def remove_all_key_bindings(self):
286         for key in all_key_values():
287             self.view.bind(key,no_action)
288
289 class SearchableListView(WidgetBasedListView):
290     def __init__(self):
291         self.current_entry_filter_index = -1
292         self.entry_filters = []
293         self.filtered_list = lambda:[]
294         self.lock = None
295         super(SearchableListView,self).__init__()
296
297     def set_filters(self, entry_filters):
298         """
299         Set the filters that could be applied to this list. Each filter
300         can be applied in turn by calling switch_entry_filter (for
301         example from a key binding).
302
303         The entry_filters argument should be a list of filters. The
304         active filter is stored into self.filtered_list and should be
305         processed by generate_widgets in the subclass.
306         """
307         self.current_entry_filter_index = 0
308         self.entry_filters = entry_filters
309         self.filtered_list = self.entry_filters[0]
310
311     def search_item(self):
312         selected_item = appuifw.selection_list(self.all_widget_texts(),search_field=1)
313         if selected_item == None or selected_item == -1:
314             selected_item = self.selected_index()
315         self.view.set_list(self.items(),selected_item)
316         self.set_bindings_for_selection(selected_item)
317
318     def switch_entry_filter(self):
319         self.current_entry_filter_index += 1
320         self.filtered_list = self.entry_filters[self.current_entry_filter_index % len(self.entry_filters)]
321         self.refresh()
322
323
324 class EditableListView(SearchableListView,KeyBindingView):
325     def __init__(self):
326         super(EditableListView, self).__init__()
327
328     def key_and_menu_bindings(self,selected_index):
329         key_and_menu_bindings=[]
330         for function in applicable_functions(self.widgets[selected_index],self.binding_map)+\
331             applicable_functions(self,self.binding_map):
332             execute_and_update_function = self.execute_and_update(function)
333             (key,description) = self.binding_map[function.__name__]
334             key_and_menu_bindings.append((get_key(key),key,description,execute_and_update_function))
335         return key_and_menu_bindings
336
337     def entry_selected(self):
338         self.current_widget().change()
339         self.refresh()
340     def execute_and_update(self,function):
341         return lambda: (function(),self.refresh(),self.index_changed())
342
343     def notify(self,item,attribute,new=None,old=None):
344         self.refresh()
345
346 #class DisplayableFunction:
347 #    def __init__(self,display_name,function):
348 #        self.display_name = display_name
349 #        self.function = function
350 #    def list_repr(self):
351 #        return self.display_name
352 #    def execute(self):
353 #        function()
354
355 # Public API
356 __all__= ('EditableListView','show_config')