f7ee31e78f89eed14e89f3b80b6d973dc4aa9f8a
[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_index(self):
255         """ Returns the currently selected index. """
256         if not self.current_index is None:
257             # We allow the current index to be overridden, so it can be
258             # valid during index_changed events. See arrow_key_pressed.
259             return self.current_index 
260         else:
261             return self.view.current()
262
263     def clip_index(self, index):
264         """
265         Make sure the given index fits within the bounds of this
266         list. If it doesn't, clip it off at the ends of the list (e.g,
267         -1 becomes 0).
268         """
269         max_index = len(self.items_cache) - 1
270         return max (0, min(max_index, index))
271
272     def wrap_index(self, index):
273         """
274         Make sure the given index fits within the bounds of this
275         list. If it doesn't, wrap it around (e.g., -1 becomes 5 in a
276         6-element list).
277         """
278         count = len(self.items_cache)
279         return index % count
280     
281 class WidgetBasedListView(ListView):
282     def __init__(self):
283         self.binding_map = {}
284         self.widgets = self.generate_widgets()
285         super(WidgetBasedListView,self).__init__()
286
287     def index_changed(self):
288         self.refresh_menu()
289         super(WidgetBasedListView, self).index_changed()
290
291     def entry_selected(self):
292         self.current_widget().change()
293         self.refresh()
294
295     def notify(self,object,attribute,new=None,old=None):
296         self.refresh()
297
298     def refresh(self):
299         self.widgets = self.generate_widgets()
300         self.redisplay_widgets()
301         super(WidgetBasedListView,self).refresh()
302
303     def redisplay_widgets(self):
304         """
305         Redisplay the widgets. Should be called if the widgets
306         themselves have changed, does not call generate_widgets again.
307         """
308         self.set_index(self.selected_index())
309
310     def items(self):
311         # Let ListView show each widget's text.
312         return self.all_widget_texts()
313
314     def all_widget_texts(self):
315         """
316         Return the widget texts as they should be displayed in the
317         list view.
318         """
319         return [entry.list_repr() for entry in self.widgets]
320
321     def current_widget(self):
322         """ Returns the currently selected widget. """
323         return self.widgets[self.selected_index()]
324
325     def generate_widgets():
326         """ This function should return a list of widgets. """
327         return []
328         
329     def menu_items(self):
330         # Determine the current menu based on the methods available on
331         # the selected widget and on ourselves.
332         menu_items = []
333         for function in applicable_functions(self.current_widget(),self.binding_map)+\
334             applicable_functions(self,self.binding_map):
335             (key,description) = self.binding_map[function.__name__]
336             def do_callback():
337                 function()
338                 self.update()
339             menu_items.append((description, do_callback, key))
340         menu_items += super(WidgetBasedListView, self).menu_items()
341         return menu_items
342
343     def set_menu(self, binding_map):
344         """
345         Set a new map of menu entries with hotkeys. This map maps method names to a
346         tuple of keyname and description.
347
348         Keyname is a string containing the name of the key (the
349         part after EKey, e.g., "0", "Star", etc.). Keyname can be "", in
350         which case the item has no shortcut.
351
352         The method name refers to a method on the selected widget, or
353         the current view.
354
355         Example: { 'search_item' : ('0', 'Search item') }
356         """
357         self.binding_map = binding_map
358
359 class SearchableListView(WidgetBasedListView):
360     def __init__(self):
361         self.current_entry_filter_index = -1
362         self.entry_filters = []
363         self.filtered_list = lambda:[]
364         self.lock = None
365         super(SearchableListView,self).__init__()
366
367     def set_filters(self, entry_filters):
368         """
369         Set the filters that could be applied to this list. Each filter
370         can be applied in turn by calling switch_entry_filter (for
371         example from a key binding).
372
373         The entry_filters argument should be a list of filters. The
374         active filter is stored into self.filtered_list and should be
375         processed by generate_widgets in the subclass.
376         """
377         self.current_entry_filter_index = 0
378         self.entry_filters = entry_filters
379         self.filtered_list = self.entry_filters[0]
380
381     def search_item(self):
382         selected_item = appuifw.selection_list(self.all_widget_texts(),search_field=1)
383         if selected_item == None or selected_item == -1:
384             selected_item = self.selected_index()
385         self.view.set_list(self.items(),selected_item)
386         self.set_bindings_for_selection(selected_item)
387
388     def switch_entry_filter(self):
389         self.current_entry_filter_index += 1
390         self.filtered_list = self.entry_filters[self.current_entry_filter_index % len(self.entry_filters)]
391         self.refresh()
392
393 #class DisplayableFunction:
394 #    def __init__(self,display_name,function):
395 #        self.display_name = display_name
396 #        self.function = function
397 #    def list_repr(self):
398 #        return self.display_name
399 #    def execute(self):
400 #        function()
401
402 # Public API
403 __all__= ('SearchableListView','show_config')