improved database search functions
[meabook] / ui / hildon_ui.py
1 """
2 Hildon UI for Meabook
3 """
4
5 import gtk
6 import hildon
7 import gobject
8 from gettext import gettext as _
9 from meabook.constants import *
10 from meabook.caller import PhoneCaller
11 from meabook.ui.ui import MeabookUI
12
13
14 def create_button(title, value):
15     """Create HildonButton."""
16
17     button = hildon.Button(gtk.HILDON_SIZE_AUTO | \
18         gtk.HILDON_SIZE_FINGER_HEIGHT, \
19         hildon.BUTTON_ARRANGEMENT_VERTICAL, title, value)
20     button.set_style(hildon.BUTTON_STYLE_PICKER)
21     button.set_alignment(0, 0, 0, 0)
22     return button
23
24
25 def create_menu_button(title):
26     """Create HildonMenu button."""
27
28     return hildon.Button(gtk.HILDON_SIZE_AUTO, \
29         hildon.BUTTON_ARRANGEMENT_HORIZONTAL, title)
30
31
32 def set_selector_content(selector, handler, items=[]):
33     """Updates selector widget content."""
34
35     selector.handler_block(handler) # temporary block handler
36     # setting new content
37     # model: name, internal_name, type
38     model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_INT, \
39         gobject.TYPE_STRING)
40     for name, internal_name, item_type in items:
41         model.append((name, internal_name, item_type))
42     if selector.get_num_columns():
43         selector.remove_column(0)
44     selector.append_text_column(model, False)
45     selector.unselect_all(0)
46     selector.handler_unblock(handler) # reconnect callback
47
48
49 def set_box_content(box, handler, items=[]):
50     """Updates box widget content."""
51
52     def on_button_click(widget):
53         handler(widget.get_title(), widget.get_data('iname'))
54
55     for child in box.get_children():
56         box.remove(child)
57     for title, value, internal_name, item_type in items:
58         button = create_button(title, value)
59         button.connect('clicked', on_button_click)
60         button.set_data('iname', internal_name)
61         button.set_relief(gtk.RELIEF_NONE)
62         button.show()
63         box.pack_start(button, expand=False)
64
65
66
67 class HildonMeabook(MeabookUI):
68     def __init__(self, controller, renderer, config):
69         MeabookUI.__init__(self, controller, renderer, config)
70         self.handler = None
71         self.window = hildon.StackableWindow()
72         self.window.set_title(_('Meabook'))
73         self.window.connect('destroy', self.exit)
74         # create menu buttons
75         menu = hildon.AppMenu()
76         settings_button = create_menu_button(_('Settings'))
77         about_button = create_menu_button(_('About'))
78         import_button = create_menu_button(_('Import'))
79         search_button = create_menu_button(_('Search'))
80         # create filter widgets and connect signals
81         self.level1_filter = gtk.RadioButton(None, _('level1'))
82         self.level2_filter = gtk.RadioButton(self.level1_filter, _('level2'))
83         self.level3_filter = gtk.RadioButton(self.level2_filter, _('level3'))
84         for filter_widget in [self.level1_filter, self.level2_filter, \
85             self.level3_filter]:
86             filter_widget.set_mode(False)
87             filter_widget.connect('toggled', self.apply_filter_cb)
88         # create items widgets
89         self.selector = hildon.TouchSelector()
90         self.box_container = hildon.PannableArea()
91         self.box = gtk.VBox()
92         # create search widgets
93         widgets_box = gtk.VBox()
94         self.search_widgets_box = gtk.HBox()
95         self.search_entry = hildon.Entry(gtk.HILDON_SIZE_AUTO | \
96             gtk.HILDON_SIZE_FINGER_HEIGHT)
97         clear_search_entry_container = gtk.EventBox()
98         clear_search_entry_image = gtk.image_new_from_icon_name( \
99             'general_delete', gtk.HILDON_SIZE_FINGER_HEIGHT)
100         # connect signals
101         settings_button.connect('clicked', self.show_settings_dialog_cb)
102         about_button.connect('clicked', self.show_about_dialog_cb)
103         import_button.connect('clicked', self.show_import_dialog_cb)
104         search_button.connect('clicked', self.show_search_dialog_cb)
105         self.handler = self.selector.connect('changed', self.select_item_cb)
106         self.search_entry.connect('key-release-event', self.search_item_cb)
107         clear_search_entry_container.connect('button-press-event', \
108             self.clear_search_entry_cb)
109         # packing widgets
110         # packing search widgets
111         clear_search_entry_container.add(clear_search_entry_image)
112         self.search_widgets_box.pack_start(self.search_entry, expand=True)
113         self.search_widgets_box.pack_start(clear_search_entry_container, \
114             expand=False, padding=24)
115         # packing items widgets
116         self.box_container.add_with_viewport(self.box)
117         widgets_box.pack_start(self.selector, expand=True)
118         widgets_box.pack_start(self.box_container, expand=True)
119         widgets_box.pack_end(self.search_widgets_box, expand=False)
120         self.window.add(widgets_box)
121         # packing menu widgets
122         menu.add_filter(self.level1_filter)
123         menu.add_filter(self.level2_filter)
124         menu.add_filter(self.level3_filter)
125         menu.append(settings_button)
126         menu.append(import_button)
127         menu.append(search_button)
128         menu.append(about_button)
129         menu.show_all()
130         self.window.set_app_menu(menu)
131         self.window.show_all()
132
133     def _show_ui(self, view='selector', show_search=False):
134         """Shows necessary widgets for selected view."""
135
136         if view == 'selector':
137             self.box_container.hide()
138             self.selector.show()
139         elif view == 'box':
140             self.selector.hide()
141             self.box_container.show_all()
142         else:
143             self.selector.hide()
144             self.box_container.hide()
145         if show_search:
146             self.search_entry.set_text('')
147             self.search_entry.set_placeholder(_('Enter search text here'))
148             self.search_widgets_box.show_all()
149         else:
150             self.search_widgets_box.hide()
151
152     def _unfreeze_ui(self):
153         while gtk.events_pending():
154             gtk.main_iteration(False)
155
156     def _update_title(self, title):
157         if title is not None:
158             self.window.set_title(title)
159
160     def _show_items_dialog(self, title, items, touch_selector_view=True):
161         """Creates dialog with items. If 'touch_selector_view' is True,
162         then items will be shown in TouchSelector widget, else each item
163         will be shown as Button in PannableArea."""
164
165         window = hildon.StackableWindow()
166         window.set_title(title)
167         if touch_selector_view:
168             container = hildon.TouchSelector()
169             handler = container.connect('changed', self.select_item_cb)
170             set_selector_content(container, handler, items)
171         else:
172             widgets_box = gtk.VBox()
173             container = hildon.PannableArea()
174             container.add_with_viewport(items_box)
175             set_box_content(widgets_box, self._show_item_dialog, items)
176         window.add(container)
177         window.show_all()
178
179     def _show_item_dialog(self, title, entry_id):
180         """Shows detailed item information."""
181
182         def show_settings_dialog(widget, parent, func1, func2, entry_id):
183             dialog = ConfigurationDialog(self.controller, self.config)
184             response = getattr(dialog, func1)(None, parent)
185             if response == gtk.RESPONSE_OK:
186                 func2(parent, entry_id)
187
188         def update_entry(window, entry_id):
189             # create widgets
190             entry = self.controller.get_item(entry_id)
191             widgets_table = gtk.Table(rows=1, columns=1)
192             info_box = gtk.VBox()
193             image_box = gtk.VBox()
194             pannable_area = hildon.PannableArea()
195             pannable_area.set_property('mov-mode', hildon.MOVEMENT_MODE_BOTH)
196             image = self.renderer.render_image(dict(entry))
197             for fname, fvalue in entry:
198                 if fname == 'image':
199                     continue
200                 button = self.renderer.render_button(_(fname) , fvalue, fname)
201                 if fname in (FIELD_PHONE_HOME, FIELD_PHONE_GENERAL, \
202                     FIELD_PHONE_INTERNAL, FIELD_PHONE_MOBILE):
203                     button.connect('clicked', self.call_cb)
204                 info_box.pack_start(button, expand=False)
205             # pack widgets
206             image_box.pack_start(image, expand=False)
207             widgets_table.attach(image_box, 0, 1, 0, 1, xoptions=gtk.FILL|gtk.SHRINK, \
208                 yoptions=gtk.FILL, xpadding=14, ypadding=8)
209             pannable_area.add_with_viewport(info_box)
210             widgets_table.attach(pannable_area, 1, 2, 0, 1, ypadding=8)
211             child = window.get_child()
212             if child:
213                 child.destroy()
214             window.add(widgets_table)
215             widgets_table.show_all()
216
217
218         # create widgets
219         window = hildon.StackableWindow()
220         window.set_title(title)
221         menu = hildon.AppMenu()
222         fields_button = create_menu_button(_('Fields to show'))
223         order_button = create_menu_button(_('Fields order'))
224         fields_button.connect('clicked', show_settings_dialog, window, \
225             'show_fields_settings_cb', update_entry, entry_id)
226         order_button.connect('clicked', show_settings_dialog, window, \
227             'show_order_settings_cb', update_entry, entry_id)
228         menu.append(fields_button)
229         menu.append(order_button)
230
231         update_entry(window, entry_id)
232         window.set_app_menu(menu)
233         menu.show_all()
234         window.show_all()
235
236
237     # Implementation of Base UI interface
238     def start(self):
239         self.apply_filter_cb(self.level1_filter)
240         gtk.main()
241
242     def exit(self, event):
243         gtk.main_quit()
244         self.controller.stop()
245
246     def create_information_dialog(self, title, message):
247         dialog = hildon.Dialog()
248         dialog.set_title(title)
249         label = gtk.Label(message)
250         dialog.vbox.add(label)
251         dialog.vbox.show_all()
252         dialog.run()
253         dialog.destroy()
254
255     def create_about_dialog(self):
256         AboutDialog()
257
258     def create_import_dialog(self):
259         chooser = gobject.new(hildon.FileChooserDialog, \
260             action=gtk.FILE_CHOOSER_ACTION_OPEN)
261         chooser.set_property('show-files', True)
262         chooser.run()
263         path = chooser.get_filename()
264         chooser.destroy()
265         return path
266
267     def create_progress_dialog(self, title=None):
268         self._update_title(title)
269         self._show_ui('hide', False)
270         hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
271         self._unfreeze_ui()
272
273     def create_configuration_dialog(self, controller, config):
274         ConfigurationDialog(controller, config).run()
275
276     def create_fields_configuration_dialog(self, all_fields, fields_to_show, parent):
277         dialog = FieldsConfigurationDialog(all_fields, fields_to_show, parent)
278         return dialog.run()
279
280     def create_fields_order_configuration_dialog(self, all_fields, ordered_fields):
281         dialog = FieldsOrderConfigurationDialog(all_fields, ordered_fields)
282         return dialog.run()
283
284     def create_search_dialog(self, controller):
285         dialog = SearchDialog(controller)
286         param = dialog.run()
287         if param:
288             print(param)
289
290
291     # Hildon UI callbacks
292     def show_about_dialog_cb(self, widget):
293         """Shows About Dialog."""
294
295         self.controller.show_about_dialog()
296
297     def show_import_dialog_cb(self, widget):
298         """Shows Import Dialog."""
299
300         if self.controller.show_import_dialog():
301             self._show_ui('selector')
302             self.apply_filter_cb(self.level1_filter)
303         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
304
305     def show_settings_dialog_cb(self, widget):
306         """Shows Config dialog."""
307
308         self.controller.show_configuration_dialog()
309
310     def show_search_dialog_cb(self, widget):
311         """Show Search dialog."""
312
313         self.controller.show_search_dialog()
314
315     def apply_filter_cb(self, widget):
316         """Updates toplevel selector with different level items."""
317
318         if not widget.get_active():
319             return
320
321         self._update_title(' - '.join([_('Meabook'), widget.get_label()]))
322
323         if widget == self.level1_filter:
324             self._show_ui()
325             set_selector_content(self.selector, self.handler, \
326                 self.controller.get_all_folders())
327         elif widget == self.level2_filter:
328             self._show_ui()
329             set_selector_content(self.selector, self.handler, \
330                 self.controller.get_all_subfolders())
331         else:
332             self._show_ui(view='box', show_search=True)
333             set_box_content(self.box, self._show_item_dialog, \
334                 self.controller.get_files_by_pattern())
335
336     def select_item_cb(self, widget, column):
337         """
338         Emits when changes selector content.
339         Opens new StackableWindow with new content.
340         """
341
342         item_name, internal_name, item_type = \
343             widget.get_model(0)[widget.get_active(0)]
344         if item_type == TYPE_DIRECTORY:
345             self._show_items_dialog(item_name, self.controller.get_items( \
346                 internal_name))
347         else:
348             self._show_item_dialog(item_name, internal_name)
349
350     def search_item_cb(self, widget, event):
351         """Search items from database."""
352
353         set_box_content(self.box, self._show_item_dialog, \
354             self.controller.get_files_by_pattern(widget.get_text()))
355         widget.grab_focus()
356
357     def clear_search_entry_cb(self, widget, event):
358         """Clears search entry content."""
359
360         self.search_entry.set_text('')
361         set_box_content(self.box, self._show_item_dialog, \
362                 self.controller.get_files_by_pattern())
363
364
365     def call_cb(self, widget):
366         """Nake a phone call."""
367
368         self.controller.call(widget.get_value())
369
370
371
372 class AboutDialog(hildon.Dialog):
373     """Hildon About Dialog."""
374
375     def __init__(self):
376         from meabook.version import version
377         hildon.Dialog.__init__(self)
378         self.set_title(_('About'))
379         info_label = gtk.Label()
380         info_label.set_use_markup(True)
381         info_label.set_justify(gtk.JUSTIFY_CENTER)
382         info_label.set_markup("<span foreground='white' size='medium'><b>" \
383             "Meabook</b></span><span foreground='white' size='small'> - " \
384             "Enterprise address book</span>\n<span foreground='white' " \
385             "size='small'>Version %s</span>\n\n\n<span foreground='white'" \
386             "size='small'><b>Developers:</b></span>\n<span foreground=" \
387             "'white' size='small'>Tanya Makova | </span><span foreground=" \
388             "'#299BFC' size='small'>tanyshk@gmail.com</span>\n<span " \
389             "foreground='white' size='small'>Max Usachev | </span><span " \
390             "foreground='#299BFC' size='small'>maxusachev@gmail.com</span>" \
391             "\n" % version)
392         self.vbox.add(info_label)
393         self.vbox.show_all()
394         self.run()
395         self.destroy()
396
397
398
399 class SearchDialog:
400     """Hildon Search Dialog."""
401
402     def __init__(self, controller):
403         self.controller = controller
404
405     def run(self):
406         dialog = hildon.Dialog()
407         dialog.set_title(_('Search'))
408
409         selector = hildon.TouchSelector(text=True)
410         #for item in ('cn', 'mobile'):
411         for item in self.controller.get_fields():
412             selector.append_text(_(item))
413         selector.set_active(0, 0)
414
415         button = hildon.PickerButton(gtk.HILDON_SIZE_AUTO | \
416             gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
417         button.set_alignment(0, 0, 0, 0)
418         button.set_title(_("Search criterion"))
419         button.set_selector(selector)
420
421         entry = hildon.Entry(gtk.HILDON_SIZE_AUTO | gtk.HILDON_SIZE_FINGER_HEIGHT)
422         entry.set_placeholder(_("Enter search text here"))
423
424         dialog.vbox.pack_start(button, expand=False)
425         dialog.vbox.pack_end(entry, expand=False)
426         dialog.vbox.show_all()
427         dialog.add_button(_('Search'), gtk.RESPONSE_OK)
428         response = dialog.run()
429         if response == gtk.RESPONSE_OK:
430             result = (self.controller.get_localized_fields()[ \
431                 button.get_value().decode('utf-8')], entry.get_text())
432             dialog.destroy()
433             return result
434         dialog.destroy()
435         return (False, None)
436
437
438
439 class ConfigurationDialog:
440     """Hildon Configuration Dialog."""
441
442     def __init__(self, controller, config):
443         self.config = config
444         self.controller = controller
445
446     def run(self):
447         dialog = hildon.Dialog()
448         dialog.set_title(_('Settings'))
449
450         button_order = create_button(_('Fields order'), self._update_value( \
451             None, self.config.get_order()))
452         button_order.connect('clicked', self.show_order_settings_cb, dialog)
453         button_fields = create_button(_('Fields to show'), self._update_value( \
454             None, self.config.get_fields()))
455         button_fields.connect('clicked', self.show_fields_settings_cb, dialog)
456
457         dialog.vbox.pack_start(button_fields, expand=False)
458         dialog.vbox.pack_start(button_order, expand=False)
459         dialog.vbox.show_all()
460         dialog.run()
461         dialog.destroy()
462
463     def _update_value(self, widget, fields):
464         """Updates widget title."""
465
466         value = ', '.join([_(field) for field in fields])
467         if widget is not None:
468             widget.set_value(value)
469         return value
470
471     def show_fields_settings_cb(self, widget, parent):
472         """Shows dialog for selecting fields to show."""
473
474         dialog = hildon.PickerDialog(parent)
475         dialog.set_title(_('Fields to show'))
476         selector = hildon.TouchSelector(text=True)
477
478         fields = self.controller.get_fields()
479         # fill items list
480         for field in fields:
481             selector.append_text(_(field))
482
483         selector.set_column_selection_mode( \
484             hildon.TOUCH_SELECTOR_SELECTION_MODE_MULTIPLE)
485         selector.unselect_all(0)
486
487         # mark necessary fields
488         fields_to_select = self.config.get_fields()
489         model = selector.get_model(0)
490         for index, field in enumerate(fields):
491             if field in fields_to_select:
492                 selector.select_iter(0, model.get_iter(index), False)
493
494         dialog.set_selector(selector)
495         response = dialog.run()
496         if response == gtk.RESPONSE_OK:
497             model = selector.get_model(0)
498             selected_item_indexes = [index for index in [item[0] for item \
499                 in selector.get_selected_rows(0)]]
500             selected_fields = [fields[index] for index in selected_item_indexes]
501             self.config.set_fields(selected_fields)
502             self._update_value(widget, selected_fields)
503         dialog.destroy()
504         return response
505
506     def show_order_settings_cb(self, widget, parent):
507         """Shows dialog for setting fields order."""
508
509
510         def show_fields_chooser(widget, parent):
511             """Shows dialog to select field from fields list."""
512
513             dialog = hildon.PickerDialog(parent)
514             dialog.set_title(_('Fields'))
515             selector = hildon.TouchSelector(text=True)
516             dialog.set_selector(selector)
517             selector.set_column_selection_mode( \
518                 hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
519             # fill fields list
520             for field in self.controller.get_fields():
521                 selector.append_text(_(field))
522             dialog.run()
523             widget.set_value(selector.get_current_text())
524             dialog.destroy()
525
526
527         dialog = hildon.Dialog()
528         dialog.set_title(_('Fields order'))
529         pannable_area = hildon.PannableArea()
530         pannable_area.set_size_request_policy(hildon.SIZE_REQUEST_CHILDREN)
531
532         vbox = gtk.VBox()
533         for index, field in enumerate(self.config.get_order()):
534             button = create_button(' '.join([_('Position'), str(index)]), \
535                 _(field))
536             button.connect('clicked', show_fields_chooser, dialog)
537             vbox.pack_start(button, expand=False)
538         pannable_area.add_with_viewport(vbox)
539         dialog.add_button(_('Done'), gtk.RESPONSE_OK)
540         dialog.vbox.pack_start(pannable_area)
541         dialog.vbox.show_all()
542         response = dialog.run()
543         if response == gtk.RESPONSE_OK:
544             fields_dict = self.controller.get_localized_fields()
545             new_ordered_fields = [fields_dict[button.get_value().decode( \
546                 'utf-8')] for button in vbox.get_children()]
547             self.config.set_order(new_ordered_fields)
548             self._update_value(widget, new_ordered_fields)
549         dialog.destroy()
550         return response
551
552
553
554 class FieldsConfigurationDialog():
555     def __init__(self, fields, fields_to_select, parent):
556         dialog = hildon.PickerDialog(parent)
557         dialog.set_title(_('Fields to show'))
558         selector = hildon.TouchSelector(text=True)
559         # fill items list
560         for field in fields.keys():
561             selector.append_text(field)
562         selector.set_column_selection_mode( \
563             hildon.TOUCH_SELECTOR_SELECTION_MODE_MULTIPLE)
564         selector.unselect_all(0)
565         # mark necessary fields
566         model = selector.get_model(0)
567         for index, field in enumerate(fields.values()):
568             if field in fields_to_select:
569                 selector.select_iter(0, model.get_iter(index), False)
570         dialog.set_selector(selector)
571         # creates class attributes
572         self.fields = fields
573         self.dialog = dialog
574         self.selector = selector
575
576     def run(self):
577         if self.dialog.run() == gtk.RESPONSE_OK:
578             model = self.selector.get_model(0)
579             indexes = [item[0] for item in self.selector.get_selected_rows(0)]
580             self.dialog.destroy()
581             return [self.fields.values()[index] for index in indexes]
582         self.dialog.destroy()
583         return None
584
585
586
587 class FieldsOrderConfigurationDialog():
588     def __init__(self, fields, ordered_fields):
589         dialog = hildon.Dialog()
590         dialog.set_title(_('Fields order'))
591         pannable_area = hildon.PannableArea()
592         pannable_area.set_size_request_policy(hildon.SIZE_REQUEST_CHILDREN)
593         vbox = gtk.VBox()
594         for index, field in enumerate(ordered_fields):
595             button = create_button(' '.join([_('Position'), str(index)]), \
596                 _(field))
597             button.connect('clicked', self.show_fields_chooser, fields.keys(), \
598                 dialog)
599             vbox.pack_start(button, expand=False)
600         pannable_area.add_with_viewport(vbox)
601         dialog.add_button(_('Done'), gtk.RESPONSE_OK)
602         dialog.vbox.pack_start(pannable_area)
603         dialog.vbox.show_all()
604         # create class attributes
605         self.dialog = dialog
606         self.fields = fields
607         self.widgets_box = vbox
608
609     def run(self):
610         if self.dialog.run() == gtk.RESPONSE_OK:
611             selected_fields = [self.fields[button.get_value().decode('utf-8')] \
612                 for button in self.widgets_box.get_children()]
613             self.dialog.destroy()
614             return selected_fields
615         self.dialog.destroy()
616         return None
617
618     def show_fields_chooser(self, widget, fields, parent):
619         """Shows dialog to select field from fields list."""
620
621         dialog = hildon.PickerDialog(parent)
622         dialog.set_title(_('Fields'))
623         selector = hildon.TouchSelector(text=True)
624         dialog.set_selector(selector)
625         selector.set_column_selection_mode( \
626             hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
627         # fill fields list
628         for field in fields:
629             selector.append_text(field)
630         dialog.run()
631         widget.set_value(selector.get_current_text())
632         dialog.destroy()