374f96493866e6b64989c7896821f75fee8047ff
[platform/upstream/ibus.git] / ui / gtk2 / candidatepanel.py
1 # vim:set et sts=4 sw=4:
2 #
3 # ibus - The Input Bus
4 #
5 # Copyright(c) 2007-2010 Peng Huang <shawn.p.huang@gmail.com>
6 # Copyright(c) 2007-2010 Red Hat, Inc.
7 #
8 # This library is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU Lesser General Public
10 # License as published by the Free Software Foundation; either
11 # version 2.1 of the License, or (at your option) any later version.
12 #
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 # Lesser General Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public
19 # License along with this library; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
21 # USA
22
23 import operator
24 import gtk
25 import gtk.gdk as gdk
26 import gobject
27 import pango
28 import ibus
29 from ibus._gtk import PangoAttrList
30 from handle import Handle
31 from i18n import _, N_
32
33 class EventBox(gtk.EventBox):
34     __gtype_name__ = "IBusEventBox"
35     def __init__(self):
36         super(EventBox, self).__init__()
37         self.connect("realize", self.__realize_cb)
38
39     def __realize_cb(self, widget):
40         widget.window.set_cursor(gdk.Cursor(gdk.HAND2))
41
42 class Label(gtk.Label):
43     __gtype_name__ = "IBusCandidateLabel"
44
45 class HSeparator(gtk.HBox):
46     __gtype_name__ = "IBusHSeparator"
47     def __init__(self):
48         super(HSeparator, self).__init__()
49         self.pack_start(gtk.HSeparator(), True, True, 4)
50
51 class VSeparator(gtk.VBox):
52     __gtype_name__ = "IBusVSeparator"
53     def __init__(self):
54         super(VSeparator, self).__init__()
55         self.pack_start(gtk.VSeparator(), True, True, 4)
56
57 class CandidateArea(gtk.HBox):
58     __gtype_name__ = "IBusCandidateArea"
59     __gsignals__ = {
60         "candidate-clicked" : (
61             gobject.SIGNAL_RUN_FIRST,
62             gobject.TYPE_NONE,
63             (gobject.TYPE_UINT, gobject.TYPE_UINT, gobject.TYPE_UINT )),
64     }
65
66     def __init__(self, orientation):
67         super(CandidateArea, self).__init__()
68         self.set_name("IBusCandidateArea")
69         self.__orientation = orientation
70         self.__labels = []
71         self.__candidates = []
72         self.__create_ui()
73
74     def __create_ui(self):
75         if self.__orientation == ibus.ORIENTATION_VERTICAL:
76             self.__vbox1 = gtk.VBox()
77             self.__vbox1.set_homogeneous(True)
78             self.__vbox2 = gtk.VBox()
79             self.__vbox2.set_homogeneous(True)
80             self.pack_start(self.__vbox1, False, False, 4)
81             self.pack_start(VSeparator(), False, False, 0)
82             self.pack_start(self.__vbox2, True, True, 4)
83
84         for i in range(0, 16):
85             label1 = Label("%c." % ("1234567890abcdef"[i]))
86             label1.set_alignment(0.0, 0.5)
87             label1.show()
88
89             label2 = Label()
90             label2.set_alignment(0.0, 0.5)
91             label1.show()
92
93             if self.__orientation == ibus.ORIENTATION_VERTICAL:
94                 label1.set_property("xpad", 8)
95                 label2.set_property("xpad", 8)
96                 ebox1 = EventBox()
97                 ebox1.set_no_show_all(True)
98                 ebox1.add(label1)
99                 ebox2 = EventBox()
100                 ebox2.set_no_show_all(True)
101                 ebox2.add(label2)
102                 self.__vbox1.pack_start(ebox1, False, False, 2)
103                 self.__vbox2.pack_start(ebox2, False, False, 2)
104                 self.__candidates.append((ebox1, ebox2))
105             else:
106                 hbox = gtk.HBox()
107                 hbox.show()
108                 hbox.pack_start(label1, False, False, 1)
109                 hbox.pack_start(label2, False, False, 1)
110                 ebox = EventBox()
111                 ebox.set_no_show_all(True)
112                 ebox.add(hbox)
113                 self.pack_start(ebox, False, False, 4)
114                 self.__candidates.append((ebox,))
115
116             self.__labels.append((label1, label2))
117
118         for i, ws in enumerate(self.__candidates):
119             for w in ws:
120                 w.connect("button-press-event", lambda w, e, i:self.emit("candidate-clicked", i, e.button, e.state), i)
121
122     def set_labels(self, labels):
123         if not labels:
124             for i in xrange(0, 16):
125                 self.__labels[i][0].set_text("%c." % ("1234567890abcdef"[i]))
126                 self.__labels[i][0].set_property("attributes", None)
127             return
128
129         i = 0
130         for text, attrs in labels:
131             self.__labels[i][0].set_text(text)
132             self.__labels[i][0].set_property("attributes", attrs)
133             i += 1
134             if i >= 16:
135                 break
136
137     def set_candidates(self, candidates, focus_candidate = 0, show_cursor = True):
138         assert len(candidates) <= len(self.__labels)
139         for i, (text, attrs) in enumerate(candidates):
140             if i == focus_candidate and show_cursor:
141                 if attrs == None:
142                     attrs = pango.AttrList()
143                 color = self.__labels[i][1].style.base[gtk.STATE_SELECTED]
144                 end_index = len(text.encode("utf8"))
145                 attr = pango.AttrBackground(color.red, color.green, color.blue, 0, end_index)
146                 attrs.change(attr)
147                 color = self.__labels[i][1].style.text[gtk.STATE_SELECTED]
148                 attr = pango.AttrForeground(color.red, color.green, color.blue, 0, end_index)
149                 attrs.insert(attr)
150
151             self.__labels[i][1].set_text(text)
152             self.__labels[i][1].show()
153             self.__labels[i][1].set_property("attributes", attrs)
154             for w in self.__candidates[i]:
155                 w.show()
156
157         for w in reduce(operator.add, self.__candidates[len(candidates):]):
158             w.hide()
159
160 class CandidatePanel(gtk.VBox):
161     __gtype_name__ = "IBusCandidate"
162     __gsignals__ = {
163         "cursor-up" : (
164             gobject.SIGNAL_RUN_FIRST,
165             gobject.TYPE_NONE,
166             ()),
167         "cursor-down" : (
168             gobject.SIGNAL_RUN_FIRST,
169             gobject.TYPE_NONE,
170             ()),
171         "page-up" : (
172             gobject.SIGNAL_RUN_FIRST,
173             gobject.TYPE_NONE,
174             ()),
175         "page-down" : (
176             gobject.SIGNAL_RUN_FIRST,
177             gobject.TYPE_NONE,
178             ()),
179         "candidate-clicked" : (
180             gobject.SIGNAL_RUN_FIRST,
181             gobject.TYPE_NONE,
182             (gobject.TYPE_UINT, gobject.TYPE_UINT, gobject.TYPE_UINT)),
183     }
184
185     def __init__(self):
186         super(CandidatePanel, self).__init__()
187         self.set_name("IBusCandidate")
188
189         self.__toplevel = gtk.Window(gtk.WINDOW_POPUP)
190         self.__viewport = gtk.Viewport()
191         self.__viewport.set_shadow_type(gtk.SHADOW_IN)
192         self.__toplevel.add(self.__viewport)
193
194         hbox = gtk.HBox()
195         handle = Handle()
196         handle.connect("move-end", self.__handle_move_end_cb)
197         hbox.pack_start(handle)
198         hbox.pack_start(self)
199
200         self.__viewport.add(hbox)
201         self.__toplevel.add_events(
202             gdk.BUTTON_PRESS_MASK | \
203             gdk.BUTTON_RELEASE_MASK | \
204             gdk.BUTTON1_MOTION_MASK)
205         self.__toplevel.connect("size-allocate", lambda w, a: self.__check_position())
206
207         self.__orientation = ibus.ORIENTATION_VERTICAL
208         self.__current_orientation = self.__orientation
209         self.__preedit_visible = False
210         self.__aux_string_visible = False
211         self.__lookup_table_visible = False
212         self.__preedit_string = ""
213         self.__preedit_attrs = pango.AttrList()
214         self.__aux_string = ""
215         self.__aux_attrs = pango.AttrList()
216         self.__lookup_table = None
217
218         self.__cursor_location = (0, 0, 0, 0)
219         self.__moved_cursor_location = None
220
221         self.__recreate_ui()
222
223     def __handle_move_end_cb(self, handle):
224         # store moved location
225         self.__moved_cursor_location = self.__toplevel.get_position() + (self.__cursor_location[2], self.__cursor_location[3])
226
227     def __recreate_ui(self):
228         for w in self:
229             self.remove(w)
230             w.destroy()
231         # create preedit label
232         self.__preedit_label = Label(self.__preedit_string)
233         self.__preedit_label.set_attributes(self.__preedit_attrs)
234         self.__preedit_label.set_alignment(0.0, 0.5)
235         self.__preedit_label.set_padding(8, 0)
236         self.__preedit_label.set_no_show_all(True)
237         if self.__preedit_visible:
238             self.__preedit_label.show()
239
240         # create aux label
241         self.__aux_label = Label(self.__aux_string)
242         self.__aux_label.set_attributes(self.__aux_attrs)
243         self.__aux_label.set_alignment(0.0, 0.5)
244         self.__aux_label.set_padding(8, 0)
245         self.__aux_label.set_no_show_all(True)
246         if self.__aux_string_visible:
247             self.__aux_label.show()
248
249         # create candidates area
250         self.__candidate_area = CandidateArea(self.__current_orientation)
251         self.__candidate_area.set_no_show_all(True)
252         self.__candidate_area.connect("candidate-clicked", lambda x, i, b, s: self.emit("candidate-clicked", i, b, s))
253         # self.update_lookup_table(self.__lookup_table, self.__lookup_table_visible)
254
255         # create state label
256         self.__state_label = Label()
257         self.__state_label.set_size_request(20, -1)
258
259         # create buttons
260         self.__prev_button = gtk.Button()
261         self.__prev_button.connect("clicked", lambda x: self.emit("page-up"))
262         self.__prev_button.set_relief(gtk.RELIEF_NONE)
263         self.__prev_button.set_tooltip_text(_("Previous page"))
264
265         self.__next_button = gtk.Button()
266         self.__next_button.connect("clicked", lambda x: self.emit("page-down"))
267         self.__next_button.set_relief(gtk.RELIEF_NONE)
268         self.__next_button.set_tooltip_text(_("Next page"))
269
270         self.__pack_all_widgets()
271
272     def __pack_all_widgets(self):
273         if self.__current_orientation == ibus.ORIENTATION_VERTICAL:
274             # package all widgets in vertical mode
275             image = gtk.Image()
276             image.set_from_stock(gtk.STOCK_GO_UP, gtk.ICON_SIZE_MENU)
277             self.__prev_button.set_image(image)
278
279             image = gtk.Image()
280             image.set_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU)
281             self.__next_button.set_image(image)
282
283             vbox = gtk.VBox()
284             vbox.pack_start(self.__preedit_label, False, False, 0)
285             vbox.pack_start(self.__aux_label, False, False, 0)
286             self.pack_start(vbox, False, False, 5)
287             self.pack_start(HSeparator(), False, False)
288             self.pack_start(self.__candidate_area, False, False, 2)
289             self.pack_start(HSeparator(), False, False)
290             hbox= gtk.HBox()
291             hbox.pack_start(self.__state_label, True, True)
292             hbox.pack_start(VSeparator(), False, False)
293             hbox.pack_start(self.__prev_button, False, False, 2)
294             hbox.pack_start(self.__next_button, False, False, 2)
295             self.pack_start(hbox, False, False)
296         else:
297             # package all widgets in HORIZONTAL mode
298             image = gtk.Image()
299             image.set_from_stock(gtk.STOCK_GO_UP, gtk.ICON_SIZE_MENU)
300             self.__prev_button.set_image(image)
301
302             image = gtk.Image()
303             image.set_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU)
304             self.__next_button.set_image(image)
305
306             vbox = gtk.VBox()
307             vbox.pack_start(self.__preedit_label, False, False, 0)
308             vbox.pack_start(self.__aux_label, False, False, 0)
309             self.pack_start(vbox, False, False, 5)
310             self.pack_start(HSeparator(), False, False)
311             hbox = gtk.HBox()
312             hbox.pack_start(self.__candidate_area, True, True, 2)
313             hbox.pack_start(VSeparator(), False, False)
314             hbox.pack_start(self.__prev_button, False, False, 2)
315             hbox.pack_start(self.__next_button, False, False, 2)
316             self.pack_start(hbox, False, False)
317
318         # self.hide_all()
319         # self.show_all()
320
321     def show_preedit_text(self):
322         self.__preedit_visible = True
323         self.__preedit_label.show()
324         self.__check_show_states()
325
326     def hide_preedit_text(self):
327         self.__preedit_visible = False
328         self.__preedit_label.hide()
329         self.__check_show_states()
330
331     def update_preedit_text(self, text, cursor_pos, visible):
332         attrs = PangoAttrList(text.attributes, text.text)
333         if visible:
334             self.show_preedit_text()
335         else:
336             self.hide_preedit_text()
337         self.__preedit_stribg = text.text
338         self.__preedit_label.set_text(text.text)
339         self.__preedit_attrs = attrs
340         self.__preedit_label.set_attributes(attrs)
341
342     def show_auxiliary_text(self):
343         self.__aux_string_visible = True
344         self.__aux_label.show()
345         self.__check_show_states()
346
347     def hide_auxiliary_text(self):
348         self.__aux_string_visible = False
349         self.__aux_label.hide()
350         self.__check_show_states()
351
352     def update_auxiliary_text(self, text, show):
353         attrs = PangoAttrList(text.attributes, text.text)
354
355         if show:
356             self.show_auxiliary_text()
357         else:
358             self.hide_auxiliary_text()
359
360         self.__aux_string = text.text
361         self.__aux_label.set_text(text.text)
362         self.__aux_attrs = attrs
363         self.__aux_label.set_attributes(attrs)
364
365     def __refresh_labels(self):
366         labels = self.__lookup_table.get_labels()
367         if labels:
368             labels = map(lambda x: (x.text, PangoAttrList(x.attributes, x.text)), labels)
369         else:
370             labels = None
371         self.__candidate_area.set_labels(labels)
372
373
374     def __refresh_candidates(self):
375         candidates = self.__lookup_table.get_candidates_in_current_page()
376         candidates = map(lambda x: (x.text, PangoAttrList(x.attributes, x.text)), candidates)
377         self.__candidate_area.set_candidates(candidates,
378                 self.__lookup_table.get_cursor_pos_in_current_page(),
379                 self.__lookup_table.is_cursor_visible()
380                 )
381
382     def update_lookup_table(self, lookup_table, visible):
383         # hide lookup table
384         if not visible:
385             self.hide_lookup_table()
386
387         self.__lookup_table = lookup_table or ibus.LookupTable()
388         orientation = self.__lookup_table.get_orientation()
389         if orientation not in (ibus.ORIENTATION_HORIZONTAL, ibus.ORIENTATION_VERTICAL):
390             orientation = self.__orientation
391         self.set_current_orientation(orientation)
392         self.__refresh_candidates()
393         self.__refresh_labels()
394
395         # show lookup table
396         if visible:
397             self.show_lookup_table()
398
399     def show_lookup_table(self):
400         self.__lookup_table_visible = True
401         self.__candidate_area.set_no_show_all(False)
402         self.__candidate_area.show_all()
403         self.__check_show_states()
404
405     def hide_lookup_table(self):
406         self.__lookup_table_visible = False
407         self.__candidate_area.hide_all()
408         self.__candidate_area.set_no_show_all(True)
409         self.__check_show_states()
410
411     def page_up_lookup_table(self):
412         self.__lookup_table.page_up()
413         self.__refresh_candidates()
414
415     def page_down_lookup_table(self):
416         self.__lookup_table.page_down()
417         self.__refresh_candidates()
418
419     def cursor_up_lookup_table(self):
420         self.__lookup_table.cursor_up()
421         self.__refresh_candidates()
422
423     def cursor_down_lookup_table(self):
424         self.__lookup_table.cursor_down()
425         self.__refresh_candidates()
426
427     def set_cursor_location(self, x, y, w, h):
428         # if cursor location is changed, we reset the moved cursor location
429         if self.__cursor_location != (x, y, w, h):
430             self.__cursor_location = (x, y, w, h)
431             self.__moved_cursor_location = None
432             self.__check_position()
433
434     def __check_show_states(self):
435         if self.__preedit_visible or \
436             self.__aux_string_visible or \
437             self.__lookup_table_visible:
438             self.show_all()
439             self.emit("show")
440         else:
441             self.hide_all()
442             self.emit("hide")
443
444     def reset(self):
445         self.update_preedit_text(ibus.Text(""), 0, False)
446         self.update_auxiliary_text(ibus.Text(""), False)
447         self.update_lookup_table(None, False)
448         self.hide()
449
450     def set_current_orientation(self, orientation):
451         if self.__current_orientation == orientation:
452             return
453         self.__current_orientation = orientation
454         self.__recreate_ui()
455         if self.__toplevel.flags() & gtk.VISIBLE:
456             self.show_all()
457
458     def set_orientation(self, orientation):
459         self.__orientation = orientation
460         self.update_lookup_table(self.__lookup_table, self.__lookup_table_visible)
461
462     def get_current_orientation(self):
463         return self.__current_orientation
464
465     # def do_expose_event(self, event):
466     #     self.style.paint_box(self.window,
467     #                 gtk.STATE_NORMAL,
468     #                 gtk.SHADOW_IN,
469     #                 event.area,
470     #                 self,
471     #                 "panel",
472     #                 self.allocation.x, self.allocation.y,
473     #                 self.allocation.width, self.allocation.height)
474
475     #     gtk.VBox.do_expose_event(self, event)
476
477     def do_size_request(self, requisition):
478         gtk.VBox.do_size_request(self, requisition)
479         self.__toplevel.resize(1, 1)
480
481     def __check_position(self):
482         cursor_location = self.__moved_cursor_location or self.__cursor_location
483
484         cursor_right = cursor_location[0] + cursor_location[2]
485         cursor_bottom = cursor_location[1] + cursor_location[3]
486
487         window_right = cursor_right + self.__toplevel.allocation.width
488         window_bottom = cursor_bottom + self.__toplevel.allocation.height
489
490         root_window = gdk.get_default_root_window()
491         sx, sy = root_window.get_size()
492
493         if window_right > sx:
494             x = sx - self.__toplevel.allocation.width
495         else:
496             x = cursor_right
497
498         if window_bottom > sy:
499             # move the window just above the cursor so the window and a preedit string do not overlap.
500             y = cursor_location[1] - self.__toplevel.allocation.height
501         else:
502             y = cursor_bottom
503
504         self.move(x, y)
505
506     def show_all(self):
507         gtk.VBox.show_all(self)
508         self.__toplevel.show_all()
509
510     def hide_all(self):
511         gtk.VBox.hide_all(self)
512         self.__toplevel.hide_all()
513
514     def move(self, x, y):
515         self.__toplevel.move(x, y)
516
517 if __name__ == "__main__":
518     table = ibus.LookupTable()
519     table.append_candidate(ibus.Text("AAA"))
520     table.append_candidate(ibus.Text("BBB"))
521     cp = CandidatePanel()
522     cp.show_all()
523     cp.update_lookup_table(table, True)
524     gtk.main()