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