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