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