Add ListView.selected_item helper.
[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 not key_name:
66         key = None
67     else:
68         key=eval('EKey%s'%key_name)
69     return key
70
71 def key_shortname(key_name):
72     """ Find the one-character name for a key """
73     if not key_name:
74         return None
75     elif key_name == 'Backspace':
76         return 'C'
77     else:
78         return key_name
79
80 def all_key_names():
81     return filter(lambda entry:entry[0:4]=='EKey',dir(key_codes))
82 def all_key_values():
83     key_values=[
84                 EKey0,
85                 EKey1,
86                 EKey2,
87                 EKey3,
88                 EKey4,
89                 EKey5,
90                 EKey6,
91                 EKey7,
92                 EKey8,
93                 EKey9,
94                 EKeyStar,
95                 EKeyHash,
96                 ]
97     return key_values
98
99
100 def save_gui(object):
101     object.old_gui = appuifw.app.body
102     object.old_menu = appuifw.app.menu
103     object.old_exit_key_handler = appuifw.app.exit_key_handler
104     object.old_title=appuifw.app.title
105
106 def restore_gui(object):
107     appuifw.app.body = object.old_gui
108     appuifw.app.menu = object.old_menu
109     appuifw.app.exit_key_handler = object.old_exit_key_handler
110     appuifw.app.title = object.old_title
111
112 class View(object):
113     def __init__(self):
114         # Store a list of keys we bound, so we can unbind them
115         self.menu_keys = []
116         self.title = None
117         self.view = None
118         self.lock = Ao_lock()
119         self.exit_flag = False
120         super(View, self).__init__()
121
122     def set_title(self, title):
123         self.title = title
124    
125     def set_view(self, view):
126         """
127         Sets the main view to be displayed (e.g., an appuifw.Listbox
128         instance).
129         """
130         self.view = view
131
132     def run(self):
133         self.adjustment = None
134         save_gui(self)
135         appuifw.app.screen=COMMON_CONFIG['screen'].encode('utf-8')
136         appuifw.app.title=self.title
137         appuifw.app.body=self.view
138         appuifw.app.exit_key_handler=self.exit
139         try:
140             self.lock.wait()
141             while not self.exit_flag:
142                 self.refresh()
143                 self.lock.wait()
144         except:
145             # TODO: Find out which exceptions to catch here. Catching
146             # and silencing all exceptions is not a good idea.
147             raise
148         restore_gui(self)
149
150     def exit(self):
151         self.exit_flag = True
152         self.lock.signal()
153
154     def update(self,subject=None):
155         """
156         Update the current view (e.g., make sure refresh is called). We
157         can't call it directly, since we're in another thread.
158         """
159         if self.lock:
160             self.lock.signal()
161
162     def refresh(self):
163         """
164         Called when the current view must be updated. Never call
165         directly. Subclasses should extend this method, not update.
166         """
167         pass
168
169     def refresh_menu(self):
170         """
171         Refresh the menu and its bindings. Calls self.menu_items() to
172         get the new menu.
173         """
174         # Two helper functions
175         def shortcut_prefix(key_name):
176             short = key_shortname(key_name)
177             return '[%s]' % short if short else '   '
178
179         def do_entry((text, callback, key_name)):
180             key = get_key(key_name)
181             if key:
182                 self.view.bind(key, callback)
183                 self.menu_keys.append(key)
184             title = "%s %s" % (shortcut_prefix(key_name), text)
185             return(title, callback)
186
187         # Clear the bindings we previously added (we can't just clear
188         # all bindings, since other classes might have added other
189         # bindings)
190         for key in self.menu_keys:
191             self.view.bind(key, no_action)
192         self.menu_keys = []
193     
194         # Set the menu, and let do_entry add binds at the same time.
195         appuifw.app.menu = [do_entry(item) for item in self.menu_items()]
196
197     def menu_items(self):
198         """
199         Should return a list of menu items. Each menu item is a tuple:
200         (text, callback, shortcut key name).
201         """
202         return [(u'Exit', self.exit, None)]
203
204 class ListView(View):
205     def __init__(self):
206         super(ListView, self).__init__()
207         self.current_index = None
208         self.set_view(appuifw.Listbox(self.items(),self.entry_selected))
209         self.view.bind(EKeyUpArrow,lambda: self.arrow_key_pressed(-1))
210         self.view.bind(EKeyDownArrow,lambda: self.arrow_key_pressed(1))
211
212     def arrow_key_pressed(self, dir):
213         """
214         This function is called when an arrow key is pressed. Since we
215         don't get any "current list index has changed" events, we'll
216         have to create these ourselves this way.
217
218         Since the current index is only updated after the key event,
219         we'll have to adjust the index with the direction of the
220         keypress (-1 for up, +1 for down).
221         """
222         self.current_index = self.wrap_index(self.selected_index() + dir)
223         self.index_changed()
224         self.current_index = None
225
226     def run(self):
227         self.index_changed()
228         super(ListView, self).run()
229
230     def entry_selected(self):
231         """
232         This function is called when the user selects an an entry (e.g.,
233         navigates to it and push the ok button).
234         """
235         pass
236
237     def index_changed(self):
238         """
239         This function is called when the index changes. The given index
240         is the new index (don't use self.selected_index() here, since it
241         won't be correct yet!).
242         """
243         pass
244
245     def items(self):
246         """ This function should return the list of items to display.
247         See appuifw.ListBox for valid elements for this list. """
248         return []
249
250     def set_index(self,index):
251         """ Changes the currently selected item to index. """
252         self.view.set_list(self.items(),index % len(self.items()))
253
254     def selected_item(self):
255         """ Returns the (title of the) currently selected list item. """
256         if not self.items_cache:
257             return None # No items, so none is selected.
258         return self.items_cache[self.selected_index()]
259
260
261     def selected_index(self):
262         """ Returns the currently selected index. """
263         if not self.current_index is None:
264             # We allow the current index to be overridden, so it can be
265             # valid during index_changed events. See arrow_key_pressed.
266             return self.current_index 
267         else:
268             return self.view.current()
269
270     def clip_index(self, index):
271         """
272         Make sure the given index fits within the bounds of this
273         list. If it doesn't, clip it off at the ends of the list (e.g,
274         -1 becomes 0).
275         """
276         max_index = len(self.items_cache) - 1
277         return max (0, min(max_index, index))
278
279     def wrap_index(self, index):
280         """
281         Make sure the given index fits within the bounds of this
282         list. If it doesn't, wrap it around (e.g., -1 becomes 5 in a
283         6-element list).
284         """
285         count = len(self.items_cache)
286         return index % count
287     
288 class WidgetBasedListView(ListView):
289     def __init__(self):
290         self.binding_map = {}
291         self.widgets = self.generate_widgets()
292         super(WidgetBasedListView,self).__init__()
293
294     def index_changed(self):
295         self.refresh_menu()
296         super(WidgetBasedListView, self).index_changed()
297
298     def entry_selected(self):
299         self.current_widget().change()
300         self.refresh()
301
302     def notify(self,object,attribute,new=None,old=None):
303         self.refresh()
304
305     def refresh(self):
306         self.widgets = self.generate_widgets()
307         self.redisplay_widgets()
308         super(WidgetBasedListView,self).refresh()
309
310     def redisplay_widgets(self):
311         """
312         Redisplay the widgets. Should be called if the widgets
313         themselves have changed, does not call generate_widgets again.
314         """
315         self.set_index(self.selected_index())
316
317     def items(self):
318         # Let ListView show each widget's text.
319         return self.all_widget_texts()
320
321     def all_widget_texts(self):
322         """
323         Return the widget texts as they should be displayed in the
324         list view.
325         """
326         return [entry.list_repr() for entry in self.widgets]
327
328     def current_widget(self):
329         """ Returns the currently selected widget. """
330         return self.widgets[self.selected_index()]
331
332     def generate_widgets():
333         """ This function should return a list of widgets. """
334         return []
335         
336     def menu_items(self):
337         # Determine the current menu based on the methods available on
338         # the selected widget and on ourselves.
339         menu_items = []
340         for function in applicable_functions(self.current_widget(),self.binding_map)+\
341             applicable_functions(self,self.binding_map):
342             (key,description) = self.binding_map[function.__name__]
343             def do_callback():
344                 function()
345                 self.update()
346             menu_items.append((description, do_callback, key))
347         menu_items += super(WidgetBasedListView, self).menu_items()
348         return menu_items
349
350     def set_menu(self, binding_map):
351         """
352         Set a new map of menu entries with hotkeys. This map maps method names to a
353         tuple of keyname and description.
354
355         Keyname is a string containing the name of the key (the
356         part after EKey, e.g., "0", "Star", etc.). Keyname can be "", in
357         which case the item has no shortcut.
358
359         The method name refers to a method on the selected widget, or
360         the current view.
361
362         Example: { 'search_item' : ('0', 'Search item') }
363         """
364         self.binding_map = binding_map
365
366 class SearchableListView(WidgetBasedListView):
367     def __init__(self):
368         self.current_entry_filter_index = -1
369         self.entry_filters = []
370         self.filtered_list = lambda:[]
371         self.lock = None
372         super(SearchableListView,self).__init__()
373
374     def set_filters(self, entry_filters):
375         """
376         Set the filters that could be applied to this list. Each filter
377         can be applied in turn by calling switch_entry_filter (for
378         example from a key binding).
379
380         The entry_filters argument should be a list of filters. The
381         active filter is stored into self.filtered_list and should be
382         processed by generate_widgets in the subclass.
383         """
384         self.current_entry_filter_index = 0
385         self.entry_filters = entry_filters
386         self.filtered_list = self.entry_filters[0]
387
388     def search_item(self):
389         selected_item = appuifw.selection_list(self.all_widget_texts(),search_field=1)
390         if selected_item == None or selected_item == -1:
391             selected_item = self.selected_index()
392         self.view.set_list(self.items(),selected_item)
393         self.set_bindings_for_selection(selected_item)
394
395     def switch_entry_filter(self):
396         self.current_entry_filter_index += 1
397         self.filtered_list = self.entry_filters[self.current_entry_filter_index % len(self.entry_filters)]
398         self.refresh()
399
400 #class DisplayableFunction:
401 #    def __init__(self,display_name,function):
402 #        self.display_name = display_name
403 #        self.function = function
404 #    def list_repr(self):
405 #        return self.display_name
406 #    def execute(self):
407 #        function()
408
409 # Public API
410 __all__= ('SearchableListView','show_config')