a981b61111d5c44bd7daadbe7360cb9154294729
[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             pass
146         restore_gui(self)
147
148     def exit(self):
149         self.exit_flag = True
150         self.lock.signal()
151
152     def update(self,subject=None):
153         """
154         Update the current view (e.g., make sure refresh is called). We
155         can't call it directly, since we're in another thread.
156         """
157         if self.lock:
158             self.lock.signal()
159
160     def refresh(self):
161         """
162         Called when the current view must be updated. Never call
163         directly. Subclasses should extend this method, not update.
164         """
165         pass
166
167     def refresh_menu(self):
168         """
169         Refresh the menu and its bindings. Calls self.menu_items() to
170         get the new menu.
171         """
172         # Two helper functions
173         def shortcut_prefix(key_name):
174             short = key_shortname(key_name)
175             return '[%s]' % short if short else '   '
176
177         def do_entry((text, callback, key_name)):
178             key = get_key(key_name)
179             if key:
180                 self.view.bind(key, callback)
181                 self.menu_keys.append(key)
182             title = "%s %s" % (shortcut_prefix(key_name), text)
183             return(title, callback)
184
185         # Clear the bindings we previously added (we can't just clear
186         # all bindings, since other classes might have added other
187         # bindings)
188         for key in self.menu_keys:
189             self.view.bind(key, no_action)
190         self.menu_keys = []
191     
192         # Set the menu, and let do_entry add binds at the same time.
193         appuifw.app.menu = [do_entry(item) for item in self.menu_items()]
194
195     def menu_items(self):
196         """
197         Should return a list of menu items. Each menu item is a tuple:
198         (text, callback, shortcut key name).
199         """
200         return [(u'Exit', self.exit, None)]
201
202 class ListView(View):
203     def __init__(self):
204         super(ListView, self).__init__()
205         self.current_index = None
206         self.set_view(appuifw.Listbox(self.items(),self.entry_selected))
207         self.view.bind(EKeyUpArrow,lambda: self.arrow_key_pressed(-1))
208         self.view.bind(EKeyDownArrow,lambda: self.arrow_key_pressed(1))
209
210     def arrow_key_pressed(self, dir):
211         """
212         This function is called when an arrow key is pressed. Since we
213         don't get any "current list index has changed" events, we'll
214         have to create these ourselves this way.
215
216         Since the current index is only updated after the key event,
217         we'll have to adjust the index with the direction of the
218         keypress (-1 for up, +1 for down).
219         """
220         self.current_index = self.wrap_index(self.selected_index() + dir)
221         self.index_changed()
222         self.current_index = None
223
224     def run(self):
225         self.index_changed()
226         super(ListView, self).run()
227
228     def entry_selected(self):
229         """
230         This function is called when the user selects an an entry (e.g.,
231         navigates to it and push the ok button).
232         """
233         pass
234
235     def index_changed(self):
236         """
237         This function is called when the index changes. The given index
238         is the new index (don't use self.selected_index() here, since it
239         won't be correct yet!).
240         """
241         pass
242
243     def items(self):
244         """ This function should return the list of items to display.
245         See appuifw.ListBox for valid elements for this list. """
246         return []
247
248     def set_index(self,index):
249         """ Changes the currently selected item to index. """
250         self.view.set_list(self.items(),index % len(self.items()))
251
252     def selected_index(self):
253         """ Returns the currently selected index. """
254         if not self.current_index is None:
255             # We allow the current index to be overridden, so it can be
256             # valid during index_changed events. See arrow_key_pressed.
257             return self.current_index 
258         else:
259             return self.view.current()
260
261     def clip_index(self, index):
262         """
263         Make sure the given index fits within the bounds of this
264         list. If it doesn't, clip it off at the ends of the list (e.g,
265         -1 becomes 0).
266         """
267         max_index = len(self.items_cache) - 1
268         return max (0, min(max_index, index))
269
270     def wrap_index(self, index):
271         """
272         Make sure the given index fits within the bounds of this
273         list. If it doesn't, wrap it around (e.g., -1 becomes 5 in a
274         6-element list).
275         """
276         count = len(self.items_cache)
277         return index % count
278     
279 class WidgetBasedListView(ListView):
280     def __init__(self):
281         self.binding_map = {}
282         self.widgets = self.generate_widgets()
283         super(WidgetBasedListView,self).__init__()
284
285     def index_changed(self):
286         self.refresh_menu()
287         super(WidgetBasedListView, self).index_changed()
288
289     def entry_selected(self):
290         self.current_widget().change()
291         self.refresh()
292
293     def notify(self,object,attribute,new=None,old=None):
294         self.refresh()
295
296     def refresh(self):
297         self.widgets = self.generate_widgets()
298         self.redisplay_widgets()
299         super(WidgetBasedListView,self).refresh()
300
301     def redisplay_widgets(self):
302         """
303         Redisplay the widgets. Should be called if the widgets
304         themselves have changed, does not call generate_widgets again.
305         """
306         self.set_index(self.selected_index())
307
308     def items(self):
309         # Let ListView show each widget's text.
310         return self.all_widget_texts()
311
312     def all_widget_texts(self):
313         """
314         Return the widget texts as they should be displayed in the
315         list view.
316         """
317         return [entry.list_repr() for entry in self.widgets]
318
319     def current_widget(self):
320         """ Returns the currently selected widget. """
321         return self.widgets[self.selected_index()]
322
323     def generate_widgets():
324         """ This function should return a list of widgets. """
325         return []
326         
327     def menu_items(self):
328         # Determine the current menu based on the methods available on
329         # the selected widget and on ourselves.
330         menu_items = []
331         for function in applicable_functions(self.current_widget(),self.binding_map)+\
332             applicable_functions(self,self.binding_map):
333             (key,description) = self.binding_map[function.__name__]
334             def do_callback():
335                 function()
336                 self.update()
337             menu_items.append((description, do_callback, key))
338         menu_items += super(WidgetBasedListView, self).menu_items()
339         return menu_items
340
341     def set_menu(self, binding_map):
342         """
343         Set a new map of menu entries with hotkeys. This map maps method names to a
344         tuple of keyname and description.
345
346         Keyname is a string containing the name of the key (the
347         part after EKey, e.g., "0", "Star", etc.). Keyname can be "", in
348         which case the item has no shortcut.
349
350         The method name refers to a method on the selected widget, or
351         the current view.
352
353         Example: { 'search_item' : ('0', 'Search item') }
354         """
355         self.binding_map = binding_map
356
357 class SearchableListView(WidgetBasedListView):
358     def __init__(self):
359         self.current_entry_filter_index = -1
360         self.entry_filters = []
361         self.filtered_list = lambda:[]
362         self.lock = None
363         super(SearchableListView,self).__init__()
364
365     def set_filters(self, entry_filters):
366         """
367         Set the filters that could be applied to this list. Each filter
368         can be applied in turn by calling switch_entry_filter (for
369         example from a key binding).
370
371         The entry_filters argument should be a list of filters. The
372         active filter is stored into self.filtered_list and should be
373         processed by generate_widgets in the subclass.
374         """
375         self.current_entry_filter_index = 0
376         self.entry_filters = entry_filters
377         self.filtered_list = self.entry_filters[0]
378
379     def search_item(self):
380         selected_item = appuifw.selection_list(self.all_widget_texts(),search_field=1)
381         if selected_item == None or selected_item == -1:
382             selected_item = self.selected_index()
383         self.view.set_list(self.items(),selected_item)
384         self.set_bindings_for_selection(selected_item)
385
386     def switch_entry_filter(self):
387         self.current_entry_filter_index += 1
388         self.filtered_list = self.entry_filters[self.current_entry_filter_index % len(self.entry_filters)]
389         self.refresh()
390
391 #class DisplayableFunction:
392 #    def __init__(self,display_name,function):
393 #        self.display_name = display_name
394 #        self.function = function
395 #    def list_repr(self):
396 #        return self.display_name
397 #    def execute(self):
398 #        function()
399
400 # Public API
401 __all__= ('SearchableListView','show_config')