OSDN Git Service

658c68987da9fbaac7cc02e455f1e3c0bbf01db8
[fukui-no-namari/fukui-no-namari.git] / src / FukuiNoNamari / thread_view.py
1 # Copyright (C) 2007 by Aiwota Programmer
2 # aiwotaprog@tetteke.tk
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
18 import pygtk
19 pygtk.require('2.0')
20 import gtk
21 import pango
22 import gobject
23 import itertools
24 from FukuiNoNamariExt import thread_view_extend
25
26
27 def get_approximate_char_height(pango_context):
28     desc = pango_context.get_font_description()
29     font = pango_context.load_font(desc)
30     ink, log = font.get_glyph_extents(0)
31     return log[3] / pango.SCALE + 2
32
33
34 class Rectangle:
35     def __init__(self, x, y, width, height):
36         self.x = x
37         self.y = y
38         self.width = width
39         self.height = height
40
41
42 class Line:
43
44     HEIGHT = 15
45
46     def __init__(self, start_index, end_index, rectangle):
47         self.start_index = start_index
48         self.end_index = end_index
49         self.rectangle = rectangle
50
51     def is_on_xy(self, x, y):
52         left = self.rectangle.x
53         right = left + self.rectangle.width
54         top = self.rectangle.y
55         bottom = top + self.rectangle.height
56         return x >= left and x < right and y >= top and y < bottom
57         
58
59 class ElementEmpty:
60
61     def __init__(self, pango_layout):
62         self.pango_layout = pango_layout
63         self.initialize()
64
65     def recalc_char_widths(self):
66         pass
67         
68     def initialize(self):
69         self.line_list = []
70
71     def is_on_xy(self, x, y):
72         for line in self.line_list:
73             if line.is_on_xy(x, y):
74                 return True
75         return False
76
77     def xy_to_index(self, x, y):
78         return 0
79
80     def build_line_list(self, x, y, width, left_margin):
81         self.initialize()
82
83         line = Line(0, 0, Rectangle(
84             x, y, width - x,
85             get_approximate_char_height(self.pango_layout.get_context())))
86         self.line_list.append(line)
87
88         return width, y
89
90     def get_text(self, selection=False, start_index=0, end_index=0xffffff):
91         return ""
92
93     def draw(self, drawingarea, y_offset, pango_layout,
94              selection=False, start_index=0, end_index=0xffffff):
95         pass
96
97     
98 class ElementText:
99
100     ch_width_dict = {}   # key: char, value: width
101
102     def __init__(self, text, pango_layout):
103         self.text = text
104         self.pango_layout = pango_layout
105
106         self.recalc_char_widths()
107
108         self.line_list = []
109
110     def recalc_char_widths(self):
111         self.widths = [i for i in itertools.repeat(0, len(self.text))]
112
113         dict = self._get_ch_width_dict()
114         need_to_get = False
115         for index, ch in enumerate(self.text):
116             if ch not in dict:
117                 need_to_get = True
118                 break
119             else:
120                 width = dict[ch]
121                 self.widths[index] = width
122
123         if need_to_get:
124             attrlist = self._get_attrs()
125             self.widths = thread_view_extend.get_char_width(
126                 self.pango_layout.get_context(), self.text, attrlist)
127             for index, width in enumerate(self.widths):
128                 dict[self.text[index]] = self.widths[index]
129
130     def _get_ch_width_dict(self):
131         return ElementText.ch_width_dict
132
133     def _get_attrs(self):
134         attrs = pango.AttrList()
135         return attrs
136
137     def is_on_xy(self, x, y):
138         for line in self.line_list:
139             if line.is_on_xy(x, y):
140                 return True
141         return False
142
143     def xy_to_index(self, x, y):
144         for line in self.line_list:
145             top = line.rectangle.y
146             bottom = top + line.rectangle.height
147             if y >= top and y < bottom:
148                 sum_of_widths = line.rectangle.x
149                 index = line.start_index
150                 for width in self.widths[line.start_index:line.end_index]:
151                     if sum_of_widths + width/2 > x:
152                         break
153                     sum_of_widths += width
154                     index += 1
155                 return index
156         
157     def build_line_list(self, x, y, width, left_margin):
158         self.line_list = []
159
160         current_line_start_index = 0
161         current_line_x = x
162         current_line_y = y
163         current_line_width = 0
164
165         ch_h = get_approximate_char_height(self.pango_layout.get_context())
166
167         for index, ch in enumerate(self.text):
168             ch_w = self.widths[index]
169             if current_line_x + current_line_width + ch_w > width:
170                 line = Line(
171                     current_line_start_index, index,
172                     Rectangle(
173                     current_line_x, current_line_y,
174                     current_line_width, ch_h))
175                 self.line_list.append(line)
176
177                 current_line_start_index = index
178                 current_line_x = left_margin
179                 current_line_y += ch_h
180                 current_line_width = ch_w
181             else:
182                 current_line_width += ch_w
183
184         if current_line_start_index < len(self.text):
185             line = Line(current_line_start_index, len(self.text),
186                         Rectangle(current_line_x,
187                                           current_line_y,
188                                           current_line_width,
189                                           ch_h))
190             self.line_list.append(line)
191
192             current_line_x += current_line_width
193
194         return current_line_x, current_line_y
195
196     def get_text(self, selection=False, start_index=0, end_index=0xffffff):
197
198         text = ""
199
200         for line in self.line_list:
201
202             t = self.text[line.start_index:line.end_index]
203             if selection:
204                 s = start_index - line.start_index
205                 s = max(s, 0)
206                 s = min(s, line.end_index - line.start_index)
207
208                 e = end_index - line.start_index
209                 e = min(e, line.end_index - line.start_index)
210                 e = max(e, 0)
211
212                 t = t[s:e]
213
214             text += t
215
216         return text
217
218     def draw(self, drawingarea, y_offset, pango_layout,
219              selection=False, start_index=0, end_index=0xffffff):
220
221         if drawingarea.get_property("has-focus"):
222             selection_fg = drawingarea.style.text[gtk.STATE_SELECTED]
223             selection_bg = drawingarea.style.base[gtk.STATE_SELECTED]
224         else:
225             selection_fg = drawingarea.style.text[gtk.STATE_ACTIVE]
226             selection_bg = drawingarea.style.base[gtk.STATE_ACTIVE]
227
228         for line in self.line_list:
229
230             text = self.text[line.start_index:line.end_index]
231             u_text = text.encode("utf8")
232             gc = drawingarea.window.new_gc()
233             gc.set_foreground(drawingarea.style.text[gtk.STATE_NORMAL])
234             gc.set_background(drawingarea.style.base[gtk.STATE_NORMAL])
235             attrs = self._get_attrs()
236             if selection:
237
238                 s = start_index - line.start_index
239                 s = max(s, 0)
240                 s = min(s, line.end_index - line.start_index)
241                 s = len(text[:s].encode("utf8"))
242
243                 e = end_index - line.start_index
244                 e = min(e, line.end_index - line.start_index)
245                 e = max(e, 0)
246                 e = len(text[:e].encode("utf8"))
247                     
248                 selection_all_attr_fg = pango.AttrForeground(
249                     selection_fg.red, selection_fg.green, selection_fg.blue,
250                     s, e)
251                 selection_all_attr_bg= pango.AttrBackground(
252                     selection_bg.red, selection_bg.green, selection_bg.blue,
253                     s, e)
254                 attrs.insert(selection_all_attr_fg)
255                 attrs.insert(selection_all_attr_bg)
256
257             pango_layout.set_text(u_text)
258             pango_layout.set_attributes(attrs)
259             drawingarea.window.draw_layout(
260                 gc, int(line.rectangle.x), line.rectangle.y + y_offset,
261                 pango_layout)
262
263
264 class ElementBoldText(ElementText):
265
266     def _get_attrs(self):
267         attrlist = pango.AttrList()
268         attr = pango.AttrWeight(pango.WEIGHT_BOLD,
269                                    end_index=0xffffff)
270         attrlist.insert(attr)
271         return attrlist
272
273     def recalc_char_widths(self):
274         attrlist = self._get_attrs()
275         self.widths = thread_view_extend.get_char_width(
276             self.pango_layout.get_context(), self.text, attrlist)
277
278
279 class ElementLink(ElementText):
280     
281     def __init__(self, text, href, pango_layout):
282         self.href = href
283         ElementText.__init__(self, text, pango_layout)
284
285     def _get_attrs(self):
286         attrlist = pango.AttrList()
287         attr = pango.AttrUnderline(pango.UNDERLINE_SINGLE,
288                                    end_index=0xffffff)
289         attrlist.insert(attr)
290         return attrlist
291
292
293 class ResLayout:
294 # represent one line
295
296     def __init__(self, left_margin, resnum, pango_layout):
297         self.element_list = [ElementEmpty(pango_layout)]
298         self.width = 0
299         self.height = 0
300         self.pango_layout = pango_layout
301         self.left_margin = left_margin
302         self.resnum = resnum
303         self.posY = 0
304         self.list_index = 0
305
306     def add_text(self, text, bold, href):
307         if isinstance(self.element_list[0], ElementEmpty):
308             self.element_list = []
309
310         if href:
311             element = ElementLink(text, href, self.pango_layout)
312             self.element_list.append(element)
313         elif bold:
314             element = ElementBoldText(text, self.pango_layout)
315             self.element_list.append(element)
316         else:
317             element = ElementText(text, self.pango_layout)
318             self.element_list.append(element)
319
320     def get_element_from_xy(self, x, y):
321         for element in self.element_list:
322             if element.is_on_xy(x, y):
323                 return element
324         return None
325
326     def get_close_element_from_xy(self, x, y):
327         x= max(x, self.left_margin)
328         element = self.get_element_from_xy(x, y)
329         if element is None and len(self.element_list) != 0:
330             element = self.element_list[len(self.element_list) - 1]
331         return element
332
333     def recalc_char_widths(self):
334         for element in self.element_list:
335             element.recalc_char_widths()
336
337     def set_width(self, width):
338
339         self.width = width
340         
341         current_x = self.left_margin
342         current_y = 0
343
344         for element in self.element_list:
345             current_x, current_y = element.build_line_list(
346                 current_x, current_y, width, self.left_margin)
347
348         self.height = current_y + get_approximate_char_height(self.pango_layout.get_context())
349
350     def get_pixel_size(self):
351         return self.width, self.height
352
353     def get_text(self, selection_start, selection_end):
354         s_s = selection_start
355         e_s = selection_end
356         s_l, s_e, s_i = selection_start
357         e_l, e_e, e_i = selection_end
358
359         text = ""
360
361         if (s_l is None or s_e is None or s_i is None or
362             e_l is None or e_e is None or e_i is None or
363             self.posY < s_l.posY or self.posY > e_l.posY):
364
365             # nothing to do
366             pass
367
368         elif self.posY > s_s[0].posY and self.posY < e_s[0].posY:
369
370             for element in self.element_list:
371                 text += element.get_text(selection=True)
372
373         elif self == s_s[0] and self == e_s[0]:
374
375             selection = False
376
377             for element in self.element_list:
378                 if s_e == element:
379                     selection = True
380                     start = s_i
381                     end = 0xffffff
382                     if e_e == element:
383                         end = e_i
384                         selection = False
385                     text += element.get_text(selection=True, start_index=start,
386                                              end_index=end)
387                 elif e_e == element:
388                     end = e_i
389                     selection = False
390                     text += element.get_text(
391                         selection=True, end_index=end)
392                 elif selection:
393                     text += element.get_text(selection=True)
394
395         elif self == s_s[0]:
396
397             selection = False
398
399             for element in self.element_list:
400                 if s_e == element:
401                     selection = True
402                     start = s_i
403                     text += element.get_text(selection=True, start_index=start)
404                 elif selection:
405                     text += element.get_text(selection=True)
406
407         elif self == e_s[0]:
408
409             selection = True
410
411             for element in self.element_list:
412                 if e_e == element:
413                     end = e_i
414                     text += element.get_text(selection=True, end_index=e_i)
415                     selection = False
416                 elif selection:
417                     text += element.get_text(selection=True)
418
419         else:
420             # nothing to do
421             pass
422
423         return text
424
425
426     def draw(self, drawingarea, x_offset, y_offset,
427              start_selection, end_selection):
428
429         s_s = start_selection
430         e_s = end_selection
431
432         s_l, s_e, s_i = s_s
433         e_l, e_e, e_i = e_s
434
435         if (s_l is None or s_e is None or s_i is None or
436             e_l is None or e_e is None or e_i is None or
437             self.posY < s_l.posY or self.posY > e_l.posY):
438
439             for element in self.element_list:
440                 element.draw(drawingarea, y_offset, self.pango_layout)
441
442         elif self.posY > s_s[0].posY and self.posY < e_s[0].posY:
443
444             for element in self.element_list:
445                 element.draw(drawingarea, y_offset, self.pango_layout,
446                              selection=True)
447
448         elif self == s_s[0] and self == e_s[0]:
449
450             selection = False
451
452             for element in self.element_list:
453                 if s_e == element:
454                     selection = True
455                     start = s_i
456                     end = 0xffffff
457                     if e_e == element:
458                         end = e_i
459                         selection = False
460                     element.draw(drawingarea, y_offset, self.pango_layout,
461                                  selection=True,
462                                  start_index=start,
463                                  end_index=end)
464                 elif e_e == element:
465                     end = e_i
466                     selection = False
467                     element.draw(drawingarea, y_offset, self.pango_layout,
468                                  selection=True, end_index=end)
469                 else:
470                     element.draw(drawingarea, y_offset, self.pango_layout,
471                                  selection=selection)
472
473         elif self == s_s[0]:
474
475             selection = False
476
477             for element in self.element_list:
478                 if s_e == element:
479                     selection = True
480                     start = s_i
481                     element.draw(drawingarea, y_offset, self.pango_layout,
482                                  selection=selection, start_index = start)
483                 else:
484                     element.draw(drawingarea, y_offset, self.pango_layout,
485                                  selection=selection)
486
487         elif self == e_s[0]:
488
489             selection = True
490
491             for element in self.element_list:
492                 if e_e == element:
493                     end = e_i
494                     element.draw(drawingarea, y_offset, self.pango_layout,
495                                  selection=selection, end_index=e_i)
496                     selection = False
497                 else:
498                     element.draw(drawingarea, y_offset, self.pango_layout,
499                                  selection=selection)
500
501         else:
502             for element in self.element_list:
503                 element.draw(drawingarea, y_offset, self.pango_layout)
504
505     def clone(self):
506         import copy
507         layout = ResLayout(self.left_margin, self.resnum, self.pango_layout)
508         layout.element_list = []
509         for element in self.element_list:
510             layout.element_list.append(copy.copy(element))
511         return layout
512
513
514 class ThreadView(gtk.DrawingArea):
515     __gsignals__ = {
516         "cursor-over-link-event":
517         (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (object, object, )),
518         "uri-clicked-event":
519         (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (object, )),
520         "set-scroll-adjustments" : (gobject.SIGNAL_RUN_LAST,
521                                    gobject.TYPE_NONE,
522                                    (gtk.Adjustment, gtk.Adjustment)),
523         "configure-event": "override",
524         "expose-event": "override",
525         "motion-notify-event": "override",
526         "button-press-event": "override",
527         "button-release-event": "override",
528         "style-set": "override",
529         "key-press-event": "override"
530         }
531
532     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
533     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
534     arrow_cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
535
536     def __init__(self):
537         super(ThreadView, self).__init__()
538         self.set_set_scroll_adjustments_signal("set-scroll-adjustments")
539         self.set_property("can_focus", True)
540
541         #self.adjustment = gtk.Adjustment()
542
543         self.add_events(
544             gtk.gdk.KEY_PRESS_MASK |
545             gtk.gdk.SCROLL_MASK |
546             gtk.gdk.POINTER_MOTION_MASK |
547             gtk.gdk.BUTTON_PRESS_MASK |
548             gtk.gdk.BUTTON_RELEASE_MASK)
549
550         self.drawingarea_prev_width = 0
551
552         self.pango_layout = self.create_pango_layout("")
553
554         self.initialize_buffer()
555
556         self.button1_pressed = False
557         self.current_pressed_uri = None
558             
559         self.popupmenu = None
560         self.menu_openuri = None
561         self.menu_copylinkaddress = None
562         self.menu_separator_link = None
563         self.menu_copyselection = None
564         self.menu_openasuri = None
565         self.menu_separator_selection = None
566
567         self.menud_uri = None
568
569         # for selection
570         self.button_pressed_pt = (None, None, None)
571         self.button_moving_pt = (None, None, None)
572
573     def initialize_buffer(self):
574         self.res_layout_list = []
575
576     def add_layout(self, res_layout):
577         if (len(self.res_layout_list) != 0):
578             last = self.res_layout_list[len(self.res_layout_list)-1]
579             x, y = last.get_pixel_size()
580             res_layout.posY = last.posY + y
581         self.set_layout_width(res_layout)
582         res_layout.list_index = len(self.res_layout_list)
583         self.res_layout_list.append(res_layout)
584
585         x, y = res_layout.get_pixel_size()
586         self.adjustment.upper = res_layout.posY + y
587         # do not use this method in a loop because expensive.
588         # self.redraw()
589
590     def create_res_layout(self, left_margin, resnum):
591         return ResLayout(left_margin, resnum, self.pango_layout)
592
593     def set_layout_width(self, layout):
594         width = self.allocation.width
595         layout.set_width(width)
596         
597     def redraw(self):
598         self.queue_draw()
599
600     def wrap_relayout(self):
601         # before relayout, find top layout on gdkwindow
602         top_layout = self.get_layout_on_y(self.adjustment.value)
603         delta = 0
604
605         if top_layout is not None:
606             delta = top_layout.posY - self.adjustment.value
607
608         self.relayout()
609         self.drawingarea_prev_width = self.allocation.width
610
611         # after relayout, set adjustment.value to top layout's posY
612         if top_layout is not None:
613             self.adjustment.value = top_layout.posY - delta
614
615     def relayout(self):
616         width = self.allocation.width
617         sum_height = 0
618         for layout in self.res_layout_list:
619             layout.set_width(width)
620             layout.posY = sum_height
621             x, y = layout.get_pixel_size()
622             sum_height += y
623
624         self.adjustment.upper = sum_height
625
626     def _set_adjustment_value(self, value):
627         value = min(self.adjustment.upper - self.adjustment.page_size, value)
628         value = max(self.adjustment.lower, value)
629         self.adjustment.value = value
630
631     def jump(self, value):
632         self._set_adjustment_value(value)
633
634     def jump_to_layout(self, layout):
635         self.jump(layout.posY)
636         
637     def jump_to_the_end(self):
638         value = self.adjustment.upper - self.adjustment.page_size
639         self._set_adjustment_value(value)
640
641     def jump_to_res(self, resnum):
642         for layout in self.res_layout_list:
643             if layout.resnum == resnum:
644                 self.jump_to_layout(layout)
645                 return True
646         return False
647
648
649     def _get_selection_start_end(self):
650         pressed_layout, pressed_element, pressed_index = self.button_pressed_pt
651         moving_layout, moving_element, moving_index = self.button_moving_pt
652
653         if (pressed_layout is None or pressed_element is None or
654             pressed_index is None or moving_layout is None or
655             moving_element is None or moving_index is None):
656             return (None, None, None), (None, None, None)
657         
658         if pressed_layout == moving_layout:
659             if pressed_element == moving_element:
660                 if moving_index < pressed_index:
661                     return self.button_moving_pt, self.button_pressed_pt
662             else:
663                 pressed_element_index = pressed_layout.element_list.index(
664                     pressed_element)
665                 moving_element_index = moving_layout.element_list.index(
666                     moving_element)
667                 if moving_element_index < pressed_element_index:
668                     return self.button_moving_pt, self.button_pressed_pt
669         elif moving_layout.posY < pressed_layout.posY:
670             return self.button_moving_pt, self.button_pressed_pt
671
672         return self.button_pressed_pt, self.button_moving_pt
673
674     def draw_viewport(self, area):
675         view_y = self.adjustment.value
676         self.window.draw_rectangle(
677             self.style.base_gc[0],
678             True, area.x, area.y, area.width, area.height)
679
680         selection_start, selection_end = self._get_selection_start_end()
681
682         top_layout = self.get_layout_on_y(view_y)
683         if top_layout is None:
684             return
685         #area_top = view_y + area.y
686         area_bottom = view_y + area.y + area.height
687
688         iter = range(top_layout.list_index, len(self.res_layout_list))
689         iter = itertools.imap(lambda index: self.res_layout_list[index], iter)
690         iter = itertools.takewhile(lambda lay: lay.posY <= area_bottom, iter)
691         for layout in iter:
692             layout.draw(self, 0, layout.posY - int(view_y),
693                 selection_start, selection_end)
694
695     def transform_coordinate_gdk_to_adj(self, y):
696         return y + self.adjustment.value
697
698     def transform_coordinate_adj_to_layout(self, x, y, layout):
699         return x, y - layout.posY
700
701     def transform_coordinate_gdk_to_layout(self, x, y, layout):
702         return self.transform_coordinate_adj_to_layout(
703             x, self.transform_coordinate_gdk_to_adj(y), layout)
704
705     def get_layout_on_y(self, y):
706
707         def binary_search(lst, start, end, func):
708
709             if end - start <= 0:
710                 return None
711
712             m = (start + end) / 2
713             ret = func(lst[m])
714
715             if ret == 0:
716                 return m
717             if ret > 0:
718                 return binary_search(lst, start, m, func)
719             return binary_search(lst, m+1, end, func)
720
721         def on_y(layout, _y):
722             top = layout.posY
723             width, height = layout.get_pixel_size()
724             bottom = top + height
725             if _y >= top and _y < bottom:
726                 return 0
727             if _y < top:
728                 return 1
729             return -1
730
731         ret = binary_search(
732             self.res_layout_list, 0, len(self.res_layout_list),
733             lambda x: on_y(x, y))
734         if ret is not None:
735             return self.res_layout_list[ret]
736         return None
737
738     def ptrpos_to_layout(self, x, y):
739         # transform coordinate, GdkWindow -> adjustment
740         adj_y = self.transform_coordinate_gdk_to_adj(y)
741         return self.get_layout_on_y(adj_y)
742
743     def ptrpos_to_uri(self,  x, y):
744         # x, y is GdkWindow coordinate
745
746         layout = self.ptrpos_to_layout(x, y)
747
748         if layout is None:
749             return None, None, None
750
751         # transform coordinate, GdkWindow -> res_layout_list
752         lay_x, lay_y = self.transform_coordinate_gdk_to_layout(x, y, layout)
753
754         # xy -> element
755         element = layout.get_element_from_xy(lay_x, lay_y)
756         if isinstance(element, ElementLink):
757             return element.href, layout, element
758
759         return None, layout, None
760
761     def get_selected_text(self):
762         selection_start, selection_end = self._get_selection_start_end()
763         s_l, s_e, s_i = selection_start
764         e_l, e_e, e_i = selection_end
765
766         if (s_l is None or s_e is None or s_i is None or
767             e_l is None or e_e is None or e_i is None):
768             return ""
769
770         text = ""
771         index = s_l.list_index
772         end = e_l.list_index
773
774         while index <= end:
775             layout = self.res_layout_list[index]
776
777             text += layout.get_text(selection_start, selection_end)
778             if index != end:
779                 text += "\n"
780
781             index += 1
782
783         return text
784
785     def _set_button_pressed_pt(self, pt):
786         self.button_pressed_pt = (None, None, None)
787         if pt == None:
788             return
789
790         x, y = pt
791         layout = self.ptrpos_to_layout(x, y)
792         if layout is None:
793             return
794
795         x, y = self.transform_coordinate_gdk_to_layout(x, y, layout)
796         element = layout.get_element_from_xy(x, y)
797         if element is None:
798             element = layout.get_close_element_from_xy(x, y)
799
800         if element is None:
801             return
802
803         index = element.xy_to_index(x, y)
804         if index is None:
805             return
806
807         self.button_pressed_pt = (layout, element, index)
808
809     def _set_button_moving_pt(self, pt):
810         self.button_moving_pt = (None, None, None)
811         if pt == None:
812             return
813
814         x, y = pt
815         layout = self.ptrpos_to_layout(x, y)
816         if layout is None:
817             return
818
819         x, y = self.transform_coordinate_gdk_to_layout(x, y, layout)
820         element = layout.get_element_from_xy(x, y)
821         if element is None:
822             element = layout.get_close_element_from_xy(x, y)
823
824         if element is None:
825             return
826
827         index = element.xy_to_index(x, y)
828         if index is None:
829             return
830
831         self.button_moving_pt = (layout, element, index)
832
833     def do_expose_event(self, event):
834         self.draw_viewport(event.area)
835
836     def do_configure_event(self, event):
837         if event.width != self.drawingarea_prev_width:
838             self.wrap_relayout()
839
840         self.adjustment.page_size = self.allocation.height
841         self.adjustment.step_increment = 20
842         self.adjustment.page_increment = self.allocation.height
843
844         # re-set 'value' for prevent overflow
845         self._set_adjustment_value(self.adjustment.value)
846
847     def on_adjustment_value_changed(self, widget, data=None):
848         self.queue_draw()
849
850     def do_motion_notify_event(self, event):
851         if event.state & gtk.gdk.BUTTON1_MASK != gtk.gdk.BUTTON1_MASK:
852             self.button1_pressed = False
853
854         if self.button1_pressed and self.current_pressed_uri is None:
855             old_lay, old_elem, old_idx = self.button_moving_pt
856             self._set_button_moving_pt((event.x, event.y))
857             new_lay, new_elem, new_idx = self.button_moving_pt
858             if (old_lay != new_lay
859                 or old_elem != new_elem
860                 or old_idx != new_idx):
861                 view_y = self.adjustment.value
862                 o_y = old_lay.posY
863                 n_y = new_lay.posY
864                 o_width, o_height = old_lay.get_pixel_size()
865                 n_width, n_height = new_lay.get_pixel_size()
866
867                 y = min(o_y, n_y)
868                 height = max(o_y, n_y) - y
869                 if o_y > n_y: height += o_height
870                 else: height += n_height
871
872                 y -= view_y
873
874                 self.queue_draw_area(0, y, n_width, height+1)
875                 #self.window.process_updates(False)
876
877         cursor = ThreadView.regular_cursor
878
879         uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
880         if layout is None:
881             cursor = ThreadView.arrow_cursor
882         else:
883             if uri is not None and uri != "":
884                 cursor = ThreadView.hand_cursor
885                 self.emit("cursor-over-link-event", event, uri)
886
887         self.window.set_cursor(cursor)
888
889     def do_button_press_event(self, event):
890         if event.button == 1:
891             self.current_pressed_uri = None
892             self.button1_pressed = True
893             uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
894             if uri is not None and layout is not None and element is not None:
895                 self.current_pressed_uri = (uri, layout, element)
896             else:
897                 self._set_button_moving_pt((event.x, event.y))
898                 self._set_button_pressed_pt((event.x, event.y))
899                 self.queue_draw()
900                 
901         elif event.button == 3:
902             time = event.time
903             uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
904             if uri is not None and layout is not None and element is not None:
905                 self.menu_openuri.show()
906                 self.menu_copylinkaddress.show()
907                 self.menu_separator_link.show()
908                 self.menu_openuri.uri = uri
909                 self.menu_copylinkaddress.uri = uri
910             else:
911                 self.menu_openuri.hide()
912                 self.menu_copylinkaddress.hide()
913                 self.menu_separator_link.hide()
914                 self.menu_openuri.uri = None
915                 self.menu_copylinkaddress.uri = None
916
917             text = self.get_selected_text()
918             if text and len(text) > 0:
919                 self.menu_copyselection.show()
920                 self.menu_openasuri.show()
921                 self.menu_separator_selection.show()
922             else:
923                 self.menu_copyselection.hide()
924                 self.menu_openasuri.hide()
925                 self.menu_separator_selection.hide()
926
927             self.popupmenu.popup(None, None, None, event.button, time)
928             return True
929             
930
931     def do_button_release_event(self, event):
932         if event.button == 1:
933             button1_pressed = self.button1_pressed
934             self.button1_pressed = False
935
936             if button1_pressed and self.current_pressed_uri is not None:
937                 uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
938                 p_uri, p_layout, p_element = self.current_pressed_uri
939                 self.current_pressed_uri = None
940                 if (uri == p_uri and layout == p_layout and
941                     element == p_element):
942                     self.emit("uri-clicked-event", uri)
943
944     def do_style_set(self, previous_style):
945         if previous_style is None:
946             return False
947
948         new = self.style.font_desc.hash()
949         old = previous_style.font_desc.hash()
950         if new != old:
951             for layout in self.res_layout_list:
952                 layout.recalc_char_widths()
953             self.wrap_relayout()
954
955     def do_key_press_event(self, event):
956         if event.type is not gtk.gdk.KEY_PRESS:
957             return
958
959         if event.keyval in (gtk.keysyms.Up, gtk.keysyms.Down,
960                             gtk.keysyms.Page_Up, gtk.keysyms.Page_Down,
961                             gtk.keysyms.Home):
962             value = self.adjustment.value
963             if event.keyval == gtk.keysyms.Up:
964                 step_increment = self.adjustment.step_increment
965                 value = value - step_increment
966             elif event.keyval == gtk.keysyms.Down:
967                 step_increment = self.adjustment.step_increment
968                 value = value + step_increment
969             elif event.keyval == gtk.keysyms.Page_Up:
970                 step_increment = self.adjustment.page_increment
971                 value = value - step_increment
972             elif event.keyval == gtk.keysyms.Page_Down:
973                 step_increment = self.adjustment.page_increment
974                 value = value + step_increment
975             elif event.keyval == gtk.keysyms.Home:
976                 value = 0
977             self.jump(value)
978         elif event.keyval == gtk.keysyms.End:
979             self.jump_to_the_end()
980
981     def do_set_scroll_adjustments(self, hadjustment, vadjustment):
982         self.adjustment = vadjustment
983         if self.adjustment is not None:
984             self.adjustment.connect(
985                 "value-changed", self.on_adjustment_value_changed)
986
987
988 class ThreadViewScrollbar(gtk.VScrollbar):
989
990     def __init__(self, adjustment=None):
991         super(ThreadViewScrollbar, self).__init__(adjustment)
992         self.set_property("no-show-all", True)
993         adjustment.connect("changed", self.on_adjustment_changed)
994
995     def change_vscrollbar_visible(self):
996         adjustment = self.get_adjustment()
997         if adjustment.upper <= adjustment.page_size:
998             self.hide()
999         else:
1000             self.show()
1001
1002     def on_adjustment_changed(self, widget, data=None):
1003         self.change_vscrollbar_visible()
1004
1005
1006 class ThreadViewContainer(gtk.HBox):
1007
1008     def __init__(self, child):
1009         super(ThreadViewContainer, self).__init__(False, 2)
1010         adjustment = gtk.Adjustment()
1011         self.drawingarea = child
1012         child.set_scroll_adjustments(None, adjustment)
1013         self.vscrollbar = ThreadViewScrollbar(adjustment)
1014         self.pack_start(child)
1015         self.pack_start(self.vscrollbar, expand=False)
1016
1017         child.connect("scroll-event", self.on_child_scroll_event)
1018
1019     def on_child_scroll_event(self, widget, event, data=None):
1020         self.vscrollbar.emit("scroll-event", event)